Skip to content

Merge pull request #176 from jplomas/main #177

Merge pull request #176 from jplomas/main

Merge pull request #176 from jplomas/main #177

Workflow file for this run

name: Release
# Publishes on push to main; branch protection + the preflight job below
# gate the publish path so unverified code never reaches npm.
on:
push:
branches: [main]
workflow_dispatch:
# Serialize releases, never cancel them: a cancellation landing between the
# tag push (prepare) and npm publish would strand a tagged-but-unpublished
# version — the orphaned-release failure this pipeline is hardened against.
# Back-to-back merges queue here instead of racing.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
# Defence-in-depth: re-runs lint + tests + build + dist-clean on the
# checked-out commit before the publish path begins.
preflight:
name: Preflight (lint + test + build + dist-clean)
runs-on: ubuntu-latest
if: github.repository == 'theQRL/qrypto.js'
permissions:
contents: read
outputs:
# The exact commit this job validated. Every downstream job either
# checks out THIS SHA or asserts against it — `main` can advance while
# a release run is queued/running, and the pipeline must never validate
# one tree and ship another.
sha: ${{ steps.resolve.outputs.sha }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: main
persist-credentials: false
- name: Resolve validated commit
id: resolve
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22.x'
- run: npm ci
- run: npm run lint
- run: npm run check-shared
- run: npm run typecheck
- run: npm test
- run: npm run build
- name: Verify dist/ is up to date
run: |
if [ -n "$(git diff --name-only packages/*/dist/)" ]; then
echo "::error::dist/ files are out of date relative to src/. Run 'npm run build' and commit the result before merging."
git diff --stat packages/*/dist/
exit 1
fi
# ===========================================================================
# Prepare: build, run multi-semantic-release, pack tarballs, hand off as
# an artifact to downstream smoke/publish/slsa jobs. The artifact is the
# single source of truth for what gets shipped — every later job consumes
# the same bytes that were inspected here.
# ===========================================================================
prepare:
name: Prepare Release
needs: preflight
runs-on: ubuntu-latest
if: github.repository == 'theQRL/qrypto.js'
environment: npm-publish
permissions:
contents: write # push release commits and tags via multi-semantic-release
issues: write # semantic-release opens issues for release failures
pull-requests: write # semantic-release comments on released PRs
outputs:
released: ${{ steps.released.outputs.released }}
count: ${{ steps.released.outputs.count }}
packages: ${{ steps.released.outputs.packages }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# semantic-release must run on writable HEAD-of-main (it pushes
# release commits and tags); the assertion below binds that HEAD
# to the commit preflight actually validated.
ref: main
fetch-depth: 0
persist-credentials: false
- name: Assert HEAD is the preflighted commit
env:
PREFLIGHT_SHA: ${{ needs.preflight.outputs.sha }}
run: |
set -euo pipefail
head_sha="$(git rev-parse HEAD)"
if [ "${head_sha}" != "${PREFLIGHT_SHA}" ]; then
echo "::error::main advanced between preflight (${PREFLIGHT_SHA}) and prepare (${head_sha}); refusing to release a tree preflight never validated. The queued release run for the newer push will release it."
exit 1
fi
- name: Setup Node.js
# Pinned to 22.14.0 to match the bundled npm version that can cleanly
# self-update to npm 11 in the publish job. Newer Node 22.x patches
# ship an npm where the global self-install corrupts its own
# node_modules (missing promise-retry).
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.14.0
- name: Install dependencies
run: npm ci
- name: Build packages before release analysis
run: npm run build
- name: Inspect package tarballs before release analysis
run: npm run release:inspect-packages
- name: Snapshot package versions
run: node scripts/release/packages.js snapshot .release/release-versions-before.json
- name: Setup SSH Agent
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
with:
ssh-private-key: ${{ secrets.BYPASS_SSH_KEY }}
- name: Configure Git for SSH
# Host keys are pinned to GitHub's published values
# (https://api.github.com/meta, verified 2026-06-13) instead of a
# run-time ssh-keyscan: trust-on-first-use would let a MITM of this
# runner supply its own key for the branch-protection-bypass push.
run: |
mkdir -p ~/.ssh
cat >> ~/.ssh/known_hosts <<'EOF'
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
EOF
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global url."git@github.com:".insteadOf "https://github.com/"
git remote set-url origin "git@github.com:${GITHUB_REPOSITORY}.git"
- name: Run multi-semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: github-actions[bot]
GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
GIT_COMMITTER_NAME: github-actions[bot]
GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
run: npm run release -- --repository-url "git@github.com:${GITHUB_REPOSITORY}.git"
- name: Detect released packages
id: released
run: node scripts/release/packages.js diff .release/release-versions-before.json .release/released-packages.json
- name: Rebuild packages after version updates
if: steps.released.outputs.released == 'true'
run: npm run build
- name: Inspect package tarballs after version updates
if: steps.released.outputs.released == 'true'
run: npm run release:inspect-packages
- name: Pack released packages
if: steps.released.outputs.released == 'true'
run: |
set -euo pipefail
mkdir -p dist/tarballs
test -s .release/released-packages.tsv
tarball_dir="$(pwd)/dist/tarballs"
while IFS=$'\t' read -r package_path package_name package_version _release_tag _tarball_name; do
echo "Packing ${package_name}@${package_version}"
(cd "${package_path}" && npm pack --pack-destination "${tarball_dir}")
done < .release/released-packages.tsv
cp .release/released-packages.json .release/released-packages.tsv dist/
- name: Generate checksums
if: steps.released.outputs.released == 'true'
run: |
set -euo pipefail
(cd dist/tarballs && sha256sum ./*.tgz) > dist/checksums-sha256.txt
(cd dist/tarballs && sha512sum ./*.tgz) > dist/checksums-sha512.txt
- name: Upload release candidate tarballs
if: steps.released.outputs.released == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-candidate-tarballs
path: |
dist/tarballs/*.tgz
dist/released-packages.json
dist/released-packages.tsv
dist/checksums-sha256.txt
dist/checksums-sha512.txt
retention-days: 5
# ===========================================================================
# Smoke test: install each tarball into throwaway CJS + ESM projects on
# a different Node major than `prepare` used to catch packaging regressions
# (missing files, broken exports map, type:module mismatch) before publish.
# ===========================================================================
smoke-node20:
name: Smoke Tarballs (Node 20)
needs: [preflight, prepare]
if: needs.prepare.outputs.released == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# The SHA preflight validated — not floating `main`, which may have
# advanced while this run was queued. Only scripts/ is used here;
# the packages under test come from the artifact handoff.
ref: ${{ needs.preflight.outputs.sha }}
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20.x
- name: Download release candidate tarballs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate-tarballs
path: dist
- name: Smoke test packed tarballs
run: node scripts/release/smoke-tarballs.js dist/released-packages.tsv dist/tarballs
# ===========================================================================
# Publish: download the prepared tarballs and publish each with npm trusted
# publishing. The npm 11 self-install lives only in this job, isolated from
# any other npm operations that could leave its node_modules in a bad
# state.
# ===========================================================================
publish:
name: Publish
needs: [prepare, smoke-node20]
if: needs.prepare.outputs.released == 'true'
runs-on: ubuntu-latest
environment: npm-publish
permissions:
contents: write # upload SBOMs and checksums to GitHub Releases
id-token: write # npm trusted publishing OIDC token + provenance signing
attestations: write # create build-provenance attestations
env:
NPM_CONFIG_PROVENANCE: "true"
NPM_CONFIG_ACCESS: public
# No checkout: this job operates exclusively on the artifact handoff.
# Everything it publishes, verifies, scans, and attests comes from the
# release-candidate tarballs — a source tree here could only drift from
# the released bytes.
steps:
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.14.0
registry-url: https://registry.npmjs.org/
- name: Use npm 11 for trusted publishing
# Exact-pinned: this is the most privileged job in the repo
# (id-token: write) and a floating range here resolves at publish
# time. Keep in sync with the root package.json `overrides.npm` pin.
run: |
npm install -g npm@11.17.0
npm --version
- name: Download release candidate tarballs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate-tarballs
path: dist
- name: Publish tarballs with trusted publishing
run: |
set -euo pipefail
while IFS=$'\t' read -r _package_path package_name package_version _release_tag tarball_name; do
echo "Publishing ${package_name}@${package_version}"
npm publish "dist/tarballs/${tarball_name}" --access public
done < dist/released-packages.tsv
# Tags and GitHub releases were already created by multi-semantic-release
# in the prepare job; if npm publishing silently failed we would otherwise
# ship a tag/release with no npm artifact (this exact failure orphaned
# wallet.js v6.2.0). Verify the registry actually serves every released
# version before attaching release assets. Recovery runbook: RELEASE.md
# "Recovering an orphaned release".
- name: Verify packages are live on npm
run: |
set -euo pipefail
while IFS=$'\t' read -r _package_path package_name package_version _release_tag _tarball_name; do
echo "Verifying ${package_name}@${package_version} on the npm registry"
ok=""
for attempt in 1 2 3 4 5 6 7 8 9 10; do
served="$(npm view "${package_name}@${package_version}" version 2>/dev/null || true)"
if [ "${served}" = "${package_version}" ]; then
echo " confirmed on attempt ${attempt}"
ok=1
break
fi
echo " not yet visible (attempt ${attempt}/10); retrying in 15s"
sleep 15
done
if [ -z "${ok}" ]; then
echo "::error::${package_name}@${package_version} was published but is not served by the registry after 10 attempts. Tag and GitHub release exist without an npm artifact — follow RELEASE.md 'Recovering an orphaned release'."
exit 1
fi
done < dist/released-packages.tsv
# SBOMs are generated FROM the released tarballs (extracted so the
# scanner reads the real package manifests), never from a source
# checkout — an SBOM of "whatever main is right now" can mis-describe
# the published bytes, which is the exact story it exists to tell.
- name: Extract tarballs for SBOM generation
run: |
set -euo pipefail
mkdir -p dist/sbom-input
while IFS=$'\t' read -r _package_path _package_name _package_version _release_tag tarball_name; do
dest="dist/sbom-input/$(basename "${tarball_name}" .tgz)"
mkdir -p "${dest}"
tar -xzf "dist/tarballs/${tarball_name}" -C "${dest}"
done < dist/released-packages.tsv
- name: Generate SBOM SPDX
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
path: dist/sbom-input
format: spdx-json
output-file: dist/sbom-spdx.json
artifact-name: sbom-spdx.json
- name: Generate SBOM CycloneDX
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
path: dist/sbom-input
format: cyclonedx-json
output-file: dist/sbom-cyclonedx.json
artifact-name: sbom-cyclonedx.json
- name: Attest release artefacts
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: |
dist/tarballs/*.tgz
dist/checksums-sha256.txt
dist/checksums-sha512.txt
# SBOM attestation subjects are the TARBALLS — the statement signed is
# "this SBOM describes these artifact digests", not "this SBOM exists".
- name: Attest SBOM SPDX
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: dist/tarballs/*.tgz
sbom-path: dist/sbom-spdx.json
- name: Attest SBOM CycloneDX
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: dist/tarballs/*.tgz
sbom-path: dist/sbom-cyclonedx.json
- name: Upload release artefacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
while IFS=$'\t' read -r _package_path _package_name _package_version release_tag tarball_name; do
gh release upload "${release_tag}" \
"dist/tarballs/${tarball_name}" \
dist/checksums-sha256.txt \
dist/checksums-sha512.txt \
dist/sbom-spdx.json \
dist/sbom-cyclonedx.json \
--clobber \
--repo "${GITHUB_REPOSITORY}"
done < dist/released-packages.tsv
# ===========================================================================
# SLSA Level 3 Provenance
# ===========================================================================
slsa-subjects:
name: Compute SLSA Subjects
needs: prepare
if: needs.prepare.outputs.released == 'true'
runs-on: ubuntu-latest
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Download release candidate tarballs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate-tarballs
path: dist
- name: Generate SLSA subject hashes
id: hash
run: |
set -euo pipefail
cd dist/tarballs
echo "hashes=$(sha256sum ./*.tgz | base64 -w0)" >> "$GITHUB_OUTPUT"
slsa-provenance:
name: SLSA Provenance
needs: [prepare, publish, slsa-subjects]
if: needs.prepare.outputs.released == 'true'
permissions:
actions: read # SLSA generator inspects the run's workflow definition
id-token: write # SLSA provenance is signed via OIDC
contents: write # upload provenance to GitHub Releases
# NOTE: SLSA generator must be referenced by tag, not SHA — the reusable
# workflow inspects its own caller-ref to embed the version into the
# provenance and aborts when called by digest.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: ${{ needs.slsa-subjects.outputs.hashes }}
upload-assets: false
provenance-name: provenance.intoto.jsonl
slsa-upload:
name: Upload SLSA Provenance
needs: [prepare, publish, slsa-provenance]
if: needs.prepare.outputs.released == 'true'
runs-on: ubuntu-latest
permissions:
contents: write # attach SLSA provenance asset to package GitHub Releases
steps:
- name: Download release metadata
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate-tarballs
path: dist
- name: Download SLSA provenance
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: provenance.intoto.jsonl
path: dist
- name: Upload SLSA provenance to package releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
while IFS=$'\t' read -r _package_path _package_name _package_version release_tag _tarball_name; do
gh release upload "${release_tag}" \
dist/provenance.intoto.jsonl \
--clobber \
--repo "${GITHUB_REPOSITORY}"
done < dist/released-packages.tsv