Skip to content

build(deps): bump @actions/core from 3.0.0 to 3.0.1 in the actions-toolkit group #60

build(deps): bump @actions/core from 3.0.0 to 3.0.1 in the actions-toolkit group

build(deps): bump @actions/core from 3.0.0 to 3.0.1 in the actions-toolkit group #60

Workflow file for this run

name: E2E
# Invokes the top-level composite against the test-fixtures/ projects on
# ubuntu/macos/windows. Asserts:
# - produced binary runs and prints the expected version
# - checksum sidecar matches the binary
# - archive round-trips (when compress != none)
# - step summary contains a row for the target
on:
push:
branches: [main]
pull_request:
paths:
# `packages/**` covers packages/build/action.yml + every source, test, and
# dist/ under packages/. The action.yml entries below are the top-level
# composite and the two sub-actions whose directories are not under packages/.
- 'packages/**'
- 'scripts/**'
- 'action.yml'
- 'matrix/action.yml'
- 'windows-metadata/action.yml'
- 'test-fixtures/**'
- '.github/workflows/e2e.yml'
- '.github/scripts/**'
- 'package.json'
- 'yarn.lock'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true
jobs:
tiny-cjs:
name: tiny-cjs / ${{ matrix.os }} / ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: node22-linux-x64
archive: tar.gz
ext: tar.gz
- os: macos-latest
target: node22-macos-arm64
archive: zip
ext: zip
- os: windows-latest
target: node22-win-x64
archive: zip
ext: zip
steps:
- uses: actions/checkout@v6
- name: Run pkg-action
id: build
uses: ./
with:
config: test-fixtures/tiny-app-cjs/package.json
targets: ${{ matrix.target }}
compress: ${{ matrix.archive }}
checksum: sha256
filename: '{name}-{version}-{os}-{arch}'
- name: Show outputs
shell: bash
run: |
echo "binaries=${{ steps.build.outputs.binaries }}"
echo "artifacts=${{ steps.build.outputs.artifacts }}"
echo "checksums=${{ steps.build.outputs.checksums }}"
echo "version=${{ steps.build.outputs.version }}"
- name: Assert at least one artifact produced
shell: bash
run: |
count=$(echo '${{ steps.build.outputs.artifacts }}' | jq 'length')
if [ "$count" -lt 1 ]; then
echo "::error::no artifacts produced"; exit 1
fi
- name: Assert checksum sidecar matches the archived artifact
shell: bash
run: |
first_artifact=$(echo '${{ steps.build.outputs.artifacts }}' | jq -r '.[0]')
sidecar="${first_artifact}.sha256"
if [ ! -f "$sidecar" ]; then
echo "::error::missing checksum sidecar at $sidecar"; exit 1
fi
# The sidecar is <digest> <basename>\n — verify the digest parses as 64 hex chars.
awk 'NR==1 { if (length($1) != 64) exit 1 }' "$sidecar"
echo "sidecar OK: $(cat "$sidecar")"
codegen-drift:
name: Verify action.yml codegen drift
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: yarn
- run: yarn install --non-interactive
- run: yarn gen
- name: Verify committed action.yml matches source
run: |
git diff --exit-code -- action.yml packages/build/action.yml docs/inputs.md || {
echo "::error::action.yml or docs/inputs.md is out of sync. Run 'yarn gen' locally and commit."
exit 1
}
# ──────────────────────────────────────────────────────────────────────
# M2: matrix sub-action demo.
#
# Stage 1 ("plan") runs the matrix sub-action and emits a JSON array of
# {target, runner} tuples. Stage 2 ("fan-out") consumes that array via
# strategy.matrix and invokes the top-level composite on each runner. This
# is the canonical downstream usage for pkg-action/matrix and doubles as a
# smoke test that the JSON shape is strategy.matrix-compatible.
matrix-plan:
name: Matrix plan
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- uses: actions/checkout@v6
- name: Expand targets into a matrix
id: plan
uses: ./matrix
with:
targets: |
node22-linux-x64
node22-macos-arm64
node22-win-x64
- name: Show expansion
run: |
echo 'matrix=${{ steps.plan.outputs.matrix }}'
matrix-fanout:
name: matrix-fanout / ${{ matrix.entry.target }}
needs: matrix-plan
runs-on: ${{ matrix.entry.runner }}
strategy:
fail-fast: false
matrix:
entry: ${{ fromJson(needs.matrix-plan.outputs.matrix) }}
steps:
- uses: actions/checkout@v6
- name: Build fixture for ${{ matrix.entry.target }}
id: build
uses: ./
with:
config: test-fixtures/tiny-app-cjs/package.json
targets: ${{ matrix.entry.target }}
compress: none
checksum: sha256
filename: '{name}-{version}-{os}-{arch}'
- name: Assert at least one binary
shell: bash
run: |
count=$(echo '${{ steps.build.outputs.binaries }}' | jq 'length')
if [ "$count" -lt 1 ]; then
echo "::error::no binary produced for ${{ matrix.entry.target }}"; exit 1
fi
# ──────────────────────────────────────────────────────────────────────
# M2 §2.5: multi-target, single-runner. Proves the orchestrator's per-
# target loop against a real pkg invocation that asks for >1 target at
# once. Both linux-x64 and linuxstatic-x64 are native on ubuntu-latest,
# so no cross-compile risks muddy the result.
multi-target-linux:
name: multi-target / ubuntu-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build two linux targets in one invocation
id: build
uses: ./
with:
config: test-fixtures/tiny-app-cjs/package.json
targets: |
node22-linux-x64
node22-linuxstatic-x64
compress: none
checksum: sha256
filename: '{name}-{version}-{os}-{arch}'
- name: Assert both binaries produced
shell: bash
run: |
count=$(echo '${{ steps.build.outputs.binaries }}' | jq 'length')
if [ "$count" -ne 2 ]; then
echo "::error::expected 2 binaries, got $count"
echo '${{ steps.build.outputs.binaries }}' | jq
exit 1
fi
# Every binary must exist on disk.
echo '${{ steps.build.outputs.binaries }}' | jq -r '.[]' | while read -r bin; do
[ -f "$bin" ] || { echo "::error::missing binary $bin"; exit 1; }
done
# A sidecar checksum must accompany each artifact.
echo '${{ steps.build.outputs.artifacts }}' | jq -r '.[]' | while read -r art; do
[ -f "$art.sha256" ] || { echo "::error::missing sidecar $art.sha256"; exit 1; }
done
# ──────────────────────────────────────────────────────────────────────
# M3 §3.6: Windows metadata e2e smoke. Builds a win-x64 binary with
# product-name / company-name / file-version set at the top-level, then
# re-parses the output with resedit to verify those strings land in the
# VS_VERSIONINFO block of the emitted PE. This covers the full chain:
# parseInputs → parseWindowsMetadataInputs → orchestrator loop →
# applyWindowsMetadata → NtExecutable.generate.
windows-metadata:
name: windows-metadata / windows-latest
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: yarn
- run: yarn install --non-interactive
shell: bash
- name: Build tiny-app with windows metadata
id: build
uses: ./
with:
config: test-fixtures/tiny-app-cjs/package.json
targets: node22-win-x64
compress: none
checksum: sha256
filename: '{name}-{version}-{os}-{arch}'
windows-product-name: 'PkgActionTest'
windows-company-name: 'Acme Ltd'
windows-file-version: '1.2.3.4'
windows-file-description: 'pkg-action M3 smoke'
- name: Verify VS_VERSIONINFO round-trips via resedit
shell: bash
run: |
bin=$(echo '${{ steps.build.outputs.binaries }}' | jq -r '.[0]')
[ -f "$bin" ] || { echo "::error::binary missing: $bin"; exit 1; }
echo "Checking $bin"
node --experimental-strip-types .github/scripts/assert-windows-metadata.ts "$bin"
# ──────────────────────────────────────────────────────────────────────
# Real-world smoke: pull @anthropic-ai/claude-code from npm, build a
# native binary per runner OS/arch in SEA mode with Zstd-compressed
# bundled sources, tar.gz archive, execute the binary with --version,
# then verify the archive round-trips and the sha256 sidecar matches
# the archive bytes.
#
# claude-code is ESM, so SEA mode is required (standard pkg can't
# bytecode-compile ESM). Both `sea: true` and `compress: 'Zstd'` are
# declared in the pkg config (package.json#pkg) — expressible in
# config starting @yao-pkg/pkg v6.19.0 (yao-pkg/pkg#263). Each matrix
# entry builds for the runner's native target so the produced binary
# can be launched in-place, giving real cross-OS + cross-arch
# coverage (x64 and arm64 on Linux, arm64 on macOS, x64 on Windows).
claude-code-smoke:
name: claude-code-smoke / ${{ matrix.os }} / ${{ matrix.target }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: node22-linux-x64
- os: ubuntu-24.04-arm
target: node22-linux-arm64
- os: macos-latest
target: node22-macos-arm64
- os: windows-latest
target: node22-win-x64
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .node-version
- name: Prepare claude-code fixture
id: fix
shell: bash
run: |
set -euo pipefail
fix="${RUNNER_TEMP}/claude-code-fixture"
mkdir -p "$fix"
cd "$fix"
npm init -y >/dev/null
# Pin 2.1.112 — the last pure-JS release of
# @anthropic-ai/claude-code. Starting at 2.1.113 the package
# ships as a thin wrapper around a native binary distributed
# via platform optionalDependencies (bin points to a shell-
# script placeholder like `bin/claude.exe`), which pkg can't
# meaningfully bundle. --ignore-scripts skips the postinstall
# native-binary fetcher — irrelevant for 2.1.112 but a cheap
# guardrail if the pin ever drifts.
npm install --omit=dev --ignore-scripts @anthropic-ai/claude-code@2.1.112
# Resolve the package's `claude` bin entry. Prefer the named
# 'claude' key so we don't accidentally pick a future per-OS
# alias (e.g. 'claude.exe') that would break pkg on Linux.
entry=$(node -e "
const path = require('path');
const pkgPath = require.resolve('@anthropic-ai/claude-code/package.json', { paths: [process.cwd()] });
const pkgDir = path.dirname(pkgPath);
const pkg = require(pkgPath);
const bin = typeof pkg.bin === 'string'
? pkg.bin
: (pkg.bin && pkg.bin.claude) || Object.values(pkg.bin || {})[0];
if (!bin) throw new Error('claude bin not found in package.json');
console.log(path.relative(process.cwd(), path.resolve(pkgDir, bin)).split(path.sep).join('/'));
")
echo "entry=$entry"
# Trip-wire: if the pin ever drifts to a claude-code version
# that again points `bin.claude` at a shell-script placeholder
# (bin/claude.exe), fail loudly here rather than letting pkg
# silently bundle a non-JS file.
case "$entry" in
*.js|*.mjs|*.cjs) ;;
*) echo "::error::claude bin is not a JS entrypoint ($entry) — pin may have drifted past 2.1.112"; exit 1 ;;
esac
node -e "
const fs = require('fs');
const p = './package.json';
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.bin = './' + process.argv[1];
// Full pkg-layer config lives here — no CLI-mirror inputs.
// Requires @yao-pkg/pkg >= 6.19.0 for \`compress\` in config
// (yao-pkg/pkg#263).
pkg.pkg = {
assets: ['node_modules/@anthropic-ai/claude-code/**/*'],
sea: true,
compress: 'Zstd',
};
fs.writeFileSync(p, JSON.stringify(pkg, null, 2));
" "$entry"
cat ./package.json
# Emit drive-letter + forward-slash form (e.g. `D:/a/_temp/…`).
# The downstream composite is Node-based: Node's fs resolves
# `D:/a/…` correctly on Windows but mis-parses cygpath's POSIX
# form (`/d/a/…`) as drive-relative, yielding `D:\d\a\…`.
echo "fixture=${fix//\\//}" >> "$GITHUB_OUTPUT"
- name: Build claude binary
id: build
uses: ./
with:
config: ${{ steps.fix.outputs.fixture }}/package.json
targets: ${{ matrix.target }}
# 6.19.0+ is required for the `compress` key in pkg config.
# SEA (`sea: true`) and Zstd (`compress: 'Zstd'`) live in the
# package.json#pkg field, not as action inputs.
pkg-version: ~6.19.0
compress: tar.gz
checksum: sha256
filename: 'claude-{version}-{os}-{arch}'
- name: Run claude --version
shell: bash
run: |
set -uo pipefail
bin=$(echo '${{ steps.build.outputs.binaries }}' | jq -r '.[0]')
[ -f "$bin" ] || { echo "::error::binary missing: $bin"; exit 1; }
chmod +x "$bin" || true
# Capture without `set -e` so stderr-carrying, non-zero exits
# still print the output for diagnosis.
out=$("$bin" --version 2>&1)
status=$?
echo "--- claude --version (exit=$status) ---"
echo "$out"
echo "--- end ---"
[ "$status" -eq 0 ] || { echo "::error::claude --version exited $status"; exit 1; }
# claude --version prints a semver-shaped string. Enforce the
# shape to catch silent regressions where the binary boots but
# spits an unrelated banner.
echo "$out" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+' || {
echo "::error::unexpected --version output"; exit 1;
}
- name: Verify tar.gz archive + sha256 sidecar
shell: bash
run: |
set -euo pipefail
# GNU tar in git-bash parses `D:\a\...` / `D:/a/...` as
# host:path (colon-separated remote rsh target). cygpath
# converts to the POSIX form (`/d/a/...`) that GNU tar treats
# as a local absolute path. No-op on Linux/macOS.
to_posix() {
if command -v cygpath >/dev/null 2>&1; then
cygpath -u "$1"
else
echo "$1"
fi
}
archive=$(to_posix "$(echo '${{ steps.build.outputs.artifacts }}' | jq -r '.[0]')")
[ -f "$archive" ] || { echo "::error::archive missing: $archive"; exit 1; }
case "$archive" in
*.tar.gz) ;;
*) echo "::error::expected .tar.gz archive, got: $archive"; exit 1 ;;
esac
# Sidecar must exist and carry a 64-hex-char sha256 + the archive
# basename. Recompute the digest locally and compare byte-for-byte
# against the sidecar so we catch any post-build tampering.
sidecar="${archive}.sha256"
[ -f "$sidecar" ] || { echo "::error::missing checksum sidecar: $sidecar"; exit 1; }
recorded=$(awk 'NR==1 { print $1 }' "$sidecar")
[ "${#recorded}" -eq 64 ] || { echo "::error::malformed sha256 in sidecar: $recorded"; exit 1; }
# sha256sum on git-bash prefixes the whole line with a literal
# backslash when the filename contains backslashes (Windows
# paths), per GNU coreutils' name-escape convention — strip it
# before comparing against the sidecar's clean hex digest.
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$archive" | sed 's/^\\//' | awk '{print $1}')
else
actual=$(shasum -a 256 "$archive" | awk '{print $1}')
fi
if [ "$recorded" != "$actual" ]; then
echo "::error::sha256 mismatch — sidecar=$recorded actual=$actual"; exit 1
fi
echo "sidecar sha256 OK: $recorded"
# Tar round-trip: extract into a scratch dir, confirm the binary
# lands inside, then run --version on the extracted copy so we
# know the archive itself — not just the pre-archive binary — is
# usable.
extract=$(to_posix "${RUNNER_TEMP}/claude-extract")
rm -rf "$extract"
mkdir -p "$extract"
tar -tzf "$archive" >/dev/null
tar -xzf "$archive" -C "$extract"
bin_basename=$(basename "$(echo '${{ steps.build.outputs.binaries }}' | jq -r '.[0]')")
extracted_bin=$(find "$extract" -type f -name "$bin_basename" | head -n 1)
[ -n "$extracted_bin" ] || { echo "::error::extracted tar missing $bin_basename"; exit 1; }
chmod +x "$extracted_bin" || true
# Temporarily suspend `set -e` so non-zero exits still print
# the captured output for diagnosis.
set +e
out=$("$extracted_bin" --version 2>&1)
status=$?
set -e
echo "--- extracted $extracted_bin --version (exit=$status) ---"
echo "$out"
echo "--- end ---"
[ "$status" -eq 0 ] || { echo "::error::extracted binary exited $status"; exit 1; }
echo "$out" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+' || {
echo "::error::extracted binary produced unexpected output"; exit 1;
}