feat(deploy): scripted, idempotent deploy step for installed tg checkouts (tg-cli #36 deploy follow-up)#41
feat(deploy): scripted, idempotent deploy step for installed tg checkouts (tg-cli #36 deploy follow-up)#41alex-mextner wants to merge 1 commit into
Conversation
…outs tg is a committed Bun script run via a symlink (tg -> <checkout>/tg), so the checked-out file IS the running binary — there is no build step. Deploying a merged change is a guarded fast-forward of the checkout the symlink points at. scripts/deploy.sh makes that one-step deploy safe and idempotent: - resolves the checkout from the tg on PATH (--checkout DIR to override), following the symlink chain; refuses a dirty (tracked) tree and a non-fast-forward divergence; --dry-run reports what would land. - tg-ctl-aware: the resident daemon loads its code at start, so a deploy that changes the tg-ctl entry or anything under features/ (which it imports) only applies after a restart that DROPS the pane/cwd/session registration. The script warns by default and restarts only with --restart-ctl; daemon calls are timeout-bounded and a stop failure is surfaced on its own. - post-deploy tg --version + install-skill run the deployed checkout's own tg, bounded by timeout, and never abort the deploy on failure. Closes the ROADMAP 'tg-cli #36 merged but NOT deployed' gap: there is now a documented, scripted way to deploy a merged change to a live checkout. Bumps VERSION to 1.14.0 and documents the step in the README. Tests: 25 hermetic tests drive the real scripts/deploy.sh against throwaway git repos and stub tg/tg-ctl binaries (no host mutation) — FF, dirty/non-FF/detached refusal, untracked-file tolerance, symlink-chain resolution, the daemon warn/restart/stop-fail/no-daemon branches, and the version/install-skill paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live-checkout state (deploy=HOLD context)Two notes on the actual live environment, for the shipper / reviewer: 1. The ROADMAP premise was already stale. It claimed the live
So 2. The live checkout is now at OWED (deploy=HOLD — not applied): the running |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0d861ae542
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| echo "deploy: The running daemon still holds the OLD code. Restart it to apply:" >&2 | ||
| echo "deploy: tg-ctl status # note the registered pane/cwd/session" >&2 | ||
| echo "deploy: tg-ctl stop && tg-ctl start [--pane %N] [--cwd ...] [--session ...]" >&2 | ||
| echo "deploy: (or re-run this deploy with --restart-ctl to stop/start automatically)" >&2 |
There was a problem hiding this comment.
Fix the --restart-ctl rerun guidance
When the default deploy lands daemon-code changes and prints this banner, the suggested --restart-ctl rerun cannot work: the first run has already fast-forwarded HEAD, so a second run exits at the earlier “already up to date” branch before recomputing ctl_changed or reaching the restart logic. Users following this instruction will leave the resident tg-ctl on old code; either remove the rerun suggestion or make --restart-ctl handle the already-deployed/stale-daemon case.
Useful? React with 👍 / 👎.
| # silently ships stale daemon behavior (e.g. this PR's VERSION bump in | ||
| # features/cli/version.ts, which a features/tg-ctl-only matcher would not catch). | ||
| ctl_changed=0 | ||
| if git_c diff --name-only "HEAD..$upstream" | grep -qE '^(tg-ctl(/|$)|features/)'; then |
There was a problem hiding this comment.
Avoid grep -q under pipefail here
For a large pending diff where a features/ or tg-ctl path appears before many other filenames, grep -q can exit immediately after the match and cause git diff to receive SIGPIPE; because the script has pipefail enabled, the whole if can evaluate false even though daemon code changed. In that case ctl_changed remains 0, so a running daemon gets no warning or restart. Capture the filename list first or use a non-early-exiting match.
Useful? React with 👍 / 👎.
What & why
tgis a committed Bun script run via a symlink (tg→<checkout>/tg), so the checked-out file is the running binary — there is no build step. "Deploying" a merged change is just a guarded fast-forwardgit pullin the checkout the symlink points at. Until now that step was undocumented and unscripted, which is how the ROADMAP item "tg-cli #36 merged but NOT deployed" happened: a merged--tag/help change sat un-deployed because the live~/.files/repos/tg-clicheckout was never pulled.This adds
scripts/deploy.sh— the documented, scripted, safe, idempotent deploy step — plus a README section and aCHANGELOG/VERSION bump to 1.14.0.What the script does
tgon PATH (follows the symlink chain hop-by-hop;--checkout DIRto override).node_modulesdo not block it), a detached HEAD, and a non-fast-forward divergence (exit2). Idempotent no-op when up to date.--dry-runreports what would land without touching anything.tg-ctl rundaemon loads its code at start, so a deploy that changes thetg-ctlentry or anything underfeatures/(which the daemon imports) only takes effect after a restart — which drops the daemon's pane/cwd/session registration. The script warns by default (prints exactly what to restart) and only restarts with--restart-ctl. Daemon calls aretimeout-bounded; astopfailure is surfaced on its own rather than being misattributed tostart.tg --version+install-skillrun the deployed checkout's owntg(not whatever is first on PATH), bounded bytimeout, and never abort the deploy on failure.Acceptance evidence
Dry-run against the real live checkout (
~/.files/repos/tg-cli), correctly detecting the 3 pending commits and flagging the tg-ctl restart need:Tests — 25 hermetic tests drive the real
scripts/deploy.shagainst throwaway git repos + stubtg/tg-ctlbinaries (zero host mutation; tests run on a hermetic PATH so they can never resolve/pull the live checkout). Cover: up-to-date no-op, fast-forward, dirty-tree/non-FF/detached refusal, untracked-file tolerance, symlink-chain resolution, the daemon warn /--restart-ctlrestart / stop-failure / start-failure / no-daemon branches, and the--version+install-skillpaths (incl. failure warning).Review
Pre-commit multi-model review (GLM / Gemini + fast board) run on the exact staged diff across 5 rounds; substantive findings fixed each round (notably:
--checkoutrunning hooks against the wrong PATHtg, single-hop symlink resolution,set -e/pipefailaborting the deploy on an empty version probe, swallowedtg-ctl stopfailure, untracked-files blocking,tg-ctlchange-detection too narrow). Remaining suggestions (arbitrary-remote-name support, exit-code taxonomy for "deployed-but-post-step-failed", RCE threat-model note) consciously declined as YAGNI for a single-machinegit pullwrapper.NOT done here (deploy = HOLD)
This PR adds the deploy tooling; it does not itself mutate the live machine. See the PR thread for the live-checkout state + the owed
tg-ctlrestart.🤖 Generated with Claude Code