Skip to content

Unified Release

Unified Release #383

Workflow file for this run

name: Unified Release
on:
# Nightly builds at 4 AM UTC
schedule:
- cron: '0 4 * * *'
# Auto-trigger on PR merge with release labels
# - rc: can be triggered from dev or main
# - stable: only from main
pull_request:
types: [closed]
branches: [main, dev]
# Manual trigger for all release actions
workflow_dispatch:
inputs:
action:
description: 'Release action to perform'
required: true
type: choice
options:
- nightly
- bump-rc
- promote
skip_build:
description: 'Skip build (for promote action)'
type: boolean
default: false
# Prevent concurrent releases
concurrency:
group: unified-release
cancel-in-progress: false
permissions: write-all
jobs:
# Gate check: Skip automated commits to prevent infinite loops
gate:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
action: ${{ steps.detect.outputs.action }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Detect release action
id: detect
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "action=nightly" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "action=${{ inputs.action }}" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Only run if PR was merged (not just closed)
if [[ "${{ github.event.pull_request.merged }}" != "true" ]]; then
echo "⏭️ PR closed without merge, skipping"
echo "action=none" >> $GITHUB_OUTPUT
exit 0
fi
# Check PR labels for release type
LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}"
BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
echo "PR labels: $LABELS"
echo "Base branch: $BASE_BRANCH"
if [[ "$LABELS" == *"rc"* ]]; then
# RC can be triggered from dev or main
echo "🚀 RC label detected, triggering bump-rc"
echo "action=bump-rc" >> $GITHUB_OUTPUT
elif [[ "$LABELS" == *"stable"* ]]; then
# Stable can ONLY be triggered from main
if [[ "$BASE_BRANCH" != "main" ]]; then
echo "❌ Stable label only allowed on PRs to main, not $BASE_BRANCH"
echo "action=none" >> $GITHUB_OUTPUT
else
echo "🎉 Stable label detected, triggering promote"
echo "action=promote" >> $GITHUB_OUTPUT
fi
else
echo "⏭️ No release label found, skipping"
echo "action=none" >> $GITHUB_OUTPUT
fi
else
echo "action=none" >> $GITHUB_OUTPUT
fi
- name: Check for automated commit (prevent infinite loop)
id: check
run: |
# Skip if no action detected (PR without release label, or closed without merge)
if [[ "${{ steps.detect.outputs.action }}" == "none" ]]; then
echo "⏭️ No release action detected, skipping"
echo "should_run=false" >> $GITHUB_OUTPUT
exit 0
fi
# Manual workflow_dispatch always runs (override gate check)
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "✅ Manual trigger, bypassing gate check"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
# PR merges with release labels always run
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "✅ PR merge with release label, proceeding"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
COMMIT_MSG=$(git log -1 --pretty=%B)
echo "Last commit: $COMMIT_MSG"
if echo "$COMMIT_MSG" | grep -Eq "^chore: (release|pre-release|bump|promote)"; then
echo "⏭️ Skipping automated commit to prevent loop"
echo "should_run=false" >> $GITHUB_OUTPUT
else
echo "✅ Not an automated commit, proceeding"
echo "should_run=true" >> $GITHUB_OUTPUT
fi
- name: Check for changes (nightly only)
if: steps.detect.outputs.action == 'nightly'
run: |
# Find last nightly tag
LAST_NIGHTLY=$(git tag -l 'v*-nightly.*' --sort=-v:refname | head -1 || echo "")
if [[ -z "$LAST_NIGHTLY" ]]; then
echo "No previous nightly tag found, will build"
exit 0
fi
COMMITS=$(git rev-list $LAST_NIGHTLY..HEAD --count 2>/dev/null || echo "0")
echo "Commits since $LAST_NIGHTLY: $COMMITS"
if [[ "$COMMITS" == "0" ]]; then
echo "⏭️ No changes since last nightly, skipping"
echo "should_run=false" >> $GITHUB_OUTPUT
fi
# Version bump
version:
needs: gate
if: needs.gate.outputs.should_run == 'true'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.bump.outputs.version }}
tag: ${{ steps.bump.outputs.tag }}
npm_tag: ${{ steps.bump.outputs.npm_tag }}
is_promote: ${{ steps.bump.outputs.is_promote }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# Nightly always builds from dev branch
ref: ${{ needs.gate.outputs.action == 'nightly' && 'dev' || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Run unified release script
id: bump
run: |
node scripts/unified-release.cjs --action ${{ needs.gate.outputs.action }}
# Set is_promote output
if [[ "${{ needs.gate.outputs.action }}" == "promote" ]]; then
echo "is_promote=true" >> $GITHUB_OUTPUT
else
echo "is_promote=false" >> $GITHUB_OUTPUT
fi
- name: Push changes and tag
if: needs.gate.outputs.action != 'promote' || inputs.skip_build != true
run: |
git push origin HEAD
git push origin --tags
- name: Sync forge-core version and create tag
if: needs.gate.outputs.action != 'promote'
# Note: Removed continue-on-error to ensure version sync failures stop the release
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
run: |
VERSION="${{ steps.bump.outputs.version }}"
TAG="${{ steps.bump.outputs.tag }}"
echo "Syncing forge-core to version $VERSION and creating tag $TAG..."
# Check if PAT_TOKEN is available
if [ -z "$GH_TOKEN" ]; then
echo "::warning::PAT_TOKEN not available - forge-core sync SKIPPED"
echo "::warning::Manual action required: create tag $TAG in forge-core repository"
exit 0
fi
# Check if tag already exists
if gh api repos/namastexlabs/forge-core/git/refs/tags/$TAG 2>/dev/null; then
echo "Tag $TAG already exists in forge-core, skipping"
exit 0
fi
# Trigger sync-version-from-forge workflow which:
# 1. Updates [workspace.package] version in Cargo.toml using cargo set-version
# 2. Commits the change
# 3. Creates an annotated tag pointing at the versioned commit
echo "Triggering sync-version-from-forge workflow..."
# Capture timestamp before triggering to find our specific run
TRIGGER_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
gh workflow run sync-version-from-forge.yml \
--repo namastexlabs/forge-core \
-f version="$VERSION"
# Wait for workflow to appear in the run list
echo "Waiting for sync workflow to start..."
sleep 10
# Find the specific workflow run we just triggered (created after our trigger time)
RUN_ID=""
for i in {1..12}; do
RUN_ID=$(gh run list --repo namastexlabs/forge-core \
--workflow=sync-version-from-forge.yml \
--created=">$TRIGGER_TIME" \
--limit=1 --json databaseId \
--jq '.[0].databaseId // empty')
if [ -n "$RUN_ID" ]; then
echo "Found workflow run ID: $RUN_ID"
break
fi
echo "Waiting for workflow to appear (attempt $i)..."
sleep 5
done
if [ -z "$RUN_ID" ]; then
echo "❌ Could not find triggered workflow run"
exit 1
fi
# Poll for THIS SPECIFIC workflow run completion (max 3 minutes)
echo "Waiting for workflow run $RUN_ID to complete..."
for i in {1..36}; do
RUN_STATUS=$(gh run view "$RUN_ID" --repo namastexlabs/forge-core \
--json status,conclusion \
--jq '"\(.status) \(.conclusion)"')
echo "Attempt $i: $RUN_STATUS"
if [[ "$RUN_STATUS" == "completed success" ]]; then
echo "✅ forge-core version synced and tag $TAG created"
exit 0
elif [[ "$RUN_STATUS" == "completed failure" ]] || [[ "$RUN_STATUS" == "completed cancelled" ]]; then
echo "❌ Sync workflow failed: $RUN_STATUS"
exit 1
fi
sleep 5
done
echo "⚠️ Timed out waiting for sync workflow run $RUN_ID"
exit 1
# Build (skip for promote action)
build:
needs: version
if: needs.version.outputs.is_promote != 'true'
uses: ./.github/workflows/build-all-platforms.yml
with:
version: ${{ needs.version.outputs.version }}
npm_tag: ${{ needs.version.outputs.npm_tag }}
secrets: inherit
# Promote: Move npm dist-tags without rebuild
promote:
needs: version
if: needs.version.outputs.is_promote == 'true'
runs-on: ubuntu-latest
environment: npm-publish
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm for trusted publishing
run: npm install -g npm@11.6.2
- name: Get RC version from package.json
id: rc
run: |
# Get the version that was just promoted (now in package.json)
STABLE_VERSION=$(node -e "console.log(require('./npx-cli/package.json').version)")
# Find the RC version that matches this stable
RC_VERSION=$(npm view @automagik/forge dist-tags.next 2>/dev/null || echo "")
if [[ -z "$RC_VERSION" ]]; then
echo "❌ No RC version found on @next tag"
exit 1
fi
# Verify RC matches expected stable
RC_BASE=$(echo $RC_VERSION | sed 's/-rc\.[0-9]*$//')
if [[ "$RC_BASE" != "$STABLE_VERSION" ]]; then
echo "❌ RC version $RC_VERSION doesn't match stable $STABLE_VERSION"
exit 1
fi
echo "rc_version=$RC_VERSION" >> $GITHUB_OUTPUT
echo "stable_version=$STABLE_VERSION" >> $GITHUB_OUTPUT
echo "✅ Promoting $RC_VERSION → $STABLE_VERSION"
- name: Promote @automagik/forge
run: |
echo "Moving @automagik/forge@${{ steps.rc.outputs.rc_version }} to @latest"
npm dist-tag add @automagik/forge@${{ steps.rc.outputs.rc_version }} latest
- name: Promote automagik
run: |
echo "Moving automagik@${{ steps.rc.outputs.rc_version }} to @latest"
npm dist-tag add automagik@${{ steps.rc.outputs.rc_version }} latest
- name: Verify promotion
run: |
echo "Verifying npm dist-tags..."
SCOPED_LATEST=$(npm view @automagik/forge dist-tags.latest)
SHORT_LATEST=$(npm view automagik dist-tags.latest)
echo "@automagik/forge@latest = $SCOPED_LATEST"
echo "automagik@latest = $SHORT_LATEST"
if [[ "$SCOPED_LATEST" == "${{ steps.rc.outputs.rc_version }}" ]]; then
echo "✅ @automagik/forge promotion successful"
else
echo "⚠️ @automagik/forge promotion may have failed"
fi
# Create GitHub release
github-release:
needs: [version, build]
if: always() && needs.version.result == 'success' && (needs.build.result == 'success' || needs.build.result == 'skipped')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version.outputs.tag }}
name: ${{ needs.version.outputs.tag }}
generate_release_notes: true
prerelease: ${{ contains(needs.version.outputs.version, '-rc.') || contains(needs.version.outputs.version, '-nightly.') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}