Skip to content

Release

Release #287

Workflow file for this run

name: Release
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
workflow_dispatch:
inputs:
action:
description: Validate the release or recover one missing artifact from an immutable tag.
required: true
default: dry-run
type: choice
options:
- dry-run
- recover-npm
- recover-github-release
- recover-rust-distribution
release_tag:
description: Recovery tag in vX.Y.Z form.
required: false
type: string
release_commit:
description: Full immutable commit SHA referenced by release_tag.
required: false
type: string
permissions:
contents: read
concurrency:
group: release-main
cancel-in-progress: false
jobs:
install-matrix:
if: |
(github.event_name == 'workflow_dispatch' && inputs.action == 'dry-run') ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.conclusion == 'success' &&
vars.RELEASE_AUTOMATION_ENABLED == 'true'
)
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
node: 22
- os: macos-latest
node: 22
runs-on: ${{ matrix.os }}
steps:
- name: Checkout tested candidate
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: ${{ matrix.node }}
cache: npm
- name: Install dependencies
run: npm ci
- name: Link and smoke CLI
run: |
npm link
zeroshot --help
zeroshot --version
zeroshot list
- name: Test platform process metrics
run: |
node - <<'NODE'
const { getProcessMetrics, isPlatformSupported } = require('./src/process-metrics');
(async () => {
const metrics = await getProcessMetrics(process.pid, { samplePeriodMs: 100 });
if (!metrics.exists) throw new Error('Process should exist');
if (typeof metrics.cpuPercent !== 'number') throw new Error('cpuPercent must be numeric');
if (typeof metrics.memoryMB !== 'number') throw new Error('memoryMB must be numeric');
console.log({ platform: process.platform, supported: isPlatformSupported(), metrics });
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
dry-run:
outputs:
tag: ${{ steps.rust-version.outputs.tag }}
needs: [install-matrix]
if: |
github.event_name == 'workflow_dispatch' &&
inputs.action == 'dry-run' &&
needs.install-matrix.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout exact dispatched candidate
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.sha }}
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 24
cache: npm
registry-url: https://registry.npmjs.org
- name: Install pinned dependencies
run: npm ci
- name: Fetch immutable release history
run: git fetch --tags --force
- name: Release preflight
run: npm run release:preflight
- name: Verify package tarball
shell: bash
run: |
set -euo pipefail
pack_dir="$(mktemp -d)"
prefix_dir="$(mktemp -d)"
trap 'rm -rf "$pack_dir" "$prefix_dir"' EXIT
npm pack --pack-destination "$pack_dir"
tarball="$(find "$pack_dir" -maxdepth 1 -name '*.tgz' -print -quit)"
npm install --global --prefix "$prefix_dir" "$tarball"
"$prefix_dir/bin/zeroshot" --version
"$prefix_dir/bin/zeroshot" --help
"$prefix_dir/bin/zeroshot" list
- name: Run semantic-release dry run
id: semantic-dry-run
run: node scripts/release-dry-run.js
- name: Resolve dry-run Rust version
id: rust-version
shell: bash
run: |
version="${{ steps.semantic-dry-run.outputs.version }}"
if [[ -z "$version" ]]; then
version="$(node scripts/rust-distribution.js print-version)"
fi
echo "tag=v$version" >> "$GITHUB_OUTPUT"
release-plan:
outputs:
version: ${{ steps.semantic-plan.outputs.version }}
tag: ${{ steps.planned-tag.outputs.tag }}
needs: [install-matrix]
if: |
always() &&
needs.install-matrix.result == 'success' &&
github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.conclusion == 'success' &&
vars.RELEASE_AUTOMATION_ENABLED == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout exact CI-tested main commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event.workflow_run.head_sha }}
- name: Verify this is the CI-tested main commit
id: candidate
shell: bash
env:
TESTED_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
current_sha="$(git rev-parse HEAD)"
remote_sha="$(git ls-remote origin refs/heads/main | cut -f1)"
if [[ "$current_sha" != "$TESTED_SHA" || "$remote_sha" != "$TESTED_SHA" ]]; then
echo "Skipping obsolete release: tested=$TESTED_SHA checkout=$current_sha main=$remote_sha"
echo "current=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "current=true" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
if: steps.candidate.outputs.current == 'true'
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 24
cache: npm
registry-url: https://registry.npmjs.org
- name: Install pinned dependencies
if: steps.candidate.outputs.current == 'true'
run: npm ci
- name: Fetch immutable release history
if: steps.candidate.outputs.current == 'true'
run: git fetch --tags --force
- name: Release preflight
if: steps.candidate.outputs.current == 'true'
run: npm run release:preflight
- name: Verify package tarball
if: steps.candidate.outputs.current == 'true'
shell: bash
run: |
set -euo pipefail
pack_dir="$(mktemp -d)"
prefix_dir="$(mktemp -d)"
trap 'rm -rf "$pack_dir" "$prefix_dir"' EXIT
npm pack --pack-destination "$pack_dir"
tarball="$(find "$pack_dir" -maxdepth 1 -name '*.tgz' -print -quit)"
npm install --global --prefix "$prefix_dir" "$tarball"
"$prefix_dir/bin/zeroshot" --version
"$prefix_dir/bin/zeroshot" --help
"$prefix_dir/bin/zeroshot" list
- name: Resolve semantic-release version before native builds
id: semantic-plan
if: steps.candidate.outputs.current == 'true'
env:
GITHUB_REF_NAME: main
run: node scripts/release-dry-run.js
- name: Resolve planned immutable release tag
id: planned-tag
if: steps.semantic-plan.outputs.version != ''
env:
RELEASE_VERSION: ${{ steps.semantic-plan.outputs.version }}
run: echo "tag=v$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
shell: bash
rust-recovery-plan:
outputs:
tag: ${{ steps.immutable.outputs.tag }}
if: |
github.event_name == 'workflow_dispatch' &&
inputs.action == 'recover-rust-distribution'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Validate Rust recovery inputs
shell: bash
env:
RELEASE_COMMIT: ${{ inputs.release_commit }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "::error::release_tag must match vX.Y.Z"
exit 1
}
[[ "$RELEASE_COMMIT" =~ ^[0-9a-f]{40}$ ]] || {
echo "::error::release_commit must be a full lowercase SHA"
exit 1
}
- name: Checkout immutable release commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.release_commit }}
- name: Fetch protected release refs
run: |
git fetch origin main
git fetch origin "refs/tags/${{ inputs.release_tag }}:refs/tags/${{ inputs.release_tag }}"
- name: Verify immutable matching release tag
id: immutable
shell: bash
env:
RELEASE_COMMIT: ${{ inputs.release_commit }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
tag_commit="$(git rev-parse "${RELEASE_TAG}^{commit}")"
[[ "$tag_commit" == "$RELEASE_COMMIT" ]] || {
echo "::error::release tag $RELEASE_TAG resolves to $tag_commit, not $RELEASE_COMMIT"
exit 1
}
if ! git merge-base --is-ancestor "$RELEASE_COMMIT" origin/main; then
echo "::error::release commit $RELEASE_COMMIT is not an ancestor of main"
exit 1
fi
echo "tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
rust-binaries:
needs: [dry-run, release-plan, rust-recovery-plan]
if: |
always() &&
(
(
github.event_name == 'workflow_dispatch' &&
inputs.action == 'dry-run' &&
needs.dry-run.result == 'success'
) ||
(
github.event_name == 'workflow_dispatch' &&
inputs.action == 'recover-rust-distribution' &&
needs.rust-recovery-plan.result == 'success'
) ||
(
github.event_name == 'workflow_run' &&
needs.release-plan.result == 'success' &&
needs.release-plan.outputs.version != ''
)
)
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
executable: zeroshot-rust
c-compiler: cc
- target: aarch64-unknown-linux-gnu
runner: ubuntu-24.04-arm
executable: zeroshot-rust
c-compiler: cc
- target: x86_64-apple-darwin
runner: macos-15-intel
executable: zeroshot-rust
c-compiler: cc
- target: aarch64-apple-darwin
runner: macos-14
executable: zeroshot-rust
c-compiler: cc
- target: x86_64-pc-windows-msvc
runner: windows-latest
executable: zeroshot-rust.exe
c-compiler: cl.exe
runs-on: ${{ matrix.runner }}
permissions:
contents: read
env:
RELEASE_TAG: ${{ inputs.action == 'dry-run' && needs.dry-run.outputs.tag || inputs.action == 'recover-rust-distribution' && needs.rust-recovery-plan.outputs.tag || needs.release-plan.outputs.tag }}
BINARY_PATH: target/${{ matrix.target }}/release/${{ matrix.executable }}
steps:
- name: Checkout immutable release source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ inputs.action == 'dry-run' && github.sha || inputs.action == 'recover-rust-distribution' && inputs.release_commit || github.event.workflow_run.head_sha }}
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 24
cache: npm
- name: Install pinned script dependencies
run: npm ci --ignore-scripts
- name: Setup Rust 1.97.0 target
uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.97.0
targets: ${{ matrix.target }}
- name: Verify bundled SQLite C toolchain
if: runner.os != 'Windows'
shell: bash
env:
C_COMPILER: ${{ matrix.c-compiler }}
run: command -v "$C_COMPILER"
- name: Verify bundled SQLite MSVC toolchain
if: runner.os == 'Windows'
shell: pwsh
run: |
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
$installation = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
if (-not $installation) {
throw 'MSVC C toolchain required by bundled SQLite is unavailable'
}
- name: Stage planned Rust package version
run: node scripts/rust-distribution.js stage-version --tag "$RELEASE_TAG"
shell: bash
- name: Verify Rust and release tag versions are coupled
run: node scripts/rust-distribution.js check-version --tag "$RELEASE_TAG"
shell: bash
- name: Build standalone Rust release binary
run: cargo build --release --locked -p zeroshot-rust --bin zeroshot-rust --target ${{ matrix.target }}
- name: Run standalone Rust release binary
run: node scripts/rust-distribution.js smoke --binary "$BINARY_PATH"
shell: bash
- name: Package target archive
run: |
node scripts/rust-distribution.js package \
--target "${{ matrix.target }}" \
--version "$RELEASE_TAG" \
--binary "$BINARY_PATH" \
--out rust-release
shell: bash
- name: Run executable extracted from target archive
run: |
node scripts/rust-distribution.js smoke-archive \
--target "${{ matrix.target }}" \
--archive "rust-release/zeroshot-rust-${RELEASE_TAG}-${{ matrix.target }}.tar.gz"
shell: bash
- name: Upload target archive
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: zeroshot-rust-${{ matrix.target }}
path: rust-release/*.tar.gz
if-no-files-found: error
rust-manifest:
needs: [dry-run, release-plan, rust-recovery-plan, rust-binaries]
if: always() && needs.rust-binaries.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
env:
RELEASE_TAG: ${{ inputs.action == 'dry-run' && needs.dry-run.outputs.tag || inputs.action == 'recover-rust-distribution' && needs.rust-recovery-plan.outputs.tag || needs.release-plan.outputs.tag }}
steps:
- name: Checkout release tooling
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ inputs.action == 'dry-run' && github.sha || inputs.action == 'recover-rust-distribution' && inputs.release_commit || github.event.workflow_run.head_sha }}
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 24
cache: npm
- name: Install pinned script dependencies
run: npm ci --ignore-scripts
- name: Download every target archive
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: zeroshot-rust-*
path: rust-release
merge-multiple: true
- name: Build and verify complete checksum manifest
run: node scripts/rust-distribution.js manifest --version "$RELEASE_TAG" --dir rust-release
shell: bash
- name: Upload complete dry-run distribution
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: zeroshot-rust-${{ env.RELEASE_TAG }}
path: |
rust-release/*.tar.gz
rust-release/SHA256SUMS
if-no-files-found: error
release:
outputs:
created: ${{ steps.semantic-release.outputs.created }}
tag: ${{ steps.semantic-release.outputs.tag }}
needs: [install-matrix, release-plan, rust-manifest]
if: |
always() &&
needs.install-matrix.result == 'success' &&
needs.release-plan.result == 'success' &&
needs.release-plan.outputs.version != '' &&
needs.rust-manifest.result == 'success' &&
github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.conclusion == 'success' &&
vars.RELEASE_AUTOMATION_ENABLED == 'true'
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
steps:
- name: Checkout exact artifact-tested main commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event.workflow_run.head_sha }}
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 24
cache: npm
registry-url: https://registry.npmjs.org
- name: Install pinned dependencies
run: npm ci
- name: Fetch immutable release history
run: git fetch --tags --force
- name: Recheck main immediately before publication and confirm planned version
id: final-plan
shell: bash
env:
EXPECTED_VERSION: ${{ needs.release-plan.outputs.version }}
GITHUB_REF_NAME: main
TESTED_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
current_sha="$(git rev-parse HEAD)"
remote_sha="$(git ls-remote origin refs/heads/main | cut -f1)"
if [[ "$current_sha" != "$TESTED_SHA" || "$remote_sha" != "$TESTED_SHA" ]]; then
echo "::error::main moved after native artifacts were validated: tested=$TESTED_SHA checkout=$current_sha main=$remote_sha"
exit 1
fi
actual_version="$(node scripts/release-dry-run.js | sed -n 's/^RELEASE_DRY_RUN_RESULT=//p' | tail -1)"
if [[ "$actual_version" != "$EXPECTED_VERSION" ]]; then
echo "::error::semantic-release version changed after native artifacts: expected=$EXPECTED_VERSION actual=$actual_version"
exit 1
fi
node scripts/rust-distribution.js stage-version --tag "v$EXPECTED_VERSION"
node scripts/rust-distribution.js check-version --tag "v$EXPECTED_VERSION"
- name: Run semantic-release
id: semantic-release
shell: bash
env:
EXPECTED_TAG: ${{ needs.release-plan.outputs.tag }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tags_before="$(git tag --points-at HEAD --list 'v*')"
npx semantic-release
tags_after="$(git tag --points-at HEAD --list 'v*')"
if [[ "$tags_after" != "$EXPECTED_TAG" || "$tags_after" == "$tags_before" ]]; then
echo "::error::semantic-release did not create expected tag $EXPECTED_TAG (before=$tags_before after=$tags_after)"
exit 1
fi
echo "created=true" >> "$GITHUB_OUTPUT"
echo "tag=$tags_after" >> "$GITHUB_OUTPUT"
- name: Assert release state
env:
GITHUB_TOKEN: ${{ github.token }}
run: npm run release:assert-published
rust-publish:
needs: [release, rust-manifest, rust-recovery-plan]
if: |
always() &&
needs.rust-manifest.result == 'success' &&
(
(
needs.release.result == 'success' &&
needs.release.outputs.created == 'true'
) ||
(
github.event_name == 'workflow_dispatch' &&
inputs.action == 'recover-rust-distribution' &&
needs.rust-recovery-plan.result == 'success'
)
)
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
id-token: write
env:
RELEASE_TAG: ${{ inputs.action == 'recover-rust-distribution' && needs.rust-recovery-plan.outputs.tag || needs.release.outputs.tag }}
steps:
- name: Checkout immutable release tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ env.RELEASE_TAG }}
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 24
cache: npm
registry-url: https://registry.npmjs.org
- name: Install pinned script dependencies
run: npm ci --ignore-scripts
- name: Download complete Rust distribution
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: zeroshot-rust-${{ env.RELEASE_TAG }}
path: rust-release
- name: Verify existing assets and upload only missing names
env:
GH_TOKEN: ${{ github.token }}
run: node scripts/rust-distribution.js publish-assets --tag "$RELEASE_TAG" --dir rust-release
- name: Stage coupled shim package version
working-directory: npm/zeroshot-rust
run: npm version "${RELEASE_TAG#v}" --no-git-tag-version
shell: bash
- name: Idempotently publish standalone Rust shim package
working-directory: npm/zeroshot-rust
shell: bash
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
package="@the-open-engine/zeroshot-rust@$version"
if published="$(npm view "$package" version 2>/dev/null)"; then
[[ "$published" == "$version" ]] || {
echo "::error::$package resolved to unexpected version $published"
exit 1
}
echo "$package is already published; recovery is complete"
else
npm publish --provenance --access public
fi
recover:
if: |
github.event_name == 'workflow_dispatch' &&
(inputs.action == 'recover-npm' || inputs.action == 'recover-github-release')
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
id-token: write
steps:
- name: Validate recovery inputs
shell: bash
env:
RELEASE_COMMIT: ${{ inputs.release_commit }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "::error::release_tag must match vX.Y.Z"
exit 1
}
[[ "$RELEASE_COMMIT" =~ ^[0-9a-f]{40}$ ]] || {
echo "::error::release_commit must be a full lowercase SHA"
exit 1
}
- name: Checkout immutable release commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.release_commit }}
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 24
cache: npm
registry-url: https://registry.npmjs.org
- name: Install pinned dependencies
run: npm ci
- name: Fetch protected refs
run: |
git fetch origin main
git fetch origin "refs/tags/${{ inputs.release_tag }}:refs/tags/${{ inputs.release_tag }}"
- name: Verify recovery package
run: |
npm run release:preflight
npm run lint
npm run typecheck
npm run check:agent-cli-provider:ci
npm pack --dry-run --json
- name: Recover missing artifact
env:
GITHUB_TOKEN: ${{ github.token }}
RECOVERY_ACTION: ${{ inputs.action }}
RELEASE_COMMIT: ${{ inputs.release_commit }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: npm run release:recover