Unified Release #383
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |