mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 14:48:06 -05:00
381 lines
14 KiB
YAML
381 lines
14 KiB
YAML
name: Release Calendar
|
|
|
|
# Creates release PRs on a schedule or manually via workflow_dispatch.
|
|
#
|
|
# HOW IT WORKS:
|
|
# 1. Dev → Staging: Creates snapshot branch (release/staging-YYYY-MM-DD) to prevent PR drift
|
|
# 2. Staging → Main: Opens PR directly from staging (no feature branch needed)
|
|
# 3. New commits to dev won't auto-update staging PRs - you control what's released
|
|
#
|
|
# SCHEDULE:
|
|
# • Friday 17:00 UTC (10am PT): Creates release/staging-* branch from dev → staging PR
|
|
# • Sunday 17:00 UTC (10am PT): Creates staging → main PR (direct from staging)
|
|
#
|
|
# MANUAL TRIGGER:
|
|
# Run via workflow_dispatch from main branch:
|
|
# - staging: Creates dev snapshot → staging PR
|
|
# - production: Creates staging → main PR (direct from staging)
|
|
#
|
|
# REQUIREMENTS:
|
|
# • Scheduled cron only runs when this file exists on the default branch (main)
|
|
# • Manual triggers work from any branch, but should run from main for consistency
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
job_to_run:
|
|
description: "Which job to run (staging: dev→staging, production: staging→main)"
|
|
required: false
|
|
type: choice
|
|
options:
|
|
- staging
|
|
- production
|
|
default: staging
|
|
schedule:
|
|
# Friday 17:00 UTC (see timezone conversions above) to prepare the weekend staging PR.
|
|
- cron: "0 17 * * 5"
|
|
# Sunday 17:00 UTC (same times as above) to prepare the production release PR.
|
|
- cron: "0 17 * * 0"
|
|
|
|
jobs:
|
|
release_to_staging:
|
|
name: Create dev to staging release PR
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
issues: write
|
|
steps:
|
|
- name: Guard Friday schedule
|
|
id: guard_schedule
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Allow workflow_dispatch based on input
|
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
|
JOB_TO_RUN="${{ inputs.job_to_run }}"
|
|
if [ "$JOB_TO_RUN" == "staging" ]; then
|
|
echo "Manual trigger: running staging job (job_to_run=${JOB_TO_RUN})"
|
|
echo "continue=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "Manual trigger: skipping staging job (job_to_run=${JOB_TO_RUN})"
|
|
echo "continue=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# For schedule events, check day of week
|
|
DOW=$(date -u +%u)
|
|
if [ "$DOW" != "5" ]; then
|
|
echo "Not Friday in UTC (current day-of-week: $DOW). Exiting job early."
|
|
echo "continue=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
echo "continue=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Check out repository
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' }}
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Check for existing dev to staging PR
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' }}
|
|
id: check_dev_staging
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
PR_DATE=$(date +%Y-%m-%d)
|
|
BRANCH_NAME="release/staging-${PR_DATE}"
|
|
echo "date=${PR_DATE}" >> "$GITHUB_OUTPUT"
|
|
echo "branch_name=${BRANCH_NAME}" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "Checking for existing pull requests from ${BRANCH_NAME} to staging..."
|
|
EXISTING_PR=$(gh pr list --base staging --head "${BRANCH_NAME}" --state open --limit 1 --json number --jq '.[0].number // ""')
|
|
echo "existing_pr=${EXISTING_PR}" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ -n "$EXISTING_PR" ]; then
|
|
echo "Found existing release PR: #${EXISTING_PR}. Skipping creation."
|
|
else
|
|
echo "No existing release PR found. Proceeding to create a new one."
|
|
fi
|
|
|
|
- name: Log existing PR
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr != '' }}
|
|
run: |
|
|
echo "Release PR already exists: #${{ steps.check_dev_staging.outputs.existing_pr }}"
|
|
|
|
- name: Ensure release labels exist
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr == '' }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
for LABEL in release automated staging; do
|
|
echo "Ensuring label exists: ${LABEL}"
|
|
gh label create "${LABEL}" --color BFD4F2 --force || true
|
|
done
|
|
|
|
- name: Create release branch from dev
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr == '' }}
|
|
env:
|
|
BRANCH_NAME: ${{ steps.check_dev_staging.outputs.branch_name }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
echo "Creating release branch ${BRANCH_NAME} from dev"
|
|
git fetch origin dev
|
|
|
|
# Check if branch already exists locally
|
|
if git show-ref --verify --quiet refs/heads/"${BRANCH_NAME}"; then
|
|
echo "Branch ${BRANCH_NAME} already exists locally, checking out..."
|
|
git checkout "${BRANCH_NAME}"
|
|
else
|
|
git checkout -b "${BRANCH_NAME}" origin/dev
|
|
fi
|
|
|
|
# Check if branch already exists on remote
|
|
if git ls-remote --heads origin "${BRANCH_NAME}" | grep -q "${BRANCH_NAME}"; then
|
|
echo "Branch ${BRANCH_NAME} already exists on remote. Skipping push."
|
|
else
|
|
echo "Pushing branch ${BRANCH_NAME} to remote..."
|
|
if ! git push origin "${BRANCH_NAME}"; then
|
|
echo "❌ ERROR: Failed to push branch ${BRANCH_NAME} to remote"
|
|
exit 1
|
|
fi
|
|
echo "✓ Successfully pushed branch ${BRANCH_NAME}"
|
|
fi
|
|
|
|
- name: Create dev to staging release PR
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr == '' }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR_DATE: ${{ steps.check_dev_staging.outputs.date }}
|
|
BRANCH_NAME: ${{ steps.check_dev_staging.outputs.branch_name }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
python <<'PY'
|
|
import os
|
|
import pathlib
|
|
import textwrap
|
|
from datetime import datetime
|
|
|
|
pr_date = os.environ["PR_DATE"]
|
|
branch_name = os.environ["BRANCH_NAME"]
|
|
formatted_date = datetime.strptime(pr_date, "%Y-%m-%d").strftime("%B %d, %Y")
|
|
|
|
pathlib.Path("pr_body.md").write_text(textwrap.dedent(f"""\
|
|
## 🚀 Weekly Release to Staging
|
|
|
|
**Release Date:** {formatted_date}
|
|
**Release Branch:** `{branch_name}`
|
|
|
|
This automated PR promotes a snapshot of `dev` to `staging` for testing.
|
|
|
|
### What's Included
|
|
All commits merged to `dev` up to the branch creation time.
|
|
|
|
**Note:** This PR uses a dedicated release branch, so new commits to `dev` will NOT automatically appear here.
|
|
|
|
### Review Checklist
|
|
- [ ] All CI checks pass
|
|
- [ ] Code review completed
|
|
- [ ] QA team notified
|
|
- [ ] Ready to merge to staging environment
|
|
|
|
### Next Steps
|
|
After merging, the staging environment will be updated. A production release PR will be created on Sunday.
|
|
|
|
---
|
|
*This PR was automatically created by the Release Calendar workflow on {formatted_date}*
|
|
"""))
|
|
PY
|
|
|
|
TITLE="Release to Staging - ${PR_DATE}"
|
|
echo "Creating PR with title: ${TITLE} from branch ${BRANCH_NAME}"
|
|
|
|
if ! gh pr create \
|
|
--base staging \
|
|
--head "${BRANCH_NAME}" \
|
|
--title "${TITLE}" \
|
|
--label release \
|
|
--label automated \
|
|
--label staging \
|
|
--body-file pr_body.md; then
|
|
echo "❌ ERROR: Failed to create PR"
|
|
exit 1
|
|
fi
|
|
|
|
echo "✅ PR created successfully"
|
|
|
|
release_to_production:
|
|
name: Create staging to main release PR
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
issues: write
|
|
steps:
|
|
- name: Guard Sunday schedule
|
|
id: guard_schedule
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Allow workflow_dispatch based on input
|
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
|
JOB_TO_RUN="${{ inputs.job_to_run }}"
|
|
if [ "$JOB_TO_RUN" == "production" ]; then
|
|
echo "Manual trigger: running production job (job_to_run=${JOB_TO_RUN})"
|
|
echo "continue=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "Manual trigger: skipping production job (job_to_run=${JOB_TO_RUN})"
|
|
echo "continue=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# For schedule events, check day of week
|
|
DOW=$(date -u +%u)
|
|
if [ "$DOW" != "7" ]; then
|
|
echo "Not Sunday in UTC (current day-of-week: $DOW). Exiting job early."
|
|
echo "continue=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
echo "continue=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Check out repository
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' }}
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Determine release readiness
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' }}
|
|
id: production_status
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
PR_DATE=$(date +%Y-%m-%d)
|
|
echo "date=${PR_DATE}" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "Fetching latest branches..."
|
|
git fetch origin main staging
|
|
|
|
COMMITS_AHEAD=$(git rev-list --count origin/main..origin/staging)
|
|
echo "commits=${COMMITS_AHEAD}" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ "$COMMITS_AHEAD" -eq 0 ]; then
|
|
echo "staging_not_ahead=true" >> "$GITHUB_OUTPUT"
|
|
echo "Staging is up to date with main. No release PR needed."
|
|
exit 0
|
|
fi
|
|
|
|
echo "staging_not_ahead=false" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "Checking for existing pull requests from staging to main..."
|
|
EXISTING_PR=$(gh pr list --base main --head staging --state open --limit 1 --json number --jq '.[0].number // ""')
|
|
echo "existing_pr=${EXISTING_PR}" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ -n "$EXISTING_PR" ]; then
|
|
echo "Found existing production release PR: #${EXISTING_PR}. Skipping creation."
|
|
else
|
|
echo "No existing production release PR found. Ready to create a new one."
|
|
fi
|
|
|
|
- name: Log staging up to date
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.production_status.outputs.staging_not_ahead == 'true' }}
|
|
run: |
|
|
echo "Staging branch is up to date with main. Skipping production release PR creation."
|
|
|
|
- name: Log existing PR
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.production_status.outputs.existing_pr != '' }}
|
|
run: |
|
|
echo "Production release PR already exists: #${{ steps.production_status.outputs.existing_pr }}"
|
|
|
|
- name: Ensure release labels exist
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.production_status.outputs.staging_not_ahead != 'true' && steps.production_status.outputs.existing_pr == '' }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
for LABEL in release automated production; do
|
|
echo "Ensuring label exists: ${LABEL}"
|
|
gh label create "${LABEL}" --color BFD4F2 --force || true
|
|
done
|
|
|
|
- name: Create staging to main release PR
|
|
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.production_status.outputs.staging_not_ahead != 'true' && steps.production_status.outputs.existing_pr == '' }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR_DATE: ${{ steps.production_status.outputs.date }}
|
|
COMMITS_AHEAD: ${{ steps.production_status.outputs.commits }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
python <<'PY'
|
|
import os
|
|
import pathlib
|
|
import textwrap
|
|
from datetime import datetime
|
|
|
|
commits_ahead = os.environ["COMMITS_AHEAD"]
|
|
pr_date = os.environ["PR_DATE"]
|
|
formatted_date = datetime.strptime(pr_date, "%Y-%m-%d").strftime("%B %d, %Y")
|
|
|
|
pathlib.Path("pr_body.md").write_text(textwrap.dedent(f"""\
|
|
## 🎯 Production Release
|
|
|
|
**Release Date:** {formatted_date}
|
|
**Commits ahead**: {commits_ahead}
|
|
|
|
This automated PR promotes tested changes from `staging` to `main` for production deployment.
|
|
|
|
### What's Included
|
|
All changes that have been verified in the staging environment.
|
|
|
|
**Note:** This PR is directly from `staging`, so new commits merged to `staging` will automatically appear here.
|
|
|
|
### Pre-Deployment Checklist
|
|
- [ ] All staging tests passed
|
|
- [ ] QA sign-off received
|
|
- [ ] Stakeholder approval obtained
|
|
- [ ] Deployment plan reviewed
|
|
- [ ] Rollback plan confirmed
|
|
|
|
### Deployment Notes
|
|
Merging this PR will trigger production deployment.
|
|
|
|
---
|
|
*This PR was automatically created by the Release Calendar workflow on {formatted_date}*
|
|
"""))
|
|
PY
|
|
|
|
TITLE="Release to Production - ${PR_DATE}"
|
|
echo "Creating PR with title: ${TITLE} from staging with ${COMMITS_AHEAD} commits ahead."
|
|
|
|
gh pr create \
|
|
--base main \
|
|
--head staging \
|
|
--title "${TITLE}" \
|
|
--label release \
|
|
--label automated \
|
|
--label production \
|
|
--body-file pr_body.md
|