Skip to content

refactor: reduce switchboard to the preview-deployment daemon - #6

Open
luthermonson wants to merge 2 commits into
mainfrom
refactor/daemon-split
Open

refactor: reduce switchboard to the preview-deployment daemon#6
luthermonson wants to merge 2 commits into
mainfrom
refactor/daemon-split

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

What this is

Reduces ephpm/switchboard to the daemon half of the split we designed: the
HTTP/API surface (webhook receiver) now lives in ephpm/switchboard-api (PHP),
and this repo becomes a queue worker that consumes job files, provisions
previews, and reports to GitHub. Built against switchboard-api's README.md
("The job file contract") and MIGRATION.md.

Do not merge — reporting for review. All checks green locally
(cargo build, 103 tests, cargo clippy --all-targets -- -D warnings,
cargo +nightly fmt --check).

Removed (moved to switchboard-api)

  • src/webhook.rs — signature verification + PullRequestEvent. Deleted.
  • The axum HTTP server, /webhook and /health routes, AppState, and the
    webhook secret. axum is dropped from Cargo.toml.

preview_label is kept (now cfg(test)) as the Rust half of the
cross-implementation guard against switchboard-api's PreviewLabel port —
if either side changes the algorithm, one of the two suites fails.

New modules

File Purpose
job.rs Schema-1 job deserialization. schema is rejected first; then every field that reaches git/the filesystem (head.ref, head.sha, owner/repo, clone URL, pull_ref) is re-validated against strict patterns. Defense in depth — the queue is the trust boundary now.
queue.rs The watcher: scan queue/, hardlink-claim (link(), not rename()), coalesce newest-per-label, dedup, dispatch on intent. Retains failed jobs in claimed/ for inspection.
app_auth.rs Mints installation tokens in memory. The token type redacts itself in Debug/Display, is never written to disk. Fails closed if the App key is not 0600.
git_askpass.rs Passes the token to git via GIT_ASKPASS env, not argv; the helper script holds no secret (reads it from the environment).
site.rs Ports ePHPm's canonical site-key derivation (Router::resolve_site + is_valid_site_key) so the vhost dir, <key>.db, the temp/session root and <key>.toml all match the key ePHPm derives from Host.

Changed

  • deployer.rs — consumes a Job; clones via refs/pull/<n>/head from the
    base repo (works for forks/deleted forks) with GIT_ASKPASS auth; writes
    the #391 <key>.toml docroot override; teardown now removes the vhost dir,
    the per-site DB (<key>.db + -wal/-shm/-journal, which live outside the
    vhost), the per-vhost temp/session root, and the override file
    .
  • github.rs — Deployments API lifecycle: create Deployment as the first
    action → queuedin_progresssuccess (with environment_url) /
    failure; inactive on teardown. Failed builds also post a marker'd PR comment
    with the log.
  • config.rs — dropped listen/webhook_secret; added the queue, teardown
    (sqlite_dir, site_overrides_dir, vhost_temp_base, sites_domain_suffix),
    and fork-policy knobs.
  • main.rs — orchestrator + queue loop, graceful shutdown on SIGTERM/Ctrl-C.

Security fix (a real hole in today's code)

The pre-split materialize_env resolved operator ${secret.NAME} into any
fork PR's preview environment — building untrusted code with your secrets. Fixed:

  • fork deploys are refused unless --allow-fork-deploy (a second gate over
    switchboard-api's SWITCHBOARD_ALLOW_FORKS);
  • even when allowed, a fork gets no operator secrets unless --fork-secrets;
  • fork teardowns are always processed.

Design decisions forced by the constraints

  • #400 (Composer broken under ephpm php)build: and the implicit
    composer install run the system composer/PHP (--composer), never
    ephpm php. Documented.
  • DB seeding can't run from a shellseed: children have no $_SERVER DB
    creds; documented that DB seeding must go over HTTP into the running site, as
    wordpress-sample does.
  • Canonical site key — ported ePHPm's derivation rather than inventing a
    second one (the #290/#291 anti-pattern). --sites-domain-suffix must match
    ePHPm's; default (unset) names the vhost dir by the full FQDN.
  • Teardown temp-root — ePHPm names the per-vhost state root with a hash over
    the container path. The daemon reproduces it and removes by the unique
    <key>- prefix as a fallback, since the hash could differ across processes
    (different TMPDIR, or a std-hasher change). The DB/override/vhost dir — the
    persistent leaks — are named deterministically and don't depend on the hash.
  • MSRV 1.88 — the daemon uses let-chains; rust-version and the msrv CI
    job bumped from 1.85 to 1.88.

Tests

103 passing, covering: job parse/validation (schema rejection, argument-injection
refs, bad SHA, off-host clone URL, pull_ref mismatch, fork fallback); queue claim
/dedup/coalescing (newest-per-label, teardown-supersedes-deploy, retain-on-fail,
quarantine-invalid, no-reprocess-of-claimed); teardown completeness (DB + temp +
override + vhost all removed, prefix-fallback for a mismatched digest); the
fork-secret withholding; canonical site-key parity; token redaction; askpass
never leaking into argv.

Split switchboard in two: the webhook receiver moved to ephpm/switchboard-api
(PHP), and this repo becomes the daemon half — a queue worker, not an HTTP
server. It consumes switchboard-api's job files, provisions previews, and
reports to GitHub via the Deployments API.

Removed
- webhook.rs (signature verification + PullRequestEvent) — now in the API.
- The axum HTTP server, /webhook and /health routes, and the webhook secret.

New
- job.rs: schema-1 job deserialization with strict, defense-in-depth validation
  (ref/sha/clone-url/pull_ref re-checked before any value reaches git).
- queue.rs: the watcher — scan, hardlink-claim, per-label coalescing, dedup,
  dispatch on intent.
- app_auth.rs: in-memory GitHub App token minting; the token type redacts itself
  in logs and is never written to disk. Fails closed on a non-0600 App key.
- git_askpass.rs: passes the token to git via GIT_ASKPASS env, never argv/disk.
- site.rs: ports ePHPm's canonical site-key derivation so the vhost dir, the
  per-site DB, the temp/session root and the override file all agree with the
  key ePHPm derives from the Host header.
- preview.rs: reduced preview_host; preview_label kept (cfg(test)) as the
  cross-implementation guard against switchboard-api's PreviewLabel port.

Changed
- deployer.rs: consumes a Job; clones via refs/pull/<n>/head with GIT_ASKPASS
  auth; writes the #391 docroot override; teardown now removes the vhost dir,
  the per-site DB (+ -wal/-shm/-journal, which live outside the vhost), the
  per-vhost temp/session root, and the override file.
- github.rs: Deployments API lifecycle (queued -> in_progress -> success/failure,
  inactive on teardown) plus a failure-only PR comment.
- config.rs: drop listen/webhook_secret; add queue, teardown, and fork-policy knobs.

Security fixes called out
- Fork PRs no longer receive operator secrets by default (the old materialize_env
  resolved ${secret.NAME} for any fork). Fork deploys are refused unless
  --allow-fork-deploy; even then secrets are withheld unless --fork-secrets.

Notes
- build: runs system composer/PHP, not `ephpm php` (issue #400).
- seed: DB seeding must go over HTTP into the running site (documented).
- MSRV bumped to 1.88 (let-chains); msrv CI job pinned to match.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant