Skip to content

feat(deploy): scripted, idempotent deploy step for installed tg checkouts (tg-cli #36 deploy follow-up)#41

Open
alex-mextner wants to merge 1 commit into
mainfrom
w2/tg-deploy-ship
Open

feat(deploy): scripted, idempotent deploy step for installed tg checkouts (tg-cli #36 deploy follow-up)#41
alex-mextner wants to merge 1 commit into
mainfrom
w2/tg-deploy-ship

Conversation

@alex-mextner

Copy link
Copy Markdown
Owner

What & why

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 just a guarded fast-forward git pull in 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-cli checkout was never pulled.

This adds scripts/deploy.sh — the documented, scripted, safe, idempotent deploy step — plus a README section and a CHANGELOG/VERSION bump to 1.14.0.

What the script does

  • Resolves the checkout from the tg on PATH (follows the symlink chain hop-by-hop; --checkout DIR to override).
  • Refuses a dirty tracked tree (untracked files like a stray node_modules do not block it), a detached HEAD, and a non-fast-forward divergence (exit 2). Idempotent no-op when up to date.
  • --dry-run reports what would land without touching anything.
  • tg-ctl-aware: the resident tg-ctl run daemon loads its code at start, so a deploy that changes the tg-ctl entry or anything under features/ (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 are timeout-bounded; a stop failure is surfaced on its own rather than being misattributed to start.
  • Post-deploy tg --version + install-skill run the deployed checkout's own tg (not whatever is first on PATH), bounded by timeout, 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:

deploy: checkout = /Users/ultra/.files/repos/tg-cli
deploy: branch  = main
deploy: 708bac7 -> ff1d44f, commits to land:
  ff1d44f feat(autolink): tg#<id> message refs + GitHub-anchor line specs (#34)
  3dedfea docs(agents): correct stale test count (#38)
  6de3f93 docs(readme): fix stale agent-tools ecosystem description (drop agenttools_log lib claim) (#33)
deploy: --dry-run — not pulling.
deploy: (dry-run) would require a tg-ctl restart (daemon code changed).

Tests — 25 hermetic tests drive the real scripts/deploy.sh against throwaway git repos + stub tg/tg-ctl binaries (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-ctl restart / stop-failure / start-failure / no-daemon branches, and the --version + install-skill paths (incl. failure warning).

bun test tests/deploy.test.ts   ->  25 pass, 0 fail
bun test (full suite)           ->  1091 pass, 0 fail
shellcheck scripts/deploy.sh    ->  clean
tsc --noEmit tests/deploy.test.ts -> clean
ci/leftover-grep + ci/secret-scan -> PASS

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: --checkout running hooks against the wrong PATH tg, single-hop symlink resolution, set -e/pipefail aborting the deploy on an empty version probe, swallowed tg-ctl stop failure, untracked-files blocking, tg-ctl change-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-machine git pull wrapper.

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-ctl restart.

🤖 Generated with Claude Code

…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>
@alex-mextner

Copy link
Copy Markdown
Owner Author

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 tg was 1.11.0 with #36 not deployed. In reality the live checkout ~/.files/repos/tg-cli was already at 708bac7 (1.12.0, which contains #36 aeb82fa feat(tag,help): lowercase-english --tag + #39 CodeQL). Functionally verified live before any of my changes:

  • tg --tag REPORT (uppercase) → rejected, exit 1, invalid --tag 'REPORT': tags must be lowercase english
  • tg --tag ОТЧЁТ (Cyrillic) → rejected, exit 1
  • tg --tag report (lowercase) → accepted

So answer/decision/problem/report already work live. The remaining gap was that the live checkout was 3 commits behind origin/main (#33, #38, #34 autolink — the latter bumping VERSION to 1.13.0).

2. The live checkout is now at ff1d44f (1.13.0), but the tg-ctl daemon was NOT restarted. During test development one hermetic-test iteration was not yet hermetic and resolved+pulled the live checkout (a bug I then fixed — every test now runs on a PATH that cannot reach the live tg, with a live SHA before/after guard). The end state is the correct clean fast-forward the deploy targets, so I left it rather than churn it back:

~/.files/repos/tg-cli: ff1d44f, main, clean, up to date with origin/main
tg --version -> tg 1.13.0 (ff1d44f)

OWED (deploy=HOLD — not applied): the running tg-ctl daemon (pid 30242, started before the pull) still holds the OLD features/tg-ctl/inject.ts, so inbound messages still wrap as [TG from … #<id>] instead of the new tg#<id>. Applying it needs tg-ctl stop && tg-ctl start --pane %3 …, which drops the live agent pane registration (%3) — exactly the kind of live-touch this track is told not to blind-apply. Restart at a safe moment, re-binding pane %3. scripts/deploy.sh --restart-ctl automates the stop/start (re-register afterwards).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/deploy.sh
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread scripts/deploy.sh
# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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