Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/actions/verify-dist/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Verify build output
description: >-
Check that dist/ holds exactly the five files the release build hands
over, each matching its checksum. Shell only, from the same commit as
the workflow that calls it: nothing installed, nothing from scripts/.

runs:
using: composite
steps:
- name: Verify build output
shell: bash
run: |
set -euo pipefail
expected='catalog-index.json
catalog-index.json.sha256
catalog-sbom.spdx.json
catalog.sqlite
catalog.sqlite.sha256'
if [ "$(ls -A dist | LC_ALL=C sort)" != "$expected" ]; then
echo "::error::dist/ holds something other than the five build files; refusing to sign it"
ls -la dist
exit 1
fi
# Checked from the repository root: each .sha256 line names its
# file as `dist/<name>`, the path the build job hashed it under,
# so a `cd dist` first makes sha256sum look for dist/dist/<name>.
sha256sum -c dist/catalog.sqlite.sha256 dist/catalog-index.json.sha256
84 changes: 84 additions & 0 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Release build

# The release's `build` job, callable so that release.yml and
# release-dry-run.yml run the same steps. A dry run that copied them
# would drift from the release it is meant to rehearse, which is how
# the checksum check in #952 reached main untested.
on:
workflow_call:
inputs:
assign-ids:
description: >-
Run `pnpm assign-ids` locally before validating. A pull request
gets its ids from assign-ids.yml in parallel, so its tree may
not carry them yet; main always does.
type: boolean
default: false

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
# fetch-depth 1 is enough: the build stamps `updated_at` with the
# HEAD commit's date, which a shallow clone carries.
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "24"

- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Assign ids and io keys (local, not committed)
if: inputs.assign-ids
run: pnpm assign-ids

- name: Validate before release
run: pnpm validate

- name: Build database
run: pnpm build

- name: Build web index
run: pnpm build:index

- name: Generate checksum
run: |
sha256sum dist/catalog.sqlite > dist/catalog.sqlite.sha256
sha256sum dist/catalog-index.json > dist/catalog-index.json.sha256

# Here rather than beside the signature because the SBOM describes
# the dependencies the build ran with, and node_modules exists only
# in this job.
- name: Generate SBOM
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
with:
artifact-name: catalog-sbom.spdx.json
output-file: dist/catalog-sbom.spdx.json

- name: Upload build output
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: catalog-dist
path: |
dist/catalog.sqlite
dist/catalog.sqlite.sha256
dist/catalog-index.json
dist/catalog-index.json.sha256
dist/catalog-sbom.spdx.json
retention-days: 1
if-no-files-found: error
53 changes: 53 additions & 0 deletions .github/workflows/release-dry-run.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Release dry run

# Rehearses release.yml up to the signature: the same build job, then the
# same artifact check sign-and-release runs before it touches the key.
# Release only runs on a push to main, so without this a change to the
# release path is first exercised by a real release. No secret is
# referenced anywhere here, so a pull request's code can run it.
on:
pull_request:
branches: [main]
paths:
- ".github/workflows/release.yml"
- ".github/workflows/release-build.yml"
- ".github/workflows/release-dry-run.yml"
- ".github/actions/verify-dist/**"
- "scripts/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
build:
uses: ./.github/workflows/release-build.yml
with:
assign-ids: true

verify:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Download build output
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: catalog-dist
path: dist

- name: Verify build output
uses: ./.github/actions/verify-dist
79 changes: 6 additions & 73 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ permissions:
# that ran install before the split.
# build install, validate, build, index, checksums, SBOM. A
# read-only token and nothing else; hands dist/ on as
# an artifact.
# an artifact. Lives in release-build.yml, which
# release-dry-run.yml also calls on pull requests.
# sign-and-release downloads that artifact, installs nothing, verifies
# minisign by digest, signs under umask 077 with the
# key in a mktemp file, attests and publishes. Until
Expand Down Expand Up @@ -68,64 +69,9 @@ jobs:
build:
needs: version
if: needs.version.outputs.has-changesets == 'false'
runs-on: ubuntu-latest
permissions:
contents: read

steps:
# fetch-depth 1 is enough: the build stamps `updated_at` with the
# HEAD commit's date, which a shallow clone carries.
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "24"

- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Validate before release
run: pnpm validate

- name: Build database
run: pnpm build

- name: Build web index
run: pnpm build:index

- name: Generate checksum
run: |
sha256sum dist/catalog.sqlite > dist/catalog.sqlite.sha256
sha256sum dist/catalog-index.json > dist/catalog-index.json.sha256

# Here rather than beside the signature because the SBOM describes
# the dependencies the build ran with, and node_modules exists only
# in this job.
- name: Generate SBOM
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
with:
artifact-name: catalog-sbom.spdx.json
output-file: dist/catalog-sbom.spdx.json

- name: Upload build output
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: catalog-dist
path: |
dist/catalog.sqlite
dist/catalog.sqlite.sha256
dist/catalog-index.json
dist/catalog-index.json.sha256
dist/catalog-sbom.spdx.json
retention-days: 1
if-no-files-found: error
uses: ./.github/workflows/release-build.yml

sign-and-release:
needs: build
Expand Down Expand Up @@ -155,23 +101,10 @@ jobs:
# The artifact was produced by a job that ran code from the tree, so
# it is data to check, not a directory to trust: exactly the files
# the build is expected to hand over, each matching its checksum.
# A composite action from this commit's .github/, shell only, shared
# with release-dry-run.yml so a pull request exercises this check.
- name: Verify build output
run: |
set -euo pipefail
expected='catalog-index.json
catalog-index.json.sha256
catalog-sbom.spdx.json
catalog.sqlite
catalog.sqlite.sha256'
if [ "$(ls -A dist | LC_ALL=C sort)" != "$expected" ]; then
echo "::error::dist/ holds something other than the five build files; refusing to sign it"
ls -la dist
exit 1
fi
# Checked from the repository root: each .sha256 line names its
# file as `dist/<name>`, the path the build job hashed it under,
# so a `cd dist` first makes sha256sum look for dist/dist/<name>.
sha256sum -c dist/catalog.sqlite.sha256 dist/catalog-index.json.sha256
uses: ./.github/actions/verify-dist

# Verified by a digest pinned here, in review, in git, before the
# signing key exists anywhere in this job. `curl | tar xz` ran an
Expand Down
Loading