Merge pull request #176 from jplomas/main #177
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |