Skip to content

Build, install and roll back a darwin-arm64 fork binary on this Mac - #34

Merged
tomkaczocha merged 9 commits into
mainfrom
t3/factory-issue-3
Oct 15, 2026
Merged

tomkaczocha merged 9 commits into
mainfrom
t3/factory-issue-3

Conversation

@rawphp

@rawphp rawphp commented Oct 15, 2026

Copy link
Copy Markdown

Closes #3

Contract

  • done: factory/scripts/build.sh builds release/t3fac-<version>-darwin-arm64.tar.gz through upstream's apps/server/scripts/cli.ts build-exe and scripts/build-cli-archive.ts (none copied), version = factory/UPSTREAM tag minus v when nightly, else apps/server/package.json, then +fac.<short sha>; it ends with a smoke test of the built t3 serve (200 on /.well-known/t3/environment within 60 s, factory started in the server log, server stopped, temp dir deleted). install.sh <archive> unpacks to ~/.local/share/t3fac/versions/<version>/, points current at it, writes the LaunchAgent plist from factory/launchd/io.github.rawphp.t3fac.plist, boots the job out when loaded and bootstraps it. rollback.sh points current at the version installed before it and restarts; with none it exits 1 and says so. A vitest test drives both scripts with HOME in a temp dir, fake archives and a stub launchctl.
  • out: npm packages, desktop app, other platforms, notarisation, CI, sync, running install.sh against the real HOME, multi-level rollback.
  • risk: none
  • test: vp test run apps/server/src/factory → pass (22 tests). factory/scripts/build.sh run by hand at the tip → pass (smoke output below).

Acceptance criteria

  • build.sh produces release/t3fac-<version>-darwin-arm64.tar.gz with t3, client/, resource-monitor/, node_modules/ via upstream's scripts — factory/scripts/build.sh:49-66; archive listing checked with tar -tzf (top level: t3, client, resource-monitor, node_modules).
  • Version rule (nightly tag minus v, else package.json, then +fac.<sha>) — installScripts.test.ts "build.sh --print-version" cases; the real build printed 0.0.46-nightly.20261015.3145+fac.a2df0cec7.
  • Smoke test: serve on a free port, 200 within 60 s, log holds factory started, server stopped, temp dir deleted, non-zero on any failure — factory/scripts/build.sh:73-111; output below.
  • install.sh: versions dir, current symlink, plist from template, bootout when loaded then bootstrap; job args current/t3 serve --base-dir ~/.config/t3fac --port 3876; logs in ~/Library/Logs/t3fac/ — tests "installs the first version and bootstraps the job without a bootout", "keeps the old version as previous and boots the job out first when loaded".
  • rollback.sh points current at the earlier version and restarts; exit 1 with a message when none — tests "rolls back to the previous version", "exits 1 ... when there is no earlier version".
  • Test with HOME in a temp dir, fake archive, stub launchctl on PATH; checks symlink, plist, launchctl calls — apps/server/src/factory/installScripts.test.ts.
  • No script touches ~/.t3, ~/.config/softfac, 3773 or 3874 — grep -nE '\.t3($|[^a-z])|softfac|3773|3874' over the scripts and plist finds nothing; the smoke runs with HOME set to its temp dir.

build.sh smoke output (run at a2df0ce on this Mac)

build: version 0.0.46-nightly.20261015.3145+fac.a2df0cec7 (SEA node 26.8.2)
smoke: serve on 127.0.0.1:63573
smoke: /.well-known/t3/environment -> 200 after 4s
smoke: log holds "factory started"
smoke: server stopped, temp dir removed
/Users/tomkaczocha/.config/softfac/t3/worktrees/t3code-factory/t3-e9d892fa/release/t3fac-0.0.46-nightly.20261015.3145+fac.a2df0cec7-darwin-arm64.tar.gz

Two things the build host needed, both noted in the script or here rather than hidden:

  • The cargo step runs from / with its own CARGO_HOME under native/resource-monitor/target/. Cargo reads .cargo/config.toml from every parent of the working directory and from CARGO_HOME, and this Mac's home-level config replaces crates.io with a private registry whose credentials expire, which failed the first run. The lockfile pins crates.io, so the build ignores host cargo config.
  • This Mac's default stable rustc is 1.94.0 and the sysinfo crate needs 1.95, which rustup has installed. The runs above used RUSTUP_TOOLCHAIN=1.95.0. rustup update stable removes the need; the script does not pin a toolchain.

Gate

  • vp test run apps/server/src/factory → exit 0 (4 files, 22 tests)
  • vp run --filter t3 typecheck → exit 0
  • factory/scripts/check-seams.sh → exit 0 (no upstream file changed; factory/SEAMS unchanged)

Models

  • maker: claude-sonnet (host)
  • checker: claude-opus (host)

Review

  • cycles: 3
  • fixed: install.sh wiped versions/<v> before validating the archive and accepted an empty version (now unpacks beside, checks t3, then swaps; rejects empty, ., .., /); bootstrap raced bootout in both scripts (now waits up to 10 s for launchctl print to report unloaded); rollback deleted previous before the restart succeeded (now after); build.sh probes had no per-request deadline (now curl --max-time 2, 60 s overall); mutation pass: 9/9 named mutants killed, one untested branch (reinstalling the current version keeps previous) covered in cycle 3 with two test nits.
  • leftovers: none

Lessons

  • Run each heavyweight build step by hand once before the first fix packet. Symptom: the maker's first build.sh failed on the host's cargo config, and the smoke loop would have hung next. Cause: a long script surfaces one host failure per run. Default: after a build script lands, the orchestrator runs its steps one at a time from the worktree, collects every host failure, and sends one fix packet.
  • Hand checks that start a server go in a script under .factory/, not an inline command. Symptom: an inline Bash with kill and rm -rf was denied by the host, and a second attempt stalled with no output. Default: write the check to .factory/<name>.sh, run it with a bounded timeout, and read its log file.
  • Remove a review worktree under .factory/ before running a path-filtered test gate. Symptom: vp test run apps/server/src/factory reported 8 files instead of 4. Cause: the detached reviewer worktree lived inside the main worktree, so the path filter matched its copy of the tests. Default: git worktree remove it as soon as the reviewer reports, and before the gate.

Commits

  • 212cf434e test: install, rollback and build version scripts — Make slice (red)
  • 10a3b70f5 feat(factory): build, install and rollback scripts for the darwin-arm64 binary — Make slice (green)
  • fb7399f8b fix(factory): build.sh ignores host cargo config and bounds each smoke probe — Make fix after the first real build (no red phase: shell behavior proven by rerunning build.sh)
  • 3c841833d test: install keeps the live version on a bad archive; restart waits for launchd to unload — Check cycle 1 (red)
  • d0229babc fix(factory): install validates the archive before replacing a version; restart waits for bootout to finish — Check cycle 1 (green)
  • a2df0cec7 test: reinstalling the current version keeps previous; invalid names and restart retry — Check cycle 2 (tests for behavior that already works; red shown by removing the guards locally)

Files

  • factory/scripts/build.sh — build through upstream's scripts, rename the archive, smoke test
  • factory/scripts/install.sh — unpack, switch current, write plist, restart job
  • factory/scripts/rollback.sh — switch current to previous, restart job
  • factory/launchd/io.github.rawphp.t3fac.plist — LaunchAgent template with @HOME@ placeholder
  • apps/server/src/factory/installScripts.test.ts — install, rollback and version-rule tests
  • factory/README.md — the three commands

Red on purpose: install.sh, rollback.sh and build.sh do not exist yet.

Issue: #3
…e probe

The cargo step now runs from / with its own CARGO_HOME under the gitignored target/, so a host-level crates.io source replacement in a parent or home cargo config no longer breaks the build.

Each smoke probe gets curl --max-time 2, and the poll loop breaks once 60s have elapsed, so a connection that hangs while the server initialises cannot stall the build.

Issue: #3
…for launchd to unload

The launchctl stub now keeps the job loaded for two print polls after bootout and fails bootstrap with EIO while loaded (or when LAUNCHCTL_BOOTSTRAP_FAIL is set).

Red before the fix: the second install and the rollback exit 5 because the scripts bootstrap straight after bootout. The new failure-case describe cannot even set up (its second install hits the same exit 5), so the bad-archive, empty-version and failed-restart cases are skipped rather than individually red.

Issue: #3
…n; restart waits for bootout to finish

Issue: #3
…and restart retry

Closes three gaps from the testing review:
- reinstalling the current version leaves previous untouched, so rollback still lands on the older version
- invalid-version guard is tested for both the empty and '..' archive names
- the failed-restart rollback retry now asserts it ends with bootstrap instead of re-checking current

Issue: #3
@rawphp
rawphp marked this pull request as ready for review October 15, 2026 11:03
@rawphp

rawphp commented Oct 15, 2026

Copy link
Copy Markdown
Author

Factory gate: passed (attempt 1, run by the factory in a fresh checkout of the pull request's head a2df0ce merged with the base branch tip f1dba9a)

Command Exit Time
vp test run apps/server/src/factory 0 5.7 s
vp run --filter t3 typecheck 0 6.1 s

@rawphp

rawphp commented Oct 15, 2026

Copy link
Copy Markdown
Author

Factory gate: passed again (attempt 2, run by the factory in a fresh checkout of the pull request's head a2df0ce merged with the base branch tip b679ddd)

Command Exit Time
factory/scripts/check.sh 0 16.3 s

…dirty builds

- build.sh smoke-tests the archive in its stage dir and moves it to release/ only
  after the smoke passes, so a failed smoke leaves no archive install.sh would take.
  T3FAC_SMOKE_TIMEOUT sets the deadline (60 s).
- Uncommitted changes to tracked files add `.dirty` to the version, so a rebuild of
  the same commit never replaces the clean build on install.
- install.sh and rollback.sh source factory/scripts/lib/t3fac.sh for the install
  root, the LaunchAgent label and plist, and restart_job. T3FAC_UNLOAD_WAIT sets the
  unload wait (10 s).
- Tests: the whole of build.sh with stubbed vp and cargo (pass, early exit, no 200,
  no "factory started"), a dangling `previous`, and a job that never unloads.
…ed changes

Two builds of one commit with different uncommitted edits used to share the
`.dirty` version, so installing the second deleted the first. The suffix is now
`.dirty.<8 hex>` from `git diff HEAD --binary`, so only identical contents share a
name.
A version named after the commit could not tell builds of one commit apart when
the tree held edits or untracked files, and installing a second such build
deleted the first. build.sh now refuses to build when `git status --porcelain`
lists anything, naming the first ten paths, so the version always names exactly
what was built. The `.dirty` suffix is gone.

.worktrees/ joins .gitignore (a new seam in factory/SEAMS) so the main checkout,
which holds local worktrees, counts as clean.
@tomkaczocha
tomkaczocha merged commit aeb0727 into main Oct 15, 2026
@rawphp
rawphp deleted the t3/factory-issue-3 branch October 28, 2026 02:53
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.

Build, install and roll back a darwin-arm64 fork binary on this Mac

2 participants