Playbook: deploying gh-app-token to /usr/local/bin (fleet)

Status: ACTIVE (created 2026-08-11, #6103 — follow-up from #6096)

What this covers

Keeping the deployed on-PATH helper /usr/local/bin/gh-app-token in sync with the canonical repo implementation scripts/gh-app-token (208-line Python, argparse-based, built for #4068). This is the operator-facing binary invoked as a bare gh-app-token (e.g. gh-app-token --check) on a fleet host.

Why it matters — the footgun this fixes (#6103)

There are two copies of this helper on each host and they drift independently:

PathKept current byNotes
/opt/p24-infra/scripts/gh-app-tokensecrets-sync.yml / the repo checkout on that hostcanonical; tracks origin/main
/usr/local/bin/gh-app-tokennothing — manual deploy onlythe drift source
bin/gh-app-token.sh (wrapper)repo checkoutresolves repo root, always current

No CI job or Ansible role deploys /usr/local/bin/gh-app-token. It was placed manually and never refreshed, so it lags the repo. The stale copy (104-line, sha256 5031655d…) predates argparse entirely: it has no argument parsing, so --check (and --repos / --permissions) is silently ignored — the script always mints a fresh 1h installation token and prints it to stdout unconditionally. Running the documented safe verification gh-app-token --check against the stale copy therefore does the opposite of what it promises: it leaks a live token to the terminal. #6096 hit exactly this on dev-laptop.

The current 208-line version (sha256 ca015e61…, at repo HEAD) is a strict superset and backward compatible: a bare gh-app-token still prints the default cached token, so existing $(gh-app-token) callers are unaffected. --check prints only a status line to stderr, never the token, and exits 0.

Preconditions

  • Root on the target host (the deployed file is root:root 0755).
  • The current scripts/gh-app-token reachable on/for the host:
    • bms-4 / vps-i1: already present at /opt/p24-infra/scripts/gh-app-token (repo checkout).
    • dev-laptop: has no current checkout (its /opt/p24-infra is on the old dev branch, 85-line copy) — transport the file from bms-4 via the vps-i1 reverse tunnel.

Deploy procedure (per host)

Always back up, install with explicit mode, then verify with --check:

# 1. Back up the existing deployed copy (timestamped).
cp -a /usr/local/bin/gh-app-token "/usr/local/bin/gh-app-token.bak-$(date +%Y%m%d%H%M%S)"
 
# 2. Install the current repo version (root:root 0755). `install` sets mode atomically.
install -m 0755 -o root -g root /path/to/current/scripts/gh-app-token /usr/local/bin/gh-app-token
 
# 3. Verify byte-for-byte against the source.
sha256sum /usr/local/bin/gh-app-token   # must equal the repo-HEAD sha (ca015e61… as of #6103)
 
# 4. Verify --check is SAFE: status line to stderr only, token NEVER on stdout, exit 0.
#    Capture stdout separately so a regression (token on stdout) is caught, not printed.
out=$(gh-app-token --check 2>/tmp/ghapp.err); rc=$?
echo "exit=$rc"; echo "stdout_bytes=$(printf '%s' "$out" | wc -c)"   # expect exit=0, stdout_bytes=0
grep -q '^gh-app-token: OK' /tmp/ghapp.err && echo "stderr status OK"; rm -f /tmp/ghapp.err

stdout_bytes=0 + exit=0 + an OK status on stderr = the deployed copy is the current, safe version. Any non-zero stdout byte count means a token was printed — stop, the wrong binary is still in place.

Reaching dev-laptop (no public IP)

dev-laptop (Ubuntu, radieu-Aspire-F5-573G) is reachable only via the autossh reverse tunnel through vps-i1 (vps-i1:127.0.0.1:2222 → dev-laptop:22). From bms-4:

# vps-i1's ~/.ssh/config defines a `dev-laptop` alias (IdentityFile /root/.ssh/p24-dispatcher).
ssh root@217.154.82.162 'ssh dev-laptop "<command>"'
# Transport a file bms-4 → vps-i1 → dev-laptop:
scp /opt/p24-infra/scripts/gh-app-token root@217.154.82.162:/tmp/gh-app-token.new
ssh root@217.154.82.162 'scp /tmp/gh-app-token.new dev-laptop:/tmp/gh-app-token.new && \
  ssh dev-laptop "install -m0755 /tmp/gh-app-token.new /usr/local/bin/gh-app-token"'

The worker’s bms-4 key has no root on dev-laptop directly; the p24-dispatcher key lives on vps-i1, so all dev-laptop hops route through vps-i1.

Known gap — no automated sync (root cause of the drift)

/usr/local/bin/gh-app-token has no owning deploy automation. Until one exists, every bump of scripts/gh-app-token must be manually re-deployed to each host’s /usr/local/bin via this playbook. The durable fix is a secrets-sync.yml (or Ansible) step that installs the repo copy into /usr/local/bin/gh-app-token on each dispatch host on every sync — tracked as follow-up to #6103. Prefer pointing operators at bin/gh-app-token.sh (always current) over the standalone deployed binary in the meantime.