Skip to content

feat(e2e): claude-code smoke with Zstd + tar.gz + archive verify#12

Merged
robertsLando merged 9 commits intomainfrom
e2e/claude-code-smoke-zstd
Apr 23, 2026
Merged

feat(e2e): claude-code smoke with Zstd + tar.gz + archive verify#12
robertsLando merged 9 commits intomainfrom
e2e/claude-code-smoke-zstd

Conversation

@robertsLando
Copy link
Copy Markdown
Member

Summary

  • Adds Zstd to the compress-node input enum in packages/core/src/inputs.ts (+ regenerated action.yml, packages/build/action.yml, docs/inputs.md via yarn gen, rebundled dist). Zstd requires Node.js ≥ 22.15 on the build host — .node-version already pins 22.22.
  • New claude-code-smoke e2e job that pulls @anthropic-ai/claude-code from npm, builds a native binary per runner with SEA + compress-node: Zstd + compress: tar.gz + sha256 checksum, then runs claude --version on the binary and on the tar-extracted copy.
  • Matrix covers real cross-OS + cross-arch: ubuntu-latest/linux-x64, ubuntu-24.04-arm/linux-arm64, macos-latest/macos-arm64, windows-latest/win-x64. SEA mode chosen because claude-code is ESM and standard pkg can't bytecode-compile ESM.
  • Final verification step recomputes the archive's sha256 locally and diffs against the emitted sidecar (catches post-build tampering), validates the .tar.gz extension, extracts the tarball, and re-runs --version on the extracted binary.

Test plan

  • yarn test — 225/225 pass, including new parseInputs accepts Zstd compress-node case
  • yarn lint — clean
  • yarn gen + yarn build — committed outputs match source (codegen-drift job will re-verify on CI)
  • claude-code-smoke job green on all 4 runners (ubuntu x64, ubuntu arm64, macos arm64, windows x64)
  • Existing e2e jobs (tiny-cjs, matrix-fanout, multi-target-linux, windows-metadata) still green

🤖 Generated with Claude Code

- Add `Zstd` to the compress-node enum (core/inputs.ts), regenerate
  action.yml / packages/build/action.yml / docs/inputs.md, rebundle
  dist. Zstd requires Node.js >= 22.15 on the build host.
- New `claude-code-smoke` e2e job: installs @anthropic-ai/claude-code
  from npm, builds via SEA mode with compress-node=Zstd + tar.gz +
  sha256 on ubuntu-x64/arm64, macos-arm64, windows-x64. Runs the
  binary with `claude --version`, then recomputes the sha256 against
  the sidecar and extracts the tarball to re-verify --version on the
  archived copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 23, 2026 08:50
- @anthropic-ai/claude-code >= 2.1.117 ships as a thin wrapper that
  delegates to a native binary via optionalDependencies; pkg can't
  bundle that. Pin 2.1.114, the last release whose `bin.claude`
  points at a real `cli.js`.
- pkg 6.17 is the first yao-pkg/pkg to accept `--compress Zstd`; the
  action's default pkg-version (~6.16.0) rejected it with "Invalid
  compression algorithm Zstd". Override to ~6.18.0 for this job.
- Resolve `pkg.bin.claude` explicitly (instead of Object.values()[0])
  so a future per-OS alias can't steer us at the wrong entrypoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the action’s compress-node input to support Zstd compression and adds a new real-world E2E smoke workflow job that builds and validates a SEA-packaged @anthropic-ai/claude-code binary across multiple OS/arch runners.

Changes:

  • Add Zstd to compress-node input parsing/types, plus regenerated action.yml, packages/build/action.yml, docs, and bundled dist/ artifacts.
  • Add unit test coverage ensuring parseInputs accepts canonical Zstd (and still rejects lowercase zstd).
  • Add claude-code-smoke E2E job that builds a SEA binary with compress-node: Zstd + tar.gz, runs --version, then verifies archive integrity and checksum sidecar.

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/core/src/inputs.ts Adds Zstd to input description, type union, and enum parsing.
packages/core/test/unit/inputs.test.ts Adds unit test ensuring Zstd is accepted and validates case-sensitivity behavior.
.github/workflows/e2e.yml Adds claude-code-smoke job to build/run/verify an archived SEA binary across OS/arch matrix.
action.yml Regenerated input docs to include Zstd in compress-node description.
packages/build/action.yml Regenerated input docs to include Zstd in compress-node description.
docs/inputs.md Regenerated inputs documentation for compress-node: Zstd.
packages/build/dist/index.mjs Rebundled dist reflecting new compress-node enum value and description.
packages/windows-metadata/dist/index.mjs Rebundled dist reflecting updated compress-node description.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/e2e.yml Outdated
robertsLando and others added 7 commits April 23, 2026 11:00
- 2.1.113 already switched @anthropic-ai/claude-code from a pure-JS
  cli.js entrypoint to a native-binary wrapper (bin/claude.exe). My
  earlier pin of 2.1.114 landed inside the new layout, so pkg was
  compiling a shell-script placeholder that errors at runtime. 2.1.112
  is the last pure-JS release — verified locally with SEA+Zstd: builds
  in ~20s, binary runs and prints "2.1.112 (Claude Code)" on --version.
- Previous `--version` step captured output via `out=$(... 2>&1)` under
  `set -e`, which aborted before echoing when the binary exited non-
  zero (as it did on every runner). Suspend `set -e` around the capture
  so the logs actually show what the binary printed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced by the new claude-code-smoke e2e:

- macOS: the macos-latest runner's bsdtar rejects `--mtime` outright
  with "Option --mtime is not supported", even though upstream
  libarchive accepts it. The action was always passing --mtime; tiny-
  cjs never hit this because its macOS matrix entry uses zip. Keep
  --mtime on GNU tar (linux) only; rely on the utimes pre-pin for
  bsdtar paths (archive() is single-file, so the header inherits that
  mtime). Test updated to assert --mtime presence only on linux.

- Windows: git-bash's `sha256sum` escapes the output line with a
  leading backslash when the filename contains backslashes (Windows
  paths), per GNU coreutils convention. The e2e's verify step was
  comparing `\<digest>` against the sidecar's clean hex. Strip the
  leading backslash via sed before parsing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GNU tar from git-bash parses `D:\a\...` as a remote host:path target
(colon-separated rsh syntax), producing:
    tar (child): Cannot connect to D: resolve failed

Convert any Windows drive-letter path to the POSIX form (`/d/a/...`)
via cygpath before invoking tar. No-op on Linux/macOS — falls back to
echo when cygpath isn't on PATH. --version + sha256 sidecar check
already passed on Windows in the previous run; this unblocks the
tar-round-trip tail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review follow-ups on #12:

- Guard the pin: assert the resolved bin entry is a JS file (.js/.mjs/
  .cjs) before patching it into the fixture package.json. If the
  @anthropic-ai/claude-code pin ever drifts past 2.1.112 into the
  native-binary wrapper layout (bin → bin/claude.exe shell-script
  placeholder), fail loudly rather than bundling garbage.
- Replace the ad-hoc `${fix//\\//}` backslash-strip with the same
  cygpath helper used in the verify step, so the fixture path emitted
  to GITHUB_OUTPUT is always POSIX-form on Windows git-bash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mpatible

Reverting the $fix cygpath conversion from 3fca6c7. cygpath -u emits
POSIX form (/d/a/...), which GNU tar in git-bash handles correctly
but Node's fs on Windows misparses as drive-relative, producing paths
like D:\d\a\_temp\... and failing with ENOENT. The composite's build
step is Node-based, not shell-tar-based, so it wants the drive-letter-
plus-forward-slash form (D:/a/...) that ${fix//\\//} produces.

cygpath stays in the verify step where tar is the consumer — different
tool, different path convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced by matrix e2e debugging: the orchestrator emitted every pkg
argv twice (once as "[pkg-action] pkg …" in main.ts, once as
"[pkg-action] Invoking: …" from the runner) and was silent through the
finalize stages — no signal for when archive started, when the
checksum was computed, or which shasum files were written.

- Drop the main.ts pre-log; runPkg's "Invoking:" already carries the
  command path, and GH Actions renders the raw `[command]` line too,
  so three copies was overkill.
- Log pkg wall time after runPkg completes.
- Group the per-target finalize loop under a `logger.startGroup` so
  each target collapses into its own fold in the GH Actions UI —
  matters for matrix runs where 4+ targets interleave.
- Add explicit archive/checksum progress lines with sizes + timings
  (`archive → …`, `archived … (17.8 MB, 4.2s)`, `sha256 <digest>
  <file>`) so any downstream verification failure trivially diffs
  against the build log.
- Log each SHASUMS file as it's written, with entry count.
- Include overall wall time in the final "done" line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pkg CLI emits ~15–30 lines per build (Walking dependencies, node
download/extract, SEA asset generation, blob injection, strip warnings)
and the GH-Actions exec shim adds `[command]` echo on top. On a multi-
target run all of that interleaved with finalize output is hard to
skim.

Wrap runPkg in startGroup/endGroup with a header that names the target
list, so reviewers see a single collapsible "pkg build (targets=…)"
block followed by the one-line wall-time summary. Same pattern we
already use for the per-target finalize blocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@robertsLando robertsLando merged commit 2cb95b8 into main Apr 23, 2026
19 checks passed
robertsLando added a commit that referenced this pull request Apr 23, 2026
Main (PR #12) wired the claude-code-smoke job through the action using
`mode: sea` and `compress-node: Zstd` inputs. Those inputs no longer exist
on this branch, so move both values into the package.json#pkg field that
the fixture-prep step already injects. No behavior change on main; on this
branch the e2e now exercises the pkg-config path the slim input surface
forces users onto.
robertsLando added a commit that referenced this pull request Apr 24, 2026
…13)

* refactor!: drop pkg-CLI mirror inputs, own only action-layer surface

Pkg-specific knobs (mode, node-version, compress-node, fallback-to-source,
public, public-packages, options, no-bytecode, no-dict, debug, extra-args)
are removed from the action input surface. Users express them in their pkg
config (.pkgrc, pkg.config.{js,ts,json}, or package.json pkg field) instead.
The action keeps only action-layer concerns that do not exist in pkg config:
config path, entry, targets, pkg-version/pkg-path, plus the archive, signing,
windows-metadata, and performance groups.

Motivation: mirroring pkg's CLI forced the action to grow a new input each
time pkg added a flag, and to maintain back-compat on every pkg release. By
letting pkg own its own schema we decouple the action from pkg's evolution.

Docs updated: README gets a new "pkg configuration" section with a migration
example; architecture.md §3/§6 note the scoped surface; STATUS.yaml records
the dropped-inputs list and migration path.

BREAKING CHANGE: the listed inputs are no longer accepted. Setting them now
triggers the unknown-input warning and the value is ignored. Migrate into
your pkg config file.

* review: link STATUS from architecture, cover pkg-version/pkg-path in tests

Address PR-review suggestions:
- architecture.md §6 no longer duplicates the dropped-input list; points
  readers at STATUS.yaml#input-surface-slim as the single source.
- inputs.test.ts: extend the defaults test with `pkgPath` and add a positive
  round-trip test for `pkg-version` + `pkg-path`. These remain live inputs and
  deserve the same explicit coverage the rest of BuildInputs has.

223/223 unit tests, lint clean.

* feat: add config-inline input for file-less pkg config

New optional input `config-inline` accepts a JSON string carrying the pkg
config directly in the workflow. The orchestrator writes it to
`${invocationDir}/pkg-config.inline.json` and forwards that path to pkg via
`--config`. Mutually exclusive with `config`; the pair is validated at parse
time alongside a JSON-object syntax check so a typo fails before pkg runs.

Motivation: users with a trivial pkg config (entry + a couple of knobs) no
longer need to commit a separate .pkgrc.json just to customize the build.
Keeps the "pkg owns its schema" property — users still write pkg config
keys (camelCase), we just let them write them inline.

Not masked as a secret — README adds a note warning users against embedding
secrets in the value.

Docs: README gains an "Inline config" subsection with an example; action.yml
+ docs/inputs.md regenerated.

Tests: four new unit tests cover the happy path, mutual exclusion, invalid
JSON, and non-object JSON (string/array/null). 227/227 pass.

* review(copilot): use correct camelCase pkg-config keys in warnings/docs

Three nits from the Copilot review on PR #13:
- targets.ts cross-compile warnings referenced `fallback-to-source` (kebab,
  like the retired CLI flag); pkg config uses `fallbackToSource` (camelCase).
  Reword both messages to point at the actual config key.
- README "pkg configuration" example was fenced as jsonc with a `//` comment
  and trailing comma but labeled `.pkgrc.json` — .pkgrc.json is strict JSON,
  so a copy/paste would fail. Move the filename into the prose, fence as
  json, drop the comment + trailing comma.

Unit tests + lint green. Matrix bundle rebuilt to pick up the new warning
strings.

* e2e(claude-code): move pkg-layer knobs from action inputs to pkg config

Main (PR #12) wired the claude-code-smoke job through the action using
`mode: sea` and `compress-node: Zstd` inputs. Those inputs no longer exist
on this branch, so move both values into the package.json#pkg field that
the fixture-prep step already injects. No behavior change on main; on this
branch the e2e now exercises the pkg-config path the slim input surface
forces users onto.

* e2e(claude-code): use sea:true config, acknowledge CLI-only flag gap

The merge from main brought in the claude-code-smoke job, which previously
drove pkg via `mode: sea` + `compress-node: Zstd` action inputs. On this
branch those inputs are dropped. The prior merge-conflict resolution moved
both into `package.json#pkg` under the assumption that pkg config would pick
them up. That assumption was wrong:

- `mode: 'sea'` is not a pkg config key — the correct key is `sea: true`.
  (This was a user-visible bug; pkg silently ignored `mode` and ran
  standard-mode, which OOM'd trying to bytecode-compile claude-code's ESM
  bundle on macos-arm64.)
- `compressNode` / `compress` is not a pkg config key at all. `--compress`
  is CLI-only today on @yao-pkg/pkg. No config equivalent exists.

Fix the e2e:
- Write `sea: true` into package.json#pkg so SEA is actually enabled.
- Drop the Zstd request — there is no way to express it without the
  dropped CLI input. The job now exercises SEA + tar.gz; the Zstd branch
  is intentionally deferred until upstream lands config support.
- Rename + retone the job's comment block accordingly.

Also add an upstream-dependency note to STATUS.yaml and a draft issue
body at docs/upstream-pkg-config-issue.md. The draft asks yao-pkg/pkg to
accept the currently-CLI-only build flags (compress, fallbackToSource,
public, publicPackages, options, noBytecode, noDict, debug, signature)
in the config file — that's the piece that makes this PR's scope
defensible for the full input set, not just for SEA.

227 unit tests pass; lint + prettier clean.

* status: link upstream pkg-config tracking issue (yao-pkg/pkg#262)

Issue is open upstream asking @yao-pkg/pkg to accept the currently-CLI-only
build flags (compress, fallbackToSource, public, publicPackages, options,
noBytecode, noDict, debug, signature) in the config file — the piece that
makes this PR's drop-the-CLI-mirror direction fully viable.

Replace the local draft with the issue URL in STATUS.yaml#upstream-dependency.

* e2e(claude-code): enable Zstd via pkg config; bump pkg → ~6.19.0

pkg v6.19.0 (yao-pkg/pkg#263, closes #262) landed config support for
the CLI-only build flags, and ships the SEA-mode detector fix
(yao-pkg/pkg#268) that caused the prior OOM at bytecode-compile time.

- claude-code-smoke: add `compress: 'Zstd'` alongside `sea: true` in
  package.json#pkg; bump pkg-version input ~6.18.0 → ~6.19.0.
- Default `pkg-version` input bumped ~6.16.0 → ~6.19.0 — the whole
  config-as-source-of-truth direction of this branch requires it.
- Regenerated action.yml / packages/build/action.yml / docs/inputs.md.
- STATUS.yaml: upstream-dependency RESOLVED; Zstd gap dropped;
  e2e-status lists claude-code-smoke; ci-status bumped to 227 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: rebuild dist/ bundles after pkg-version bump

Rebuild packages/{build,windows-metadata}/dist/index.mjs so the
committed bundles match the ~6.19.0 default written into
packages/core/src/inputs.ts. Drift gate caught it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: correct pkg-config keys, mask config-inline, extract materialize helper

Address four items from the self-review on PR #13:

- README examples used `mode: "sea"` / `compressNode: "Brotli"` — neither
  exists in @yao-pkg/pkg's config schema. The authoritative FLAG_SPECS in
  lib/config.ts (and the docs-site configuration reference) show `sea: true`
  and `compress: "..."`. Fixed both examples so a copy-paste works.

- `config-inline` carried user-authored JSON but was not masked. Mark the
  spec with `secret: true` so parseInputs registers the raw value via
  core.setSecret — exact-match redaction in logs costs nothing given pkg
  doesn't echo the config blob. README warning softened accordingly (still
  flags the on-disk temp file).

- Extract the config-inline "bytes to disk" step out of the orchestrator
  into `materializePkgConfigInline` in fs-utils, alongside
  `PKG_CONFIG_INLINE_FILENAME`. Four new unit tests cover pass-through,
  both-undefined, inline-writes-and-returns-path, and inline-wins-over-config.
  Orchestrator is now a two-liner that just logs the resulting path.

- BASE_BUILD fixture in pkg-runner.test.ts still pinned pkgVersion to the
  old `~6.16.0` default; bump to `~6.19.0` to match parseInputs.

Regenerated action.yml + docs/inputs.md (secret flag on config-inline
doesn't affect the YAML shape but the description changed). Rebuilt
packages/{build,windows-metadata}/dist/index.mjs.

Tests 231/231 pass (was 227/227), typecheck + lint + prettier green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants