Releases: forgesworn/anvil
v0.8.2
Bug Fixes
- strip lifecycle-script stdout before parsing npm pack --json
v0.8.1
Bug Fixes
- remove invalid runner.temp from publish job-level env
v0.8.0
Features
- bridge auto-release to release.yml via workflow_dispatch
v0.7.0
Features
- chain auto-release publish into release.yml via workflow_call
- release workflow accepts explicit tag input
v0.6.0
Features
- create or update Release to support chained auto-release flow
v0.5.1
Bug Fixes
- include repo root in context7 indexed folders
v0.5.0
Features
- conventional commit versioning, security fixes, wider ecosystem positioning
Bug Fixes
- add .env, key material, and IDE patterns to .gitignore
- harden CI/CD workflows against expression injection and credential leaks
- harden scripts against injection, path traversal, and fragile patterns
v0.4.1 — fix scoped-package verify URL
Patch release. Fixes a scoped-package URL bug in the artefact integrity verify recipe, surfaced during the Phase 3 pilot on @forgesworn/shamir-words@1.1.0. Adds a bats regression test so it cannot silently come back.
What was broken
The verify recipe in the GitHub Release body used the local npm pack filename in the registry URL path. For unscoped packages this worked because the local pack name equals the registry-side name (e.g. nsec-tree-1.5.0.tgz). For scoped packages npm flattens the scope with a dash in the local filename but the registry serves the tarball at the unscoped basename within the scope-namespaced path:
local pack: scope-pkg-1.0.0.tgz
registry url: https://registry.npmjs.org/@scope/pkg/-/pkg-1.0.0.tgz
Consumers who copy-pasted the verification recipe from the release body would get a 404 on scoped packages.
Only the URL was wrong. The hashes recorded in the integrity block (sha256 and npm sha512 integrity) were and remain correct, and they still match the registry tarball byte-for-byte. The bug was purely cosmetic for the verification UX, not for the integrity claim itself.
What's fixed
steps/update-release.sh now strips the scope prefix from the package name with ${name##*/} and rebuilds the URL filename as ${basename}-${version}.tgz. For unscoped packages the construction produces the same filename as before, so behaviour for nsec-tree, geohash-kit, and other unscoped consumers is unchanged.
Existing already-published scoped releases on v0.4.0 can have their release body retroactively corrected via gh release edit.
What's new
test/update-release.bats adds four regression tests covering:
@forgesworn/shamir-words@1.1.0(the original Phase 3 bug)nsec-tree@1.5.0(unscoped, behaviour-unchanged guard)@noble/hashes@1.4.2(different scope shape)gh release upload --clobberinvocation (idempotency check)
The tests use a fake gh on PATH that captures every invocation; assertions are made on both the positive URL (must appear) and the negative URL (the broken form must not appear inside any registry URL).
Migration
None. Bumping @v0.4.0 to @v0.4.1 is a drop-in replacement. Anyone pinning @v0 already has the fix — the floating tag was moved to the fix commit before this patch release was cut.
Stats
- 1 file changed (test only); fix commit was already on main
- 63/63 bats tests pass (4 new)
- shellcheck clean
- Phase 1, 2, and 3 pilots complete: nsec-tree (unscoped), geohash-kit (unscoped), @forgesworn/shamir-words (scoped, surfaced this bug)
Full diff: v0.4.0...v0.4.1
v0.4.0 — multi-runner reproducible-build attestation
Multi-runner reproducible-build attestation. The v0.4 flagship. The reusable workflow now runs the release as a four-job DAG and publishes only when two parallel builds on independent CI runners produce byte-identical tarballs. None of semantic-release, @changesets/cli, release-it, release-please, or np offers this today.
What's new
- Reusable workflow split into a four-job DAG:
build-a(full gates + record),build-b(build + record only),reproduce(compare sha256s),publish(publish-npm + publish-jsr + update-release). Both build jobs upload their tarball as a CI artifact; the reproduce job downloads both and compares; the publish job downloads the canonicalbuild-atarball and uploads exactly that to npm. steps/normalise-mtimes.shwalks the working tree and touches every file and directory toSOURCE_DATE_EPOCH(derived fromgit log -1 --format=%ct), sonpm packproduces tar headers that are byte-equal between independent runs. Belt-and-braces — even if a build tool ignoresSOURCE_DATE_EPOCH, the file stamps are uniform by the time we pack. Cross-platform GNU + BSDtouch. Excludesnode_modulesand.git.steps/compare-tarball-meta.shis the reproducibility gate. Compares twotarball.metafiles. Under defaultreproducibility-mode: strict, a sha256 mismatch is a hard failure: both hashes are printed, the diff between the two tar listings is dumped, common non-determinism causes are listed, and the publish is blocked. Underwarnthe mismatch is logged but the release proceeds. Underoffthe second build is skipped entirely (v0.3 single-runner behaviour).- Canonical tarball is uploaded as a GitHub Release asset. Consumers now have two independent sources for the bytes (npm registry + GitHub Releases) and can hash-compare against either using the verify recipe in the release body.
- "Reproducible build" header line is prepended above the integrity block in the release body when the reproduce gate matched. Cosmetic but makes the property visible without parsing the integrity block.
Migration
Bumping the pin to @v0.4.0 enables reproducibility-mode: strict by default. Most well-behaved JS builds are accidentally reproducible — the common failure modes are timestamps and sorted-by-fs paths, both of which v0.4's mtime normalisation handles for free. Libraries with Date.now() or __BUILD_TIMESTAMP__-style build-time injection need to fix their build before the upgrade lands.
The safer path:
jobs:
release:
uses: forgesworn/release-action/.github/workflows/release.yml@v0.4.0
with:
vector-test-command: npm run test:vectors
reproducibility-mode: warnAfter a couple of green releases under warn, drop the input to take the default strict. Full walkthrough including diagnostic recipes for common non-determinism causes lives in docs/migration-from-v0.3.md.
What's not changing
- Composite action (
uses: forgesworn/release-action@v0.4.0) stays at v0.3-equivalent semantics: all gates, record-tarball, integrity block, but no reproduce gate because composite actions cannot define multi-job DAGs. Use the reusable workflow for the v0.4 flagship guarantee. The composite remains as a power-user escape hatch only. - Trusted publisher configuration is unchanged. The OIDC trusted-publisher record still points at your repo and your caller workflow file, not at
forgesworn/release-action. - All v0.3 inputs (
vector-test-command,strict-action-pins, etc.) work unchanged.
Known limits
- Single OS only. Both builds run on
ubuntu-24.04. Cross-OS reproducibility is a stronger claim that adds correctness burden on consumers and is out of scope for v0.4. - Two-run sample size. A non-determinism source that fires probabilistically (one in a thousand) won't reliably surface in two runs. Cost-of-CI-minutes trade-off.
SOURCE_DATE_EPOCHis opt-in for build tools. We can't forceesbuild/rollup/webpack/tscto honour it. Mtime normalisation closes the file-stamp gap unconditionally; embedded build-time strings inside compiled output remain the consumer's bug to fix and reliably fail the gate.
Why this matters
Two independent runners arriving at the same bytes is a stronger claim than SLSA provenance. Provenance attests one runner built these bytes once. The reproduce gate attests two runners building the same commit arrive at the same bytes — the actual byte-identical-output property that crypto-library customers care about and the property the rest of the JS release tooling ecosystem currently leaves to hope.
Stats
- 2 new step scripts (
normalise-mtimes.sh,compare-tarball-meta.sh) - 1 modified for reproducibility (
record-tarball.sh) - 16 new bats tests (59 total, all passing)
- shellcheck clean
- Zero new dependencies; still pure
bash+jq+gh+npm - ~1250 lines of bash across all step scripts (still inside the 30-minute audit budget)
- New design doc and migration guide
Full diff: v0.3.0...v0.4.0
v0.3.0 — artefact integrity bundle
Artefact integrity bundle. Two new pre-publish gates and a rewired publish path that lets downstream consumers verify the registry tarball byte-for-byte against what CI built.
What's new
record-tarballpacks the release artefact once into a known location, parsesnpm pack --jsonfor the sha512 integrity, hashes the tarball with sha256, and writestarball.metafor the rest of the pipeline to consume.publish-npmnow uploads the exact tarball recorded above rather than re-packing. The bytes the registry receives are byte-identical to the bytes the integrity block reports. On a clean re-run of an already-published release, the registry'sdist.integrityis compared to the recorded value: a match exits silently, a mismatch fails the workflow loudly. That scenario is registry tarball substitution and you want to know about it on the next CI run.update-releaseappends anArtefact integrityblock to the GitHub Release body containing filename, size, sha256, sha512 (npm format), and a one-linecurl | shasumrecipe. Anyone can fetch the registry tarball and confirm byte-equivalence with what CI built.verify-action-pinswalks.github/workflows/*.ymlin your repo and warns on anyuses: owner/repo@refline whose ref is not a 40-char hex SHA. Warn-only by default. Newstrict-action-pins: trueinput promotes warnings to a hard failure.forgesworn/release-actionis exempt by name; seeTHREAT-MODEL.mdfor the rationale.
Migration
None required. The new gates are additive and verify-action-pins is warn-only by default. Existing v0.2.x consumers can bump their pin to v0.3.0 (or stay on @v0) without changing any caller-workflow input.
If you want the strict pin gate:
with:
vector-test-command: npm run test:vectors
strict-action-pins: trueKnown limits
- The recorded sha256 is a single-runner integrity anchor, not a reproducibility proof. Two runners building the same commit may still produce two different hashes today, due to embedded timestamps and path leakage. Cross-runner reproducible-build is a planned
v0.4theme. The integrity block defends against registry tarball substitution, which does not require build-level determinism to detect. verify-action-pinscannot statically resolve dynamicuses: ${{ matrix.action }}lines and silently skips them. Audit those templates separately.
Verify a release tarball
Every release body now ends with a verify recipe like:
curl -sLO https://registry.npmjs.org/<pkg>/-/<pkg>-1.2.3.tgz
shasum -a 256 <pkg>-1.2.3.tgzCompare the result to the sha256: line in the release body. They should match exactly.
Stats
- 2 new step scripts (
record-tarball.sh,verify-action-pins.sh) - 13 new bats tests (43 total, all passing)
- shellcheck clean
- Zero new dependencies; still pure
bash+jq+gh+npm - ~1000 lines of bash across all step scripts (still inside the 30-minute audit budget)