choreci): add weekly nightly-to-main promotion workflow

Adds automated workflow that creates a PR from nightly → main every
Monday at 9:00 AM UTC for scheduled release promotion.

Features:

Pre-flight health check verifies critical workflows are passing
Skips PR creation if nightly has no new commits
Detects existing PRs and adds comments instead of duplicates
Labels PRs with 'automated' and 'weekly-promotion'
Creates GitHub issue on failure for visibility
Manual trigger via workflow_dispatch with reason input
NO auto-merge - requires human review and approval
This gives early-week visibility into nightly changes and prevents
Friday surprises from untested code reaching main.
This commit is contained in:
Jeremy
2026-01-30 14:32:17 +00:00
parent 9f94fdeade
commit b43a5dbae8

View File

@@ -0,0 +1,481 @@
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 09:00 UTC (4am EST / 5am EDT)
- cron: '0 9 * * 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...');
// Get the latest workflow runs on the nightly branch
const { data: runs } = await github.rest.actions.listWorkflowRunsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
branch: 'nightly',
status: 'completed',
per_page: 10,
});
if (runs.workflow_runs.length === 0) {
core.setOutput('is_healthy', 'true');
core.setOutput('latest_run_url', 'No completed runs found');
core.setOutput('failure_reason', '');
core.info('No completed workflow runs found on nightly - proceeding');
return;
}
// Check the most recent critical workflows
const criticalWorkflows = ['Nightly Build & Package', 'Quality Checks', 'E2E Tests'];
const recentRuns = runs.workflow_runs.slice(0, 10);
let hasFailure = false;
let failureReason = '';
let latestRunUrl = recentRuns[0]?.html_url || 'N/A';
for (const workflowName of criticalWorkflows) {
const latestRun = recentRuns.find(r => r.name === workflowName);
if (latestRun && latestRun.conclusion === 'failure') {
hasFailure = true;
failureReason = `${workflowName} failed (${latestRun.html_url})`;
latestRunUrl = latestRun.html_url;
core.warning(`Critical workflow "${workflowName}" has failed`);
break;
}
}
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 }}
pr_url: ${{ steps.create-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:_" >> /tmp/commit_log.md
fi
echo '```' >> /tmp/commit_log.md
echo "$COMMIT_LOG" >> /tmp/commit_log.md
echo '```' >> /tmp/commit_log.md
if [ "$COMMIT_COUNT" -gt 50 ]; then
echo "" >> /tmp/commit_log.md
echo "_...and $((COMMIT_COUNT - 50)) more commits_" >> /tmp/commit_log.md
fi
# 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 }}');
notify-on-failure:
name: Notify on Failure
needs: [check-nightly-health, create-promotion-pr]
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]
runs-on: ubuntu-latest
if: always()
steps:
- name: Generate Summary
run: |
echo "## 📋 Weekly Nightly Promotion Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
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 |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
if [ "$HEALTH" = "true" ]; then
echo "| Nightly Health Check | ✅ Healthy |" >> $GITHUB_STEP_SUMMARY
else
echo "| Nightly Health Check | ❌ Unhealthy: $FAILURE_REASON |" >> $GITHUB_STEP_SUMMARY
fi
if [ "$SKIPPED" = "true" ]; then
echo "| PR Creation | ⏭️ Skipped (no changes) |" >> $GITHUB_STEP_SUMMARY
elif [ -n "$PR_URL" ]; then
echo "| PR Creation | ✅ [PR #$PR_NUMBER]($PR_URL) |" >> $GITHUB_STEP_SUMMARY
else
echo "| PR Creation | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "_Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}_" >> $GITHUB_STEP_SUMMARY