Skip to content

Commit 9781c17

Browse files
robertsLandoclaude
andauthored
refactor!: shrink scope to build + sign + archive + checksum (#10)
* refactor!: shrink scope to build + sign + archive + checksum BREAKING. The action now stops at producing signed, checksummed files on disk and emitting their paths. Distribution concerns move to dedicated downstream actions. Removed (inputs, source, tests, docs, e2e jobs): - Release attach — attach-to-release, release-{tag,name,body,draft, prerelease}, generate-release-table. Replace with softprops/action-gh-release against outputs.artifacts. - Docker OCI publish — docker-* inputs + packages/core/src/docker.ts. Replace with docker/build-push-action against outputs.binaries. - Homebrew tap PR — homebrew-* + renderHomebrewFormula. Replace with a dedicated tap-updater action. - Scoop bucket PR — scoop-* + renderScoopManifest. - SLSA provenance — provenance input + attest-build-provenance wire-up. Replace with actions/attest-build-provenance@v4 as a chained step. - SBOM — sbom input + packages/core/src/sbom.ts. Replace with anchore/sbom-action. - Workflow artifact upload — upload-artifact + artifact-name inputs + @actions/artifact integration. Replace with actions/upload-artifact@v4. - release-url output and summary Release trailer. Runtime deps drop @actions/artifact (v6.2.1) and @actions/github (v9.1.0). Cap moves from 6 → 4 runtime deps. Kept unchanged: - Build pipeline (all pkg-* inputs, targets, mode, compress-node, etc.) - Post-build archive + checksum - Windows metadata (resedit) - Signing — macOS codesign + notarytool, Windows signtool, Azure Trusted Signing - Matrix sub-action - Step-summary (no Release trailer) Bundle shrinks: packages/build/dist/index.mjs ~3.6 MB → ~1 MB. Tests: 296 → 222 (deletions, no regressions). Typecheck + lint green. docs/publishing.md, distribution.md, docker.md, provenance.md, sbom.md removed. README rewritten around outputs + "After the build" handoff examples. docs/architecture.md trimmed accordingly. STATUS records scope decision and a `removed:` section with replacement pointers. Pre-cut state is accessible at the `pre-scope-cut` tag. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * chore(ci): drop spike-node24.yml Dead weight. M-1 pre-flight probe to confirm runs.using:node24 on hosted runners; that question has been answered for months — every sub-action uses node24 and the main e2e workflow runs it across ubuntu/macos/windows on every push. workflow_dispatch-only, so not load-bearing. Also drop the two docs/architecture.md references to it. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * docs: address Copilot review - README quick start: wrap artifacts JSON via fromJson+join so upload-artifact gets newline-separated paths. - architecture: clarify DI boundary (exec/logger only; fs not universally injected). - architecture: codegen drift gate lives in ci.yml plus e2e codegen-drift job, not e2e.yml alone. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat: v1.0 hardening — tar reproducibility + digests output + post-sign verify Three narrow-scope wins before v1.0.0. Each is a bug or contract tightening, not a new feature surface. - Tar reproducibility (packages/core/src/archive.ts): pass --mtime, --uid=0, --gid=0, --numeric-owner + pin source mtime with utimes before shelling out. Flags understood by both GNU tar (ubuntu) and bsdtar (macos + windows). Previously: same binary → different tar bytes → different sha across runs, breaking provenance/cache chains. - digests output (packages/build/src/main.ts): emit a per-artifact { "<basename>": { sha256: "…", sha512: "…" } } JSON map alongside the existing SHASUMS files. Saves downstream consumers a file read + awk parse. - Post-sign verification (packages/core/src/signing.ts): chain codesign --verify after sign, signtool verify /pa after signtool/azuresigntool. Catches bad identities and silent signing failures before archive/checksum. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(archive): use GNU tar owner/group flags on linux GNU tar (ubuntu runners) rejects --uid=0 / --gid=0; those are bsdtar-only. Branch on process.platform: linux → --owner=0 --group=0, else --uid=0 --gid=0. --mtime + --numeric-owner are portable across both. Caught by E2E on refactor/scope-cut-build-only — tiny-cjs/ubuntu-latest failed with: "tar: unrecognized option '--uid=0'". Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 4395ea1 commit 9781c17

40 files changed

Lines changed: 3972 additions & 70359 deletions

.github/workflows/e2e.yml

Lines changed: 0 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -250,167 +250,3 @@ jobs:
250250
[ -f "$bin" ] || { echo "::error::binary missing: $bin"; exit 1; }
251251
echo "Checking $bin"
252252
node --experimental-strip-types .github/scripts/assert-windows-metadata.ts "$bin"
253-
254-
# ──────────────────────────────────────────────────────────────────────
255-
# M6 §6.1: SLSA build-provenance attestation. Runs the composite with
256-
# provenance=true on a cheap ubuntu-latest target; asserts that every
257-
# artifact has a corresponding attestation uploaded to the repo's
258-
# attestations store via `gh attestation list` + a JSON walk.
259-
provenance:
260-
name: provenance / ubuntu-latest
261-
runs-on: ubuntu-latest
262-
permissions:
263-
contents: read
264-
id-token: write
265-
attestations: write
266-
steps:
267-
- uses: actions/checkout@v6
268-
269-
- name: Build tiny-app with provenance enabled
270-
id: build
271-
uses: ./
272-
with:
273-
config: test-fixtures/tiny-app-cjs/package.json
274-
targets: node22-linux-x64
275-
compress: tar.gz
276-
checksum: sha256
277-
filename: '{name}-{version}-{os}-{arch}'
278-
provenance: true
279-
280-
- name: Verify an attestation was produced for the artifact
281-
env:
282-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
283-
shell: bash
284-
run: |
285-
artifact=$(echo '${{ steps.build.outputs.artifacts }}' | jq -r '.[0]')
286-
[ -f "$artifact" ] || { echo "::error::artifact missing: $artifact"; exit 1; }
287-
echo "Verifying attestation for $artifact"
288-
# gh attestation verify hits the repo's attestation store. When the
289-
# attestation from this run was indexed successfully, this call
290-
# exits 0 with a signed summary. Any failure means provenance
291-
# wasn't produced or wasn't discoverable — treat both as fatal.
292-
gh attestation verify "$artifact" --repo "$GITHUB_REPOSITORY"
293-
294-
# ──────────────────────────────────────────────────────────────────────
295-
# M6 §6.2: SBOM generation. Runs the composite with sbom=cyclonedx on a
296-
# cheap ubuntu-latest target; asserts the SBOM file is uploaded as a
297-
# workflow artifact AND contains a valid CycloneDX 1.5 doc referencing
298-
# the binary's hash.
299-
sbom-cyclonedx:
300-
name: sbom-cyclonedx / ubuntu-latest
301-
runs-on: ubuntu-latest
302-
steps:
303-
- uses: actions/checkout@v6
304-
305-
- name: Build tiny-app with CycloneDX SBOM
306-
id: build
307-
uses: ./
308-
with:
309-
config: test-fixtures/tiny-app-cjs/package.json
310-
targets: node22-linux-x64
311-
compress: tar.gz
312-
checksum: sha256
313-
filename: '{name}-{version}-{os}-{arch}'
314-
sbom: cyclonedx
315-
316-
- name: Assert SBOM file exists and parses
317-
shell: bash
318-
run: |
319-
sbom="$RUNNER_TEMP"/pkg-action-*/final/tiny-app-cjs-0.0.1.cdx.json
320-
# Glob it explicitly.
321-
sbom_resolved=$(ls $sbom 2>/dev/null | head -n1)
322-
[ -n "$sbom_resolved" ] || { echo "::error::SBOM file not found (glob: $sbom)"; exit 1; }
323-
echo "Found SBOM: $sbom_resolved"
324-
bomFormat=$(jq -r '.bomFormat' "$sbom_resolved")
325-
specVersion=$(jq -r '.specVersion' "$sbom_resolved")
326-
[ "$bomFormat" = "CycloneDX" ] || { echo "::error::bomFormat=$bomFormat"; exit 1; }
327-
[ "$specVersion" = "1.5" ] || { echo "::error::specVersion=$specVersion"; exit 1; }
328-
# metadata.component.name must be the project name.
329-
projName=$(jq -r '.metadata.component.name' "$sbom_resolved")
330-
[ "$projName" = "tiny-app-cjs" ] || { echo "::error::component.name=$projName"; exit 1; }
331-
# formulation[].components[0].hashes must reference SHA-256.
332-
algo=$(jq -r '.formulation[0].components[0].hashes[0].alg' "$sbom_resolved")
333-
[ "$algo" = "SHA-256" ] || { echo "::error::hash alg=$algo"; exit 1; }
334-
echo "SBOM OK"
335-
336-
# ──────────────────────────────────────────────────────────────────────
337-
# M6 §6.3: Docker OCI publish. Builds a tiny-app binary and pushes it to
338-
# the repo's own ghcr.io namespace under a disposable tag, then docker-
339-
# pulls + runs the image and asserts stdout matches the expected version
340-
# line. GITHUB_TOKEN has packages:write on ghcr by default.
341-
docker-publish:
342-
name: docker / ubuntu-latest
343-
runs-on: ubuntu-latest
344-
permissions:
345-
contents: read
346-
packages: write
347-
steps:
348-
- uses: actions/checkout@v6
349-
- uses: docker/setup-buildx-action@v4
350-
351-
- name: Build tiny-app and push as OCI image
352-
id: build
353-
uses: ./
354-
with:
355-
config: test-fixtures/tiny-app-cjs/package.json
356-
targets: node22-linux-x64
357-
compress: none
358-
checksum: sha256
359-
filename: '{name}-{version}-{os}-{arch}'
360-
docker-image: ghcr.io/${{ github.repository }}-e2e:{version}-{sha}
361-
docker-username: ${{ github.actor }}
362-
docker-password: ${{ secrets.GITHUB_TOKEN }}
363-
# cc variant bundles libc + libstdc++ + libgcc, required by the
364-
# pkg-packaged Node binary (base-debian12 lacks libstdc++.so.6).
365-
docker-base-image: gcr.io/distroless/cc-debian12:latest
366-
367-
- name: Log in to ghcr.io for pull
368-
# The action pushes via a private DOCKER_CONFIG dir to keep the
369-
# password off argv, so the host daemon has no ghcr creds afterward.
370-
# Authenticate the default config explicitly before docker pull.
371-
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
372-
373-
- name: Pull + run the pushed image
374-
shell: bash
375-
env:
376-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
377-
run: |
378-
image="ghcr.io/${GITHUB_REPOSITORY,,}-e2e:0.0.1-$(git rev-parse --short HEAD)"
379-
echo "Pulling $image"
380-
docker pull "$image"
381-
out=$(docker run --rm "$image")
382-
echo "Container stdout: $out"
383-
echo "$out" | grep -q 'tiny-app-cjs 0.0.1' || {
384-
echo "::error::image did not emit expected version line"
385-
exit 1
386-
}
387-
388-
sbom-spdx:
389-
name: sbom-spdx / ubuntu-latest
390-
runs-on: ubuntu-latest
391-
steps:
392-
- uses: actions/checkout@v6
393-
394-
- name: Build tiny-app with SPDX SBOM
395-
id: build
396-
uses: ./
397-
with:
398-
config: test-fixtures/tiny-app-cjs/package.json
399-
targets: node22-linux-x64
400-
compress: none
401-
checksum: sha256
402-
filename: '{name}-{version}-{os}-{arch}'
403-
sbom: spdx
404-
405-
- name: Assert SPDX SBOM file exists and parses
406-
shell: bash
407-
run: |
408-
sbom_resolved=$(ls "$RUNNER_TEMP"/pkg-action-*/final/tiny-app-cjs-0.0.1.spdx.json 2>/dev/null | head -n1)
409-
[ -n "$sbom_resolved" ] || { echo "::error::SPDX SBOM not found"; exit 1; }
410-
echo "Found SBOM: $sbom_resolved"
411-
spdxVersion=$(jq -r '.spdxVersion' "$sbom_resolved")
412-
[ "$spdxVersion" = "SPDX-2.3" ] || { echo "::error::spdxVersion=$spdxVersion"; exit 1; }
413-
# DESCRIBES relationship to the project root must exist.
414-
count=$(jq '[.relationships[] | select(.relationshipType=="DESCRIBES")] | length' "$sbom_resolved")
415-
[ "$count" -ge 1 ] || { echo "::error::no DESCRIBES relationship"; exit 1; }
416-
echo "SPDX SBOM OK"

.github/workflows/spike-node24.yml

Lines changed: 0 additions & 45 deletions
This file was deleted.

README.md

Lines changed: 73 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,49 @@
11
# `yao-pkg/pkg-action`
22

3-
> **Status: M0 scaffold — not yet functional. Do not consume from a workflow until `v1.0.0` is tagged.**
3+
> **Status: ALPHA — API still shifting until `v1.0.0`.**
44
5-
Official GitHub Action to build, sign, archive, and publish Node.js binaries with [`@yao-pkg/pkg`](https://github.com/yao-pkg/pkg).
5+
Official GitHub Action to build Node.js binaries with
6+
[`@yao-pkg/pkg`](https://github.com/yao-pkg/pkg).
7+
8+
Scope is intentionally narrow: **build → (optional Windows metadata
9+
patch) → (optional sign) → archive → checksum**. The action stops at
10+
producing signed, checksummed files on disk and emitting their paths
11+
as step outputs. Shipping those files to a GitHub release, a workflow
12+
artifact, a container registry, or a package manager is a separate
13+
concern — chain a dedicated action against the `binaries` / `artifacts`
14+
/ `checksums` outputs.
615

716
Tracking issue: [yao-pkg/pkg#248](https://github.com/yao-pkg/pkg/issues/248).
8-
Implementation plan: see the pinned comment on that issue.
917

10-
## What this will do (once shipped)
18+
## Quick start
1119

1220
```yaml
1321
- uses: yao-pkg/pkg-action@v1
22+
id: build
1423
with:
1524
targets: node22-linux-x64,node22-macos-arm64,node22-win-x64
1625
compress: tar.gz
1726
checksum: sha256
18-
attach-to-release: true
27+
28+
- uses: actions/upload-artifact@v4
29+
with:
30+
name: pkg-binaries
31+
path: "${{ join(fromJson(steps.build.outputs.artifacts), '\n') }}"
1932
```
2033
21-
…plus Windows metadata injection (`resedit`), macOS codesign + notarize, Windows signtool + Azure Trusted Signing, archive + checksum + release upload, and a matrix helper for cross-compile-safe multi-OS jobs.
34+
## Outputs
35+
36+
| Output | Shape |
37+
| ----------- | ------------------------------------------------------------------------- |
38+
| `binaries` | JSON array of absolute paths (bare binaries) |
39+
| `artifacts` | JSON array — archive when `compress != none`, else the binary |
40+
| `checksums` | JSON array of SHASUMS file paths (one per algorithm) |
41+
| `digests` | JSON object `{ "<artifact basename>": { "sha256": "…", "sha512": "…" } }` |
42+
| `version` | Project version from `package.json#version` |
2243

2344
## Matrix helper
2445

25-
When you want one shard per target, pinned to a native runner, use the
26-
`matrix` sub-action:
46+
One shard per target, pinned to a native runner:
2747

2848
```yaml
2949
jobs:
@@ -54,64 +74,64 @@ jobs:
5474
targets: ${{ matrix.entry.target }}
5575
```
5676

57-
Full reference — inputs, self-hosted overrides, cross-compile policy — in
58-
[`docs/matrix.md`](./docs/matrix.md).
77+
Reference: [`docs/matrix.md`](./docs/matrix.md).
5978

60-
## Release attach
79+
## Windows metadata + signing
6180

62-
Publish the produced binaries to a GitHub release in the same workflow:
81+
- Windows PE resource patch (ProductName, CompanyName, FileVersion,
82+
icon, manifest) via `resedit` — set any `windows-*` input.
83+
- macOS codesign + optional notarytool staple.
84+
- Windows signtool or Azure Trusted Signing.
6385

64-
```yaml
65-
on:
66-
push:
67-
tags: ['v*']
86+
All signing happens between Windows-metadata patch and archive, so the
87+
shasum and archive contain the signed bytes. Full input reference:
88+
[`docs/inputs.md`](./docs/inputs.md).
6889

69-
permissions:
70-
contents: write
90+
## After the build — example handoffs
7191

72-
jobs:
73-
release:
74-
runs-on: ubuntu-latest
75-
steps:
76-
- uses: actions/checkout@v6
77-
- uses: yao-pkg/pkg-action@v1
78-
env:
79-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80-
with:
81-
targets: node22-linux-x64,node22-macos-arm64,node22-win-x64
82-
compress: tar.gz
83-
checksum: sha256
84-
attach-to-release: true
85-
```
92+
Attach to a GitHub release:
8693

87-
Full reference (non-tag triggers, body templating, asset overwrites,
88-
permissions) in [`docs/publishing.md`](./docs/publishing.md).
94+
```yaml
95+
- uses: yao-pkg/pkg-action@v1
96+
id: build
97+
with:
98+
targets: node22-linux-x64,node22-macos-arm64,node22-win-x64
99+
compress: tar.gz
100+
checksum: sha256
89101
90-
## Build provenance (SLSA)
102+
- uses: softprops/action-gh-release@v2
103+
with:
104+
files: |
105+
${{ join(fromJson(steps.build.outputs.artifacts), '\n') }}
106+
${{ join(fromJson(steps.build.outputs.checksums), '\n') }}
107+
```
91108

92-
Opt in with `provenance: true` and grant the two required permissions
93-
to emit a signed SLSA build-provenance attestation for every artifact:
109+
Build + push a Docker image:
94110

95111
```yaml
96-
permissions:
97-
contents: write
98-
id-token: write
99-
attestations: write
112+
- uses: yao-pkg/pkg-action@v1
113+
id: build
114+
with:
115+
targets: node22-linux-x64
100116
101-
jobs:
102-
build:
103-
runs-on: ubuntu-latest
104-
steps:
105-
- uses: actions/checkout@v6
106-
- uses: yao-pkg/pkg-action@v1
107-
with:
108-
targets: node22-linux-x64,node22-macos-arm64,node22-win-x64
109-
provenance: true
117+
- uses: docker/build-push-action@v6
118+
with:
119+
context: .
120+
push: true
121+
tags: ghcr.io/${{ github.repository }}:latest
122+
build-args: BIN_PATH=${{ fromJson(steps.build.outputs.binaries)[0] }}
110123
```
111124

112-
Consumers verify with `gh attestation verify`. Full reference —
113-
permissions, release-attach combo, verification examples — in
114-
[`docs/provenance.md`](./docs/provenance.md).
125+
SLSA provenance:
126+
127+
```yaml
128+
- uses: actions/attest-build-provenance@v4
129+
with:
130+
subject-path: ${{ join(fromJson(steps.build.outputs.artifacts), '\n') }}
131+
```
132+
133+
Homebrew tap, Scoop bucket, npm package — all live in the same
134+
"consume outputs, run a dedicated action" pattern.
115135

116136
## Development
117137

@@ -121,8 +141,6 @@ permissions, release-attach combo, verification examples — in
121141
- `yarn test` — `node --test` with `--experimental-strip-types`
122142
- `yarn lint` — ESLint + Prettier
123143

124-
See `CONTRIBUTING.md` for the strip-types dev loop and `.node-version` policy.
125-
126144
## License
127145

128146
MIT — see [`LICENSE`](./LICENSE).

0 commit comments

Comments
 (0)