Skip to content

feat: idempotent update, --force-rebuild flag, wrapper version display, deeper smoke tests - #10

Merged
e6qu merged 4 commits into
mainfrom
update-idempotent-rebuild
May 21, 2026
Merged

feat: idempotent update, --force-rebuild flag, wrapper version display, deeper smoke tests#10
e6qu merged 4 commits into
mainfrom
update-idempotent-rebuild

Conversation

@e6qu

@e6qu e6qu commented May 21, 2026

Copy link
Copy Markdown
Owner

Why

A handful of UX gaps came up after v2.4.0 shipped:

  • sclaude update / scodex update always paid the full --no-cache --pull rebuild cost, even when running twice in a row with nothing new on npm.
  • sclaude version / scodex version printed "Script version: <hash>", which is actually the Docker layer hash — there was no way to see the installed wrapper's WRAPPER_VERSION.
  • T17 only ran codex --version, which is too shallow to reach the inner CLI's config-init code; the recent "Failed to load Cloud requirements (workspace-managed policies)" regression slipped past CI as a result.

Changes

Idempotent update — new needs_image_rebuild helper queries https://registry.npmjs.org/{@anthropic-ai/claude-code,@openai/codex}/latest, compares to what is baked into the current image (via <engine> run --rm <image> claude/codex --version), and returns 0 (rebuild) or 1 (skip) accordingly. Any error along the way (no curl, no image, npm unreachable) defaults to rebuild so a real update is never silently skipped.

--force-rebuild flag — bypasses the npm check and forces the no-cache + --pull rebuild. Parsed in parse_args so it never leaks through to the inner CLI.

Wrapper version in version output — adds Wrapper version: vX.Y.Z from WRAPPER_VERSION, and renames the misleading "Script version" line to "Image hash" (that field has always been the Docker layer hash).

Deeper smoke tests

  • T17b: scodex exec --help must exit 0 and not print Error loading configuration / Failed to load Cloud requirements / Failed to load .* policies.
  • T17c: same idea for sclaude --help.

Testing

  • pre-commit run --all-files — actionlint, shellcheck, bash -n, zsh -n all green.
  • bash -n sclaude scodex test_e2e.sh and zsh -n sclaude scodex test_e2e.sh pass.
  • sclaude version on macOS prints the new format correctly (Wrapper version: vX.Y.Z, Image hash: …).

Adrian Mârza added 4 commits May 21, 2026 20:23
…y, deeper smoke tests

`sclaude update` / `scodex update` and `sclaude version` / `scodex version`
had three rough edges. This addresses all three plus adds a smoke test
that would have caught the recent Codex config-loading regression.

1. `update` is now idempotent. New `needs_image_rebuild` helper queries
   the npm registry for the latest published @anthropic-ai/claude-code
   and @openai/codex versions, compares against what is installed in
   the current image, and skips the no-cache rebuild when both already
   match. Running `update` twice in a row no longer pays the full
   rebuild cost the second time. Any error along the way (no curl, no
   image, npm unreachable) defaults to "rebuild needed" so a real
   update is never silently skipped.

2. New `--force-rebuild` flag bypasses the npm version check and forces
   the no-cache + --pull rebuild. Parsed by parse_args before dispatch
   so it never leaks through to the inner CLI.

3. `sclaude version` / `scodex version` (and the `--version` aliases)
   now show "Wrapper version: vX.Y.Z" using `WRAPPER_VERSION`. The
   line that previously read "Script version: <hash>" is renamed to
   "Image hash:" — that field has always been the Docker layer hash,
   never the wrapper version, and the old label was misleading.

4. New T17b/T17c smoke tests exercise `codex exec --help` and
   `claude --help` and assert no config-load error strings appear. T17
   was running `--version` only, which is too shallow to reach the
   inner CLI's config-init code and missed the recent Codex
   "Failed to load Cloud requirements" regression.
…arse, tighter test asserts

Addresses the six findings from the codex review on PR #10.

- self_update_wrapper preserves --force-rebuild across its re-exec so the
  user's intent survives the wrapper swap (codex review thread on
  sclaude/scodex:422).
- needs_image_rebuild matches full semver including prerelease/build
  suffixes (e.g. 1.2.3-beta.1) on both the image side and the npm side,
  so update no longer rebuilds every run when a prerelease is the npm
  "latest" tag (thread on :439).
- New fetch_npm_latest helper parses the npm /latest document via
  python3 when available and falls back to a precise grep that only
  matches the first "version" field. The previous greedy sed could
  attach to a nested key (thread on :445).
- When the image is already at latest the wrapper now also prints
  `<wrapper> update --force-rebuild` as the documented way to refresh
  base OS / apt state, so the skip path no longer silently regresses
  the security-refresh behavior of the old unconditional rebuild
  (thread on :711/741).
- T10 now invokes `update --force-rebuild` and asserts the rebuild
  banner appears in output, so the skip-if-up-to-date path can't
  silently swallow the test's intent (thread on test_e2e.sh:244).
- T17b/T17c regex tightened to specific known config-load error
  strings instead of the broad `Failed to load .*` that could
  false-positive on benign output (thread on test_e2e.sh:353).
… cap T17 timeouts

Three more issues from re-running `codex exec review` on the harden
commit.

- `fetch_npm_latest`'s python3 path and no-python grep fallback could
  both return non-zero (python parse error, or grep finding no match)
  and trip `set -e` in the caller, which would abort `update` instead
  of falling back to a rebuild. Both paths now end with `|| true` and
  the function explicitly returns 0 (codex review thread on
  sclaude/scodex:457).
- The semver regex in `needs_image_rebuild` only allowed one of
  prerelease OR build metadata via `([-+]…)?`. A valid semver such as
  `1.2.3-beta.1+build.5` was truncated to `1.2.3-beta.1`, causing
  endless rebuilds whenever npm reported the full string. Replaced
  with the full shape `[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?
  (\+[0-9A-Za-z.-]+)?` on both probes (codex review thread on
  sclaude/scodex:479).
- T17b and T17c inherited the global 600s timeout. A hang in inner-CLI
  config loading would burn 20 minutes per CI run. Cap their per-test
  timeout at 120s (configurable via `T17_TIMEOUT_SECONDS`) and restore
  the original value before T18 (codex review thread on
  test_e2e.sh:344).
…-force-rebuild

Six more findings, all addressed.

- `needs_image_rebuild`'s `$ENGINE_CMD run … --version` probes had no
  timeout. A hang during inner-CLI startup would freeze `update`
  before it could fall back to rebuilding. New `probe_image_tool_version`
  helper wraps the probe in `timeout 15` (or `gtimeout` on macOS with
  coreutils, no-op otherwise) and only looks at the first line of
  output (so an update banner cannot displace the actual version
  token).
- `verify_image_built` previously verified only `$TOOL_BIN`. Since the
  image is shared between Claude and Codex, `sclaude update` was
  passing even when the rebuilt image's Codex install was broken (and
  vice versa). Now verifies both `claude --version` and
  `codex --version` and exits non-zero on either failure.
- `--force-rebuild` was consumed in parse_args even when the user
  invoked a non-update command. `sclaude --force-rebuild "do a thing"`
  silently lost the flag. Added a guard in parse_args that errors out
  when FORCE_REBUILD is true but WRAPPER_COMMAND is not `update`.
- `fetch_npm_latest` no longer has a regex fallback for hosts without
  python3 — codex worried (correctly) that scanning the first 1KB of
  the registry JSON could misattribute a nested "version" key. With no
  parser available, the function returns empty and the caller defaults
  to rebuild. python3 is already a runtime dependency of the wrapper
  (used in show_version metadata pretty-print).
- Image version regex moved into the new helper, behind `head -1` so
  the first semver-shaped token is taken only from the first output
  line, not from any subsequent line.
@e6qu
e6qu merged commit feb212c into main May 21, 2026
5 checks passed
@e6qu
e6qu deleted the update-idempotent-rebuild branch May 21, 2026 18:52
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