Files
Codeman/.github/workflows/ci.yml
T
DevvynandClaude Opus 5 1ca35095e7 refactor(install): drive CLI detection, the install menu and hints from the catalogue
install.sh carried nine search-path arrays, eighteen near-identical
check_<cli>/get_<cli>_path functions, and three separately hand-maintained
enumerations of all nine CLIs. They had to agree and did not: upstream b6d0f1fa
is "wire OMP into install.sh's CLI detection (it had none)", and the section
comment above the roll-call named six of the nine.

All of it now reads the generated catalogue. `detect_all_clis` resolves every
CLI in one memoized pass into CLI_FOUND_PATH/CLI_FOUND_COUNT; `check_cli` and
`get_cli_path` replace the eighteen pairs; the roll-call, the "no AI CLI found"
gate and the closing reminder become loops. Probe order per CLI is unchanged and
`test/install-sh-detection-parity.test.ts` proves it against the literals
transcribed from the arrays this deletes.

Behaviour changes worth naming:

- The install menu is built from the catalogue, so it offers every enabled CLI
  that is not installed and ships a command — five instead of two. Gemini had a
  command in the registry and appeared in NO list in this script.
- Its labels are now the registry's ("Claude" rather than "Claude Code"), the
  same trade PR A made for `codeman doctor` rows. A suffix map would just be the
  hand-maintained list again.
- On a wget-only host the menu prints commands instead of running them. The
  registry's commands call curl, whereas the two literals this replaces went
  through download_to_stdout; rewriting curl to wget inside a string we are
  about to execute is the wrong instinct.

The trust boundary is mechanical, not a promise: CLI_INSTALL_CMD_TRUSTED is
written only from the generated per-platform arrays and is the only thing ever
executed; CLI_INSTALL_CMD_DISPLAY is what the optional, opt-in refresh may
rewrite. The refresh warns on all three failure shapes — empty body, unparseable
content, failed fetch — which is the silent-degradation bug from the review, and
it parses with node into tab-separated records read by `read`, never eval.

Bash 3.2 throughout (macOS ships it): parallel indexed arrays, offset/length
windows instead of delimiters, no associative arrays, namerefs, mapfile or
here-strings. Verified by executing the script under a real bash 3.2 container,
which is also now a CI step alongside `bash -n` and a catalogue `--check` — the
empty-window case (`shell` has no binaries) is a runtime `set -u` abort that
`bash -n` cannot see. Running it that way caught `detect_os` being called inside
the platform loop: ten forks, and ten copies of one error, since a `die` inside
`$( )` can only exit the subshell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
2026-09-13 17:43:13 +08:00

141 lines
4.9 KiB
YAML

name: CI
on:
push:
branches: [master, main]
pull_request:
jobs:
ci:
name: Typecheck & Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check package-lock.json version sync
run: npm run check:lockfile
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Frontend JS syntax check
run: npm run check:frontend-syntax
- name: Format check
run: npm run format:check
# install.sh reaches users through `curl | bash` with nothing between it and
# them, and until now nothing in this repo checked it at all: no shellcheck,
# no bats, and the vitest gate is Node-only.
- name: install.sh syntax
run: bash -n install.sh
# macOS ships bash 3.2 and this runner has bash 5, so the constructs that
# actually break a Mac install are invisible here without a container. This
# step is what catches them — in particular expanding an EMPTY array under
# `set -u`, which bash 3.2 treats as an unbound variable and `bash -n`
# cannot see because it is a runtime error, not a syntax one.
- name: install.sh runs on bash 3.2 (macOS's version)
run: |
set -euo pipefail
docker run --rm -v "$PWD":/w -w /w bash:3.2 bash -n /w/install.sh
docker run --rm -v "$PWD":/w -w /w -e CODEMAN_INSTALL_SH_LIB=1 bash:3.2 bash -c '
set -euo pipefail
. /w/install.sh
detect_all_clis
# `shell` declares no binaries, so its offset/length window is length 0.
# Iterating it is the empty-array case; reaching here means it did not abort.
echo "bash $BASH_VERSION: ${#CLI_IDS[@]} CLIs, $CLI_FOUND_COUNT found"
cli_catalog_names >/dev/null
cli_catalog_print_install_hints >/dev/null
'
- name: CLI catalogue artifacts are in sync with stock.ts
run: npm run generate:cli-catalog -- --check
- name: Server boot smoke test
run: |
set -u
if ! command -v tmux >/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y tmux
fi
npx tsx src/index.ts web --port 3151 > /tmp/boot.log 2>&1 &
SERVER_PID=$!
trap "kill $SERVER_PID 2>/dev/null || true" EXIT
for i in $(seq 1 30); do
if curl -fsS http://localhost:3151/api/status -o /dev/null; then
echo "Server booted in ${i}s"
exit 0
fi
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo "Server exited before becoming ready. Logs:"
cat /tmp/boot.log
exit 1
fi
sleep 1
done
echo "Server did not respond on /api/status within 30s. Logs:"
cat /tmp/boot.log
exit 1
test:
name: Unit & integration tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install tmux
run: |
if ! command -v tmux >/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y tmux
fi
- name: Run unit & integration tests
# Excludes the suites that need chromium, per-machine PNG baselines or a
# quiet machine — see config/test-suites.ts for the list and the reason
# behind each entry. Identical to what `npm test` runs locally.
# Safe in CI: TmuxManager no-ops all shell commands under VITEST (test/setup.ts).
run: npm run test:ci
- name: Run xterm-zerolag-input package tests
# Layers 1-3 of the predictive-echo suites (unit laws, fixture replay,
# seeded fuzz): deterministic, no browser, no live server. Depends on
# the ROOT `npm ci` above — workspaces hoist the package's vitest into
# the root node_modules; do not add a separate install here.
run: npx vitest run
working-directory: packages/xterm-zerolag-input
# Note: three suites are excluded from CI, each with its own local runner:
# npm run test:browser Playwright + chromium (+ a live server, and a real
# codex binary for codex-predictive-echo)
# npm run test:mobile the above plus environment-specific PNG baselines
# npm run test:perf wall-clock benchmarks; need an otherwise idle machine
# config/test-suites.ts holds the globs; the configs derive from it so the
# exclusions here and those runners cannot drift apart. Everything else runs in
# the `test` job above, which is the same thing `npm test` runs.