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
182 changes: 130 additions & 52 deletions .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,118 +7,196 @@ on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to build (e.g., v0.12.0)'
description: Immutable release tag to recover
required: true
type: string
source_ref:
description: 'Source ref to build/publish (defaults to tag; use only for release recovery)'
description: Optional recovery ref; must resolve to the release tag commit
required: false
type: string
recovery_run_id:
description: Original release workflow run containing the retained approved artifacts
required: true
type: string

permissions: {}

concurrency:
group: build-binaries-${{ github.event.inputs.tag || github.ref_name }}
group: release-${{ github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false

jobs:
publish-npm:
resolve-source:
runs-on: ubuntu-latest
environment: npm-publish
permissions:
contents: read
id-token: write
outputs:
commit: ${{ steps.source.outputs.commit }}
steps:
- name: Checkout release control code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22.19.0'
- name: Resolve and validate immutable release source
id: source
env:
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
SOURCE_REF: ${{ github.event.inputs.source_ref }}
EXPECTED_COMMIT: ${{ github.event_name == 'push' && github.sha || '' }}
run: node scripts/release-source.mjs

prepare-artifacts:
needs: resolve-source
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
env:
DIFFEX_SOURCE_COMMIT: ${{ needs.resolve-source.outputs.commit }}
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
SOURCE_REF: ${{ github.event.inputs.source_ref || github.event.inputs.tag || github.ref_name }}
steps:
- name: Checkout
- name: Checkout immutable release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.SOURCE_REF }}
ref: ${{ needs.resolve-source.outputs.commit }}
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
node-version: '22.19.0'
registry-url: 'https://registry.npmjs.org'
cache: npm

- name: Restore original approved artifact bytes
id: restore
env:
GH_TOKEN: ${{ github.token }}
RECOVERY_RUN_ID: ${{ inputs.recovery_run_id }}
run: node scripts/restore-release-artifacts.mjs
- name: Install pinned npm
if: steps.restore.outputs.restored != 'true'
run: npm install -g npm@11.16.0 --ignore-scripts
- name: Install system dependencies
if: steps.restore.outputs.restored != 'true'
run: |
sudo apt-get update
sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep
sudo ln -s $(which fdfind) /usr/local/bin/fd

- name: Install dependencies
sudo ln -s "$(command -v fdfind)" /usr/local/bin/fd
- name: Install locked dependencies
if: steps.restore.outputs.restored != 'true'
run: npm ci --ignore-scripts

- name: Build
run: npm run build

- name: Check
run: npm run check

- name: Test
run: npm test

- name: Upgrade npm for trusted publishing
- name: Freeze model data and build without catalog regeneration
if: steps.restore.outputs.restored != 'true'
run: |
npm install -g npm@11.16.0 --ignore-scripts
npm --version

- name: Publish npm packages
run: node scripts/publish.mjs

- name: Retain private payload inventory and debug maps
if: always()
npm run hydrate:model-data
node scripts/release-artifacts.mjs freeze .artifacts/release
npm run build:offline
git diff --exit-code HEAD
- name: Validate release candidate
if: steps.restore.outputs.restored != 'true'
run: |
npm run check
./test.sh
git diff --exit-code HEAD
- name: Prepare reviewed tarballs and private source snapshot
if: steps.restore.outputs.restored != 'true'
run: node scripts/release-artifacts.mjs prepare .artifacts/release
- name: Verify retained input and artifact identities
run: node scripts/release-artifacts.mjs verify .artifacts/release
- name: Retain immutable private release inputs before publication
if: steps.restore.outputs.upload == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: private-release-${{ needs.resolve-source.outputs.commit }}
path: .artifacts/release/
if-no-files-found: error
include-hidden-files: true
retention-days: 90
- name: Retain private debug maps
if: always() && steps.restore.outputs.restored != 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: private-npm-evidence-${{ env.RELEASE_TAG }}
path: |
.artifacts/npm-payload-inventory.json
.artifacts/private-debug/
name: private-debug-${{ needs.resolve-source.outputs.commit }}
path: .artifacts/private-debug/
if-no-files-found: warn
include-hidden-files: true
retention-days: 14
retention-days: 90

publish-npm:
needs: [resolve-source, prepare-artifacts]
runs-on: ubuntu-latest
environment: npm-publish
permissions:
contents: read
actions: read
id-token: write
env:
DIFFEX_SOURCE_COMMIT: ${{ needs.resolve-source.outputs.commit }}
steps:
- name: Checkout immutable release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.resolve-source.outputs.commit }}
persist-credentials: false
- name: Setup Node.js for trusted publishing
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22.19.0'
registry-url: 'https://registry.npmjs.org'
- name: Download approved artifact bytes
env:
GH_TOKEN: ${{ github.token }}
run: gh run download "$GITHUB_RUN_ID" --repo diffexai/diffex --name "private-release-$DIFFEX_SOURCE_COMMIT" --dir .artifacts/release
- name: Install pinned npm for trusted publishing
run: npm install -g npm@11.16.0 --ignore-scripts
- name: Publish or verify exact npm tarballs
run: node scripts/publish.mjs --artifacts .artifacts/release

announce-diffex-release:
needs: [resolve-source, publish-npm]
runs-on: ubuntu-latest
timeout-minutes: 20
needs: publish-npm
environment: diffex-release-upload
concurrency:
group: announce-diffex-release
cancel-in-progress: false
permissions:
contents: read
actions: read
env:
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
SOURCE_REF: ${{ github.event.inputs.source_ref || github.event.inputs.tag || github.ref_name }}
AWS_ACCESS_KEY_ID: ${{ secrets.DIFFEX_ARTIFACTS_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DIFFEX_ARTIFACTS_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_EC2_METADATA_DISABLED: 'true'
R2_ENDPOINT: https://6b24afa849f8254ab5e6e2d1956239cc.r2.cloudflarestorage.com
DIFFEX_SOURCE_COMMIT: ${{ needs.resolve-source.outputs.commit }}
steps:
- name: Checkout
- name: Checkout immutable release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.SOURCE_REF }}
ref: ${{ needs.resolve-source.outputs.commit }}
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'

node-version: '22.19.0'
- name: Download approved artifact bytes
env:
GH_TOKEN: ${{ github.token }}
run: gh run download "$GITHUB_RUN_ID" --repo diffexai/diffex --name "private-release-$DIFFEX_SOURCE_COMMIT" --dir .artifacts/release
- name: Verify AWS CLI
run: aws --version

- name: Announce verified Diffex release
env:
AWS_ACCESS_KEY_ID: ${{ secrets.DIFFEX_ARTIFACTS_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DIFFEX_ARTIFACTS_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_EC2_METADATA_DISABLED: 'true'
R2_ENDPOINT: https://6b24afa849f8254ab5e6e2d1956239cc.r2.cloudflarestorage.com
run: |
node scripts/publish-release-announcement.mjs \
--version "${RELEASE_TAG#v}" \
--source-commit "$DIFFEX_SOURCE_COMMIT" \
--artifacts .artifacts/release \
--bucket diffex-artifacts \
--endpoint "$R2_ENDPOINT"
9 changes: 6 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Use this bootstrap process only once, before the first normal release.

Bootstrap publication deliberately omits provenance because it runs interactively outside the trusted CI environment.
It does not assign the `latest` dist-tag.
The command is resumable after a partial failure: it skips packages already published at the exact workspace version and refuses packages that already exist only at other versions.
The command is resumable after a partial failure: it downloads and hashes packages already published at the exact workspace version against the retained approved artifacts and refuses packages that already exist only at other versions.

4. Run every `npm trust github` command printed by the bootstrap command.
The trusted publisher values must remain:
Expand All @@ -87,12 +87,15 @@ Run the appropriate release command only after local checks pass and the release
```bash
npm run release:patch
npm run release:minor
npm run release:major
```

The release command updates versions and generated artifacts, runs checks and tests, creates the release commits and tag, and pushes `main` plus the tag directly.
The release command updates versions and generated artifacts, runs checks and tests, creates the release commits and tag, and requires interactive review of generated diffs, and pushes `main` plus the tag atomically.
Only `patch` (fixes and additions) and `minor` (breaking changes) are supported.
Major and explicit-version release commands are rejected.
It is not a dry-run command.
Review the target version and obtain approval for the remote push before running it.

The pushed tag triggers `.github/workflows/build-binaries.yml`.
Do not announce the release until the workflow verifies npm publication and writes the release announcement successfully.

See [release promotion and recovery](docs/release/promotion.md) for immutable source selection, retained artifact recovery, and exact failure procedures.
23 changes: 14 additions & 9 deletions docs/release/contract.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Stable npm release contract

This contract governs `releases/v1/latest.json`, `releases/v1/releases/<version>.json`, and unauthenticated `GET https://api.diffex.ai/api/latest-version`.
The CLI monorepo publishes records after checking every public workspace package at its exact version and confirming tarball availability.
The CLI monorepo publishes records only after downloading every public workspace package at its exact version and comparing the tarball bytes with the retained approved build manifest.
The public API Worker validates the stored record and projects the public response without querying npm per request.
The marker means npm-verified stable release availability at publication time.
The release workflow publishes the GitHub Release after the announcement, so the marker alone does not guarantee publicly available binary assets or their upgrade path.
Integrity metadata is validated structurally; this contract does not claim that the publisher downloads tarballs and recomputes their digests.
The approved distribution policy currently enables npm and defers standalone binaries and other installation channels.
The publisher rejects additional enabled channels until their required public asset verification is implemented.
This schema remains an npm contract and does not describe binary assets, signatures, install locks, or a cross-channel release manifest.
The private build manifest binds the release commit, source tree, lockfile, disclosure policy, frozen model data, private source archive, and exact npm tarball sizes, SHA-256 digests, and SHA-512 integrity values.
Registry verification downloads each canonical npm tarball anonymously, rejects redirects, and recomputes both digests before accepting an existing npm version or advancing release storage.
The stronger publisher prerequisites add no stored or HTTP fields and require no installed-client update.

## Stored record

Expand Down Expand Up @@ -94,9 +98,10 @@ All temporary publisher fixtures are removed after testing, and these checks nee

## Integration status

Record validation and response projection are defined and tested locally.
The existing Worker shell continues returning unavailable until R2 reads and lookup outcome mapping are connected.
The live CLI checker still uses its previous authority and parsing behavior; the strict consumer parser is available for the separately planned endpoint migration.
Its existing update-note behavior has not been changed by defining a response with no note field.
There is no deployed R2-to-HTTP-to-CLI E2E path in this change, and fixture tests do not claim deployed routing or live installation readiness.
The landing application and authenticated Render `/v1` contracts require no change for this contract definition.
The public Worker serves the fixed R2 latest record, and the CLI consumes the exact canonical version endpoint with a pinned npm installation target.
The cloud production and staging operations records retain their deployment and route evidence.
This publisher hardening introduces no Worker deployment or live release publication.
Command-level publication regressions use controlled npm bytes and intercepted storage operations.
The unchanged cloud contract fixtures and Worker tests verify consumer compatibility.
Live publication against actual approved packages remains gated by the separate release authorization and packaging prerequisites.
The landing application and authenticated Render `/v1` contracts require no change.
Loading