Skip to content

Publish

Publish #124

Workflow file for this run

name: Publish
on:
workflow_dispatch:
inputs:
channel:
description: "Release channel, or publish an existing tag/ref"
required: true
type: choice
options:
- next
- finalize
- stable
- existing
default: next
bump:
description: "Version bump (next and stable only)"
required: false
type: choice
options:
- patch
- minor
- major
default: patch
prerelease_tag:
description: "Prerelease tag to finalize (defaults to latest v*-next.* tag)"
required: false
type: string
ref:
description: "Existing release tag/ref to publish (required when channel=existing)"
required: false
type: string
concurrency:
group: ${{ github.workflow }}-${{ inputs.channel }}-${{ inputs.ref || inputs.prerelease_tag || inputs.bump || github.ref }}
cancel-in-progress: false
permissions: {}
jobs:
prepare:
name: Prepare release
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
publish_action: ${{ steps.context.outputs.publish_action }}
release_tag: ${{ steps.context.outputs.release_tag }}
version: ${{ steps.context.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
ref: ${{ inputs.channel == 'existing' && inputs.ref || github.ref }}
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if [ "${{ inputs.channel }}" != "existing" ]; then
git checkout main
fi
- name: Validate existing ref input
if: inputs.channel == 'existing' && inputs.ref == ''
run: |
echo "::error::ref is required when channel=existing"
exit 1
- name: Resolve finalize ref
if: inputs.channel == 'finalize'
id: finalize_ref
run: |
if [ -n "${{ inputs.prerelease_tag }}" ]; then
TAG="${{ inputs.prerelease_tag }}"
else
TAG=$(git tag --list 'v*-next.*' --sort=-version:refname | head -n 1)
fi
case "$TAG" in
v*) ;;
*) TAG="v$TAG" ;;
esac
if [ -z "$TAG" ]; then
echo "No prerelease tag found to finalize" >&2
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
git checkout --detach "$TAG"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Install dependencies
run: bun install --frozen-lockfile
# Finalize/stable releases promote to latest, so block before any version
# bump/tag if the live contract eval fails. The gate uses a Models-capable
# PAT secret and does not override the workflow GITHUB_TOKEN.
- name: Contract eval gate
if: inputs.channel == 'finalize' || inputs.channel == 'stable'
run: bun run contract-eval
env:
GH_MODELS_TOKEN: ${{ secrets.GH_MODELS_TOKEN }}
CONTRACT_EVAL_MODEL: ${{ vars.CONTRACT_EVAL_MODEL || 'openai/gpt-4.1-mini' }}
- name: Bump version, commit, and tag
if: inputs.channel != 'existing'
run: |
if [ "${{ inputs.channel }}" = "next" ]; then
bun run release next ${{ inputs.bump }}
elif [ "${{ inputs.channel }}" = "finalize" ]; then
bun run release finalize "${{ steps.finalize_ref.outputs.tag }}"
else
bun run release ${{ inputs.bump }}
fi
- name: Resolve publish context
id: context
run: |
set -euo pipefail
VERSION=$(node -p "require('./apps/cli/package.json').version")
RELEASE_TAG="v${VERSION}"
CHECKOUT_TAG=$(git tag --points-at HEAD | grep -Fx "$RELEASE_TAG" | head -n 1 || true)
if [ -z "$CHECKOUT_TAG" ]; then
echo "::error::Publish checkout must point at $RELEASE_TAG; tags at HEAD are:"
git tag --points-at HEAD || true
exit 1
fi
if [[ "$VERSION" == *"-next."* ]]; then
PUBLISH_ACTION="publish-next"
else
PUBLISH_ACTION="publish-latest"
fi
echo "Publishing $RELEASE_TAG with $PUBLISH_ACTION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
echo "publish_action=$PUBLISH_ACTION" >> "$GITHUB_OUTPUT"
- name: Ensure GitHub Release
run: |
VERSION="${{ steps.context.outputs.version }}"
RELEASE_TAG="${{ steps.context.outputs.release_tag }}"
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
echo "GitHub Release $RELEASE_TAG already exists"
exit 0
fi
if [[ "$VERSION" == *"-next."* ]]; then
gh release create "$RELEASE_TAG" --generate-notes --prerelease
else
gh release create "$RELEASE_TAG" --generate-notes
fi
env:
GH_TOKEN: ${{ github.token }}
publish:
name: Publish npm packages
needs: prepare
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
ref: ${{ needs.prepare.outputs.release_tag }}
- uses: actions/setup-node@v6
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm
run: npm install -g npm@latest
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Validate publish checkout
run: |
set -euo pipefail
VERSION=$(node -p "require('./apps/cli/package.json').version")
RELEASE_TAG="v${VERSION}"
CHECKOUT_TAG=$(git tag --points-at HEAD | grep -Fx "$RELEASE_TAG" | head -n 1 || true)
if [ -z "$CHECKOUT_TAG" ]; then
echo "::error::Publish checkout must point at $RELEASE_TAG; tags at HEAD are:"
git tag --points-at HEAD || true
exit 1
fi
if [[ "$VERSION" == *"-next."* ]]; then
if [ "${{ needs.prepare.outputs.publish_action }}" != "publish-next" ]; then
echo "::error::Cannot publish prerelease $VERSION with ${{ needs.prepare.outputs.publish_action }}"
exit 1
fi
else
if [ "${{ needs.prepare.outputs.publish_action }}" != "publish-latest" ]; then
echo "::error::Cannot publish stable version $VERSION with ${{ needs.prepare.outputs.publish_action }}"
exit 1
fi
fi
echo "Publishing $RELEASE_TAG with ${{ needs.prepare.outputs.publish_action }}"
# Existing latest publishes still run the live contract gate because they
# promote an already-created tag to the default npm install path.
- name: Run contract eval gate
if: needs.prepare.outputs.publish_action == 'publish-latest' && inputs.channel == 'existing'
run: bun run contract-eval
env:
GH_MODELS_TOKEN: ${{ secrets.GH_MODELS_TOKEN }}
CONTRACT_EVAL_MODEL: ${{ vars.CONTRACT_EVAL_MODEL || 'openai/gpt-4.1-mini' }}
- name: Publish to npm
run: |
if [ "${{ needs.prepare.outputs.publish_action }}" = "publish-next" ]; then
bun run publish:next
else
bun run publish
fi
env:
NPM_CONFIG_PROVENANCE: true
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}