Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/pr-source-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: pr-source-check

on:
pull_request:
branches: [main]

jobs:
pr-source-check:
runs-on: ubuntu-latest
steps:
- name: Enforce source = test for PRs into main
run: |
if [ "${{ github.event.pull_request.head.ref }}" != "test" ]; then
echo "::error::PRs into main must originate from 'test'. Head is '${{ github.event.pull_request.head.ref }}'."
echo "::error::Open your PR against 'test' instead. main only advances via the Release workflow."
exit 1
fi
echo "PR source is 'test' — allowed."
36 changes: 36 additions & 0 deletions .github/workflows/release-poller.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Release poller

on:
schedule:
- cron: '*/15 * * * *'
workflow_dispatch:

permissions:
actions: write
contents: read

jobs:
check:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.RELEASE_SCHEDULER_TOKEN || secrets.GITHUB_TOKEN }}
steps:
- name: Check pending release
run: |
target=$(gh variable get PENDING_RELEASE_AT --repo "${{ github.repository }}" 2>/dev/null || echo "")
if [ -z "$target" ]; then
echo "No release scheduled."
exit 0
fi

now_epoch=$(date -u +%s)
target_epoch=$(date -d "$target" -u +%s)
if [ "$now_epoch" -lt "$target_epoch" ]; then
mins_left=$(( (target_epoch - now_epoch) / 60 ))
echo "Release scheduled for $target — $mins_left min remaining."
exit 0
fi

echo "::notice::Scheduled time reached ($target). Dispatching release."
gh workflow run release.yml --repo "${{ github.repository }}" --ref test
gh variable delete PENDING_RELEASE_AT --repo "${{ github.repository }}"
79 changes: 79 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Release (promote test → main)

on:
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run — show what would happen, do not push'
type: boolean
default: false
workflow_call:
inputs:
dry_run:
type: boolean
default: false

permissions:
contents: write

concurrency:
group: release-${{ github.repository }}
cancel-in-progress: false

jobs:
promote:
runs-on: ubuntu-latest
steps:
- name: Checkout (full history)
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: test

- name: Verify main is ancestor of test
run: |
git fetch origin main:refs/remotes/origin/main
if ! git merge-base --is-ancestor origin/main origin/test; then
echo "::error::main is not an ancestor of test. Reconcile manually before promoting."
exit 1
fi
ahead=$(git rev-list --count origin/main..origin/test)
echo "main is behind test by $ahead commit(s). Ready to fast-forward."
echo "AHEAD=$ahead" >> $GITHUB_ENV

- name: Tag release
run: |
TAG="release-$(date -u +%Y%m%d-%H%M%S)"
git tag "$TAG" origin/test
echo "TAG=$TAG" >> $GITHUB_ENV

- name: Fast-forward main → test
if: ${{ !inputs.dry_run }}
run: |
git push origin "refs/remotes/origin/test:refs/heads/main"
git push origin "$TAG"
echo "::notice::Promoted $AHEAD commits to main. Tagged $TAG."

- name: Notify Discord
if: ${{ !inputs.dry_run && env.DISCORD_WEBHOOK != '' }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
run: |
repo="${{ github.repository }}"
sha=$(git rev-parse origin/test)
short_sha="${sha:0:7}"
compare_url="https://github.com/${repo}/compare/${TAG}...main"
tag_url="https://github.com/${repo}/releases/tag/${TAG}"
payload=$(jq -n \
--arg content "🚀 **${repo}** released \`${TAG}\` — promoted **${AHEAD}** commit(s) to \`main\` (\`${short_sha}\`)." \
--arg tag "$tag_url" \
--arg cmp "$compare_url" \
'{content: $content, embeds: [{title: "View tag", url: $tag}, {title: "Diff", url: $cmp}]}')
curl -sS -H "Content-Type: application/json" -X POST -d "$payload" "$DISCORD_WEBHOOK"

- name: Dry run summary
if: ${{ inputs.dry_run }}
run: |
echo "DRY RUN — would fast-forward main to $(git rev-parse origin/test)"
echo "Tag would be: $TAG"
git log --oneline origin/main..origin/test | head -50
80 changes: 80 additions & 0 deletions .github/workflows/schedule-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Schedule a release

on:
workflow_dispatch:
inputs:
release_at:
description: 'When to release — local datetime (YYYY-MM-DD HH:MM), interpreted in the timezone below'
required: true
timezone:
description: 'Timezone for release_at'
required: true
default: 'America/Chicago'
type: choice
options:
- America/Chicago
- America/New_York
- America/Los_Angeles
- America/Denver
- UTC
action:
description: 'Action'
required: true
default: 'schedule'
type: choice
options:
- schedule
- cancel
- show

permissions:
actions: write
contents: read
# Needed to read/write repository variables via gh api
# Requires GITHUB_TOKEN with repo admin, OR a PAT stored as RELEASE_SCHEDULER_TOKEN

jobs:
manage-schedule:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.RELEASE_SCHEDULER_TOKEN || secrets.GITHUB_TOKEN }}
steps:
- name: Show current schedule
if: ${{ inputs.action == 'show' }}
run: |
current=$(gh variable get PENDING_RELEASE_AT --repo "${{ github.repository }}" 2>/dev/null || echo "")
if [ -z "$current" ]; then
echo "::notice::No release scheduled."
else
echo "::notice::Release scheduled for: $current (UTC)"
fi

- name: Cancel scheduled release
if: ${{ inputs.action == 'cancel' }}
run: |
gh variable delete PENDING_RELEASE_AT --repo "${{ github.repository }}" 2>/dev/null || true
echo "::notice::Scheduled release canceled."

- name: Schedule release
if: ${{ inputs.action == 'schedule' }}
run: |
local_input="${{ inputs.release_at }}"
tz="${{ inputs.timezone }}"

# Parse local datetime in the chosen timezone, convert to UTC ISO8601
utc_iso=$(TZ="$tz" date -d "$local_input" -u +"%Y-%m-%dT%H:%M:%SZ") || {
echo "::error::Could not parse '$local_input'. Use format: YYYY-MM-DD HH:MM"
exit 1
}

now_epoch=$(date -u +%s)
target_epoch=$(date -d "$utc_iso" -u +%s)
if [ "$target_epoch" -le "$now_epoch" ]; then
echo "::error::Scheduled time is in the past: $utc_iso"
exit 1
fi

gh variable set PENDING_RELEASE_AT --body "$utc_iso" --repo "${{ github.repository }}"

human_local=$(TZ="$tz" date -d "$utc_iso" +"%Y-%m-%d %H:%M %Z")
echo "::notice::Release scheduled for $human_local ($utc_iso UTC)"
Loading