name: Weekly Nightly to Main Promotion # Creates a PR from nightly → main every Monday for scheduled release promotion. # Includes safety checks for workflow status and provides manual trigger option. on: schedule: # Every Monday at 12:00 UTC (7:00am EST / 8:00am EDT) # Offset from nightly sync (09:00 UTC) to avoid schedule race and allow validation completion. - cron: '0 12 * * 1' workflow_dispatch: inputs: reason: description: 'Why are you running this manually?' required: true default: 'Ad-hoc promotion request' skip_workflow_check: description: 'Skip nightly workflow status check?' required: false type: boolean default: false concurrency: group: ${{ github.workflow }} cancel-in-progress: false env: NODE_VERSION: '24.12.0' SOURCE_BRANCH: 'nightly' TARGET_BRANCH: 'main' permissions: contents: read pull-requests: write issues: write actions: read jobs: check-nightly-health: name: Verify Nightly Branch Health runs-on: ubuntu-latest outputs: is_healthy: ${{ steps.check.outputs.is_healthy }} latest_run_url: ${{ steps.check.outputs.latest_run_url }} failure_reason: ${{ steps.check.outputs.failure_reason }} steps: - name: Check Nightly Workflow Status id: check uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const skipCheck = '${{ inputs.skip_workflow_check }}' === 'true'; if (skipCheck) { core.info('Skipping workflow health check as requested'); core.setOutput('is_healthy', 'true'); core.setOutput('latest_run_url', 'N/A - check skipped'); core.setOutput('failure_reason', ''); return; } core.info('Checking nightly branch workflow health...'); // Resolve current nightly HEAD SHA and evaluate workflow health for that exact commit. // This prevents stale failures from older nightly runs from blocking promotion. const { data: nightlyBranch } = await github.rest.repos.getBranch({ owner: context.repo.owner, repo: context.repo.repo, branch: 'nightly', }); const nightlyHeadSha = nightlyBranch.commit.sha; core.info(`Current nightly HEAD: ${nightlyHeadSha}`); // Check critical workflows on the current nightly HEAD only. // Nightly build itself is scheduler-driven and not a reliable per-commit gate. const criticalWorkflows = [ { workflowFile: 'quality-checks.yml', fallbackNames: ['Quality Checks'], }, { workflowFile: 'e2e-tests-split.yml', fallbackNames: ['E2E Tests'], }, { workflowFile: 'codeql.yml', fallbackNames: ['CodeQL - Analyze'], }, ]; // Retry window to avoid race conditions where required checks are not yet materialized. const maxAttempts = 6; const waitMs = 20000; let branchRuns = []; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { const { data: completedRuns } = await github.rest.actions.listWorkflowRunsForRepo({ owner: context.repo.owner, repo: context.repo.repo, branch: 'nightly', status: 'completed', per_page: 100, }); branchRuns = completedRuns.workflow_runs; const allWorkflowsPresentForHead = criticalWorkflows.every((workflow) => { const workflowPath = `.github/workflows/${workflow.workflowFile}`; return branchRuns.some( (r) => r.head_sha === nightlyHeadSha && ( r.path === workflowPath || (typeof r.path === 'string' && r.path.endsWith(`/${workflowPath}`)) || workflow.fallbackNames.includes(r.name) ), ); }); if (allWorkflowsPresentForHead) { core.info(`Required workflow runs found for nightly HEAD on attempt ${attempt}`); break; } if (attempt < maxAttempts) { core.info( `Waiting for required runs to appear for nightly HEAD (attempt ${attempt}/${maxAttempts})`, ); await new Promise((resolve) => setTimeout(resolve, waitMs)); } } if (branchRuns.length === 0) { core.setOutput('is_healthy', 'false'); core.setOutput('latest_run_url', 'No completed runs found'); core.setOutput('failure_reason', 'No completed workflow runs found on nightly'); core.warning('No completed workflow runs found on nightly - blocking promotion'); return; } let hasFailure = false; let failureReason = ''; let latestRunUrl = branchRuns[0]?.html_url || 'N/A'; for (const workflow of criticalWorkflows) { const workflowPath = `.github/workflows/${workflow.workflowFile}`; core.info( `Evaluating required workflow ${workflow.workflowFile} (path match first, names fallback: ${workflow.fallbackNames.join(', ')})`, ); const latestRunForHead = branchRuns.find( (r) => r.head_sha === nightlyHeadSha && ( r.path === workflowPath || (typeof r.path === 'string' && r.path.endsWith(`/${workflowPath}`)) || workflow.fallbackNames.includes(r.name) ), ); if (!latestRunForHead) { hasFailure = true; failureReason = `${workflow.workflowFile} has no completed run for nightly HEAD ${nightlyHeadSha}`; latestRunUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/workflows/${workflow.workflowFile}`; core.warning( `Required workflow ${workflow.workflowFile} has no completed run for current nightly HEAD`, ); break; } if (latestRunForHead.conclusion !== 'success') { hasFailure = true; failureReason = `${workflow.workflowFile} ${latestRunForHead.conclusion} (${latestRunForHead.html_url})`; latestRunUrl = latestRunForHead.html_url; core.warning( `Required workflow ${workflow.workflowFile} is ${latestRunForHead.conclusion} on nightly HEAD`, ); break; } core.info( `Required workflow ${workflow.workflowFile} passed for nightly HEAD via run ${latestRunForHead.id}`, ); } core.setOutput('is_healthy', hasFailure ? 'false' : 'true'); core.setOutput('latest_run_url', latestRunUrl); core.setOutput('failure_reason', failureReason); if (hasFailure) { core.warning(`Nightly branch has failing workflows: ${failureReason}`); } else { core.info('Nightly branch is healthy - all critical workflows passing'); } create-promotion-pr: name: Create Promotion PR needs: check-nightly-health runs-on: ubuntu-latest if: needs.check-nightly-health.outputs.is_healthy == 'true' outputs: pr_number: ${{ steps.create-pr.outputs.pr_number || steps.existing-pr.outputs.pr_number }} pr_url: ${{ steps.create-pr.outputs.pr_url || steps.existing-pr.outputs.pr_url }} skipped: ${{ steps.check-diff.outputs.skipped }} steps: - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.TARGET_BRANCH }} fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Check for Differences id: check-diff run: | git fetch origin "${{ env.SOURCE_BRANCH }}" # Compare the branches AHEAD_COUNT=$(git rev-list --count "origin/${{ env.TARGET_BRANCH }}..origin/${{ env.SOURCE_BRANCH }}") BEHIND_COUNT=$(git rev-list --count "origin/${{ env.SOURCE_BRANCH }}..origin/${{ env.TARGET_BRANCH }}") echo "Nightly is $AHEAD_COUNT commits ahead of main" echo "Nightly is $BEHIND_COUNT commits behind main" if [ "$AHEAD_COUNT" -eq 0 ]; then echo "No changes to promote - nightly is up-to-date with main" echo "skipped=true" >> "$GITHUB_OUTPUT" echo "skip_reason=No changes to promote" >> "$GITHUB_OUTPUT" else echo "skipped=false" >> "$GITHUB_OUTPUT" echo "ahead_count=$AHEAD_COUNT" >> "$GITHUB_OUTPUT" fi - name: Generate Commit Summary id: commits if: steps.check-diff.outputs.skipped != 'true' run: | # Get the date for the PR title DATE=$(date -u +%Y-%m-%d) echo "date=$DATE" >> "$GITHUB_OUTPUT" # Generate commit log COMMIT_LOG=$(git log --oneline "origin/${{ env.TARGET_BRANCH }}..origin/${{ env.SOURCE_BRANCH }}" | head -50) COMMIT_COUNT=$(git rev-list --count "origin/${{ env.TARGET_BRANCH }}..origin/${{ env.SOURCE_BRANCH }}") # Store commit log in a file to preserve formatting cat > /tmp/commit_log.md << 'COMMITS_EOF' ## Commits Being Promoted COMMITS_EOF { if [ "$COMMIT_COUNT" -gt 50 ]; then echo "_Showing first 50 of $COMMIT_COUNT commits:_" fi echo '```' echo "$COMMIT_LOG" echo '```' if [ "$COMMIT_COUNT" -gt 50 ]; then echo "" echo "_...and $((COMMIT_COUNT - 50)) more commits_" fi } >> /tmp/commit_log.md # Get files changed summary FILES_CHANGED=$(git diff --stat "origin/${{ env.TARGET_BRANCH }}..origin/${{ env.SOURCE_BRANCH }}" | tail -1) echo "files_changed=$FILES_CHANGED" >> "$GITHUB_OUTPUT" echo "commit_count=$COMMIT_COUNT" >> "$GITHUB_OUTPUT" - name: Check for Existing PR id: existing-pr if: steps.check-diff.outputs.skipped != 'true' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { data: pulls } = await github.rest.pulls.list({ owner: context.repo.owner, repo: context.repo.repo, state: 'open', head: `${context.repo.owner}:${{ env.SOURCE_BRANCH }}`, base: '${{ env.TARGET_BRANCH }}', }); if (pulls.length > 0) { core.info(`Existing PR found: #${pulls[0].number}`); core.setOutput('exists', 'true'); core.setOutput('pr_number', pulls[0].number); core.setOutput('pr_url', pulls[0].html_url); } else { core.setOutput('exists', 'false'); } - name: Create Promotion PR id: create-pr if: steps.check-diff.outputs.skipped != 'true' && steps.existing-pr.outputs.exists != 'true' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const fs = require('fs'); const date = '${{ steps.commits.outputs.date }}'; const commitCount = '${{ steps.commits.outputs.commit_count }}'; const filesChanged = '${{ steps.commits.outputs.files_changed }}'; const commitLog = fs.readFileSync('/tmp/commit_log.md', 'utf8'); const triggerReason = '${{ inputs.reason }}' || 'Scheduled weekly promotion'; const body = `## 🚀 Weekly Nightly to Main Promotion **Date:** ${date} **Trigger:** ${triggerReason} **Commits:** ${commitCount} commits to promote **Changes:** ${filesChanged} --- ${commitLog} --- ## Pre-Merge Checklist - [ ] All status checks pass - [ ] No critical security issues identified - [ ] Changelog is up-to-date (auto-generated via workflow) - [ ] Version bump is appropriate (if applicable) ## Merge Instructions This PR promotes changes from \`nightly\` to \`main\`. Once all checks pass: 1. **Review** the commit summary above 2. **Approve** if changes look correct 3. **Merge** using "Merge commit" to preserve history --- _This PR was automatically created by the [Weekly Nightly Promotion](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow._ `; try { const pr = await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, title: `Weekly: Promote nightly to main (${date})`, head: '${{ env.SOURCE_BRANCH }}', base: '${{ env.TARGET_BRANCH }}', body: body, draft: false, }); core.info(`Created PR #${pr.data.number}: ${pr.data.html_url}`); core.setOutput('pr_number', pr.data.number); core.setOutput('pr_url', pr.data.html_url); // Add labels (create if they don't exist) const labels = ['automated', 'weekly-promotion']; for (const label of labels) { try { await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, }); } catch (e) { // Label doesn't exist, create it const colors = { 'automated': '0e8a16', 'weekly-promotion': '5319e7', }; await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: colors[label] || 'ededed', description: label === 'automated' ? 'Automatically generated by CI/CD' : 'Weekly promotion from nightly to main', }); } } await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.data.number, labels: labels, }); core.info('Labels added successfully'); } catch (error) { core.setFailed(`Failed to create PR: ${error.message}`); } - name: Update Existing PR if: steps.check-diff.outputs.skipped != 'true' && steps.existing-pr.outputs.exists == 'true' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const prNumber = ${{ steps.existing-pr.outputs.pr_number }}; core.info(`PR #${prNumber} already exists - adding comment with update`); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: `🔄 **Weekly check:** This PR is still open. New commits may have been added to \`nightly\` since the original PR was created.\n\n_Triggered by [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})_`, }); core.setOutput('pr_number', prNumber); core.setOutput('pr_url', '${{ steps.existing-pr.outputs.pr_url }}'); trigger-required-checks: name: Trigger Missing Required Checks needs: create-promotion-pr if: needs.create-promotion-pr.outputs.skipped != 'true' runs-on: ubuntu-latest permissions: actions: write contents: read steps: - name: Dispatch missing required workflows on nightly head uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const owner = context.repo.owner; const repo = context.repo.repo; const { data: nightlyBranch } = await github.rest.repos.getBranch({ owner, repo, branch: 'nightly', }); const nightlyHeadSha = nightlyBranch.commit.sha; core.info(`Current nightly HEAD for dispatch fallback: ${nightlyHeadSha}`); const requiredWorkflows = [ { id: 'e2e-tests-split.yml' }, { id: 'codeql.yml' }, { id: 'codecov-upload.yml', inputs: { run_backend: 'true', run_frontend: 'true' } }, { id: 'security-pr.yml' }, { id: 'supply-chain-verify.yml' }, ]; for (const workflow of requiredWorkflows) { const { data: runs } = await github.rest.actions.listWorkflowRuns({ owner, repo, workflow_id: workflow.id, branch: 'nightly', per_page: 50, }); const hasRunForHead = runs.workflow_runs.some((run) => run.head_sha === nightlyHeadSha); if (hasRunForHead) { core.info(`Skipping ${workflow.id}; run already exists for nightly HEAD`); continue; } await github.rest.actions.createWorkflowDispatch({ owner, repo, workflow_id: workflow.id, ref: 'nightly', ...(workflow.inputs ? { inputs: workflow.inputs } : {}), }); core.info(`Dispatched ${workflow.id}; missing for nightly HEAD`); } notify-on-failure: name: Notify on Failure needs: [check-nightly-health, create-promotion-pr, trigger-required-checks] runs-on: ubuntu-latest if: | always() && (needs.check-nightly-health.outputs.is_healthy == 'false' || needs.create-promotion-pr.result == 'failure') steps: - name: Create Failure Issue uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const isHealthy = '${{ needs.check-nightly-health.outputs.is_healthy }}'; const failureReason = '${{ needs.check-nightly-health.outputs.failure_reason }}'; const latestRunUrl = '${{ needs.check-nightly-health.outputs.latest_run_url }}'; const prResult = '${{ needs.create-promotion-pr.result }}'; let title, body; if (isHealthy === 'false') { title = '🚨 Weekly Promotion Blocked: Nightly Branch Unhealthy'; body = `## Weekly Promotion Failed The weekly promotion from \`nightly\` to \`main\` was **blocked** because the nightly branch has failing workflows. ### Failure Details - **Reason:** ${failureReason} - **Latest Run:** ${latestRunUrl} - **Workflow Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} ### Required Actions 1. Investigate the failing workflow on the nightly branch 2. Fix the underlying issue 3. Re-run the failed workflow 4. Manually trigger the weekly promotion workflow once nightly is healthy --- _This issue was automatically created by the Weekly Nightly Promotion workflow._ `; } else { title = '🚨 Weekly Promotion Failed: PR Creation Error'; body = `## Weekly Promotion Failed The weekly promotion workflow encountered an error while trying to create the PR. ### Details - **PR Creation Result:** ${prResult} - **Workflow Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} ### Required Actions 1. Check the workflow logs for detailed error information 2. Manually create the promotion PR if needed 3. Investigate and fix any configuration issues --- _This issue was automatically created by the Weekly Nightly Promotion workflow._ `; } // Check for existing open issues with same title const { data: issues } = await github.rest.issues.listForRepo({ owner: context.repo.owner, repo: context.repo.repo, state: 'open', labels: 'weekly-promotion-failure', }); const existingIssue = issues.find(i => i.title === title); if (existingIssue) { // Add comment to existing issue await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: existingIssue.number, body: `🔄 **Update:** This issue occurred again.\n\n${body}`, }); core.info(`Updated existing issue #${existingIssue.number}`); } else { // Create label if it doesn't exist try { await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: 'weekly-promotion-failure', }); } catch (e) { await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: 'weekly-promotion-failure', color: 'd73a4a', description: 'Weekly promotion workflow failure', }); } // Create new issue const issue = await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, title: title, body: body, labels: ['weekly-promotion-failure', 'automated'], }); core.info(`Created issue #${issue.data.number}`); } summary: name: Workflow Summary needs: [check-nightly-health, create-promotion-pr, trigger-required-checks] runs-on: ubuntu-latest if: always() steps: - name: Generate Summary run: | { echo "## 📋 Weekly Nightly Promotion Summary" echo "" HEALTH="${{ needs.check-nightly-health.outputs.is_healthy }}" SKIPPED="${{ needs.create-promotion-pr.outputs.skipped }}" PR_URL="${{ needs.create-promotion-pr.outputs.pr_url }}" PR_NUMBER="${{ needs.create-promotion-pr.outputs.pr_number }}" FAILURE_REASON="${{ needs.check-nightly-health.outputs.failure_reason }}" echo "| Step | Status |" echo "|------|--------|" if [ "$HEALTH" = "true" ]; then echo "| Nightly Health Check | ✅ Healthy |" else echo "| Nightly Health Check | ❌ Unhealthy: $FAILURE_REASON |" fi if [ "$SKIPPED" = "true" ]; then echo "| PR Creation | ⏭️ Skipped (no changes) |" elif [ -n "$PR_URL" ]; then echo "| PR Creation | ✅ [PR #$PR_NUMBER]($PR_URL) |" else echo "| PR Creation | ❌ Failed |" fi echo "" echo "---" echo "_Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}_" } >> "$GITHUB_STEP_SUMMARY"