Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
78 changes: 78 additions & 0 deletions .github/actions/pr-status-updater/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: PR Status Updater
description: Updates commit statuses on open PRs for code freeze enforcement

inputs:
github_token:
description: "GitHub token for API access"
required: true
state:
description: "The state to set (success or failure)"
required: true
workflow_name:
description: "Name of the workflow for click-through link"
required: true

runs:
using: composite
steps:
- name: Update PR statuses
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github_token }}
STATE: ${{ inputs.state }}
WORKFLOW_NAME: ${{ inputs.workflow_name }}
run: |
set -e

REPO="${{ github.repository }}"
CONTEXT="code-freeze"

if [ "$STATE" = "failure" ]; then
DESCRIPTION="Code freeze is active - PRs not in Code Freeze/Incident milestones cannot be merged"
else
DESCRIPTION="Code freeze has ended - merging allowed"
fi

echo "Fetching open PRs..."

# Get all open PRs
PRS=$(gh pr list --repo "$REPO" --state open --json number,headRefOid,milestone --limit 1000)

echo "Found $(echo "$PRS" | jq length) open PRs"

# Process each PR
echo "$PRS" | jq -c '.[]' | while read -r pr; do
PR_NUMBER=$(echo "$pr" | jq -r '.number')
SHA=$(echo "$pr" | jq -r '.headRefOid')
MILESTONE_TITLE=$(echo "$pr" | jq -r '.milestone.title // ""')

# Determine the state for this PR
PR_STATE="$STATE"
PR_DESC="$DESCRIPTION"

# If we're freezing (state=failure), check if PR is in an allowed milestone
if [ "$STATE" = "failure" ]; then
if [[ "$MILESTONE_TITLE" == Code\ Freeze* ]] || [[ "$MILESTONE_TITLE" == Incident* ]]; then
PR_STATE="success"
PR_DESC="PR is in '$MILESTONE_TITLE' milestone - exempt from code freeze"
echo "PR #$PR_NUMBER is in milestone '$MILESTONE_TITLE' - marking as exempt"
else
echo "PR #$PR_NUMBER is NOT in a code freeze/incident milestone - blocking"
fi
fi

# Set the commit status
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$REPO/statuses/$SHA" \
-f state="$PR_STATE" \
-f target_url="${{ github.server_url }}/${{ github.repository }}/actions/workflows/$WORKFLOW_NAME" \
-f description="$PR_DESC" \
-f context="$CONTEXT"

echo "Set status '$PR_STATE' on PR #$PR_NUMBER (SHA: ${SHA:0:7})"
done

echo "Done updating PR statuses"
96 changes: 96 additions & 0 deletions .github/workflows/code_freeze_check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Code Freeze Check

on:
pull_request:
types: [opened, synchronize, reopened, edited, milestoned, demilestoned]

permissions:
contents: read
pull-requests: read
statuses: write
issues: read

jobs:
code_freeze_check:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

steps:
- name: Check code freeze status
id: check
run: |
set -e

REPO="${{ github.repository }}"
PR_NUMBER="${{ github.event.pull_request.number }}"

echo "Checking code freeze status for PR #$PR_NUMBER..."

# Get all open milestones
MILESTONES=$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$REPO/milestones?state=open" \
--jq '.[] | select(.title | startswith("Code Freeze") or startswith("Incident")) | .title')

# Check if any code freeze milestones are open
if [ -z "$MILESTONES" ]; then
echo "✅ No active code freeze - merging allowed"
echo "freeze_active=false" >> "$GITHUB_OUTPUT"
echo "pr_exempt=true" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "🥶 Active code freeze milestones:"
echo "$MILESTONES"
echo "freeze_active=true" >> "$GITHUB_OUTPUT"

# Get the PR's milestone
PR_MILESTONE=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json milestone --jq '.milestone.title // ""')

echo "PR milestone: '$PR_MILESTONE'"

# Check if PR is in an allowed milestone
if [[ "$PR_MILESTONE" == Code\ Freeze* ]] || [[ "$PR_MILESTONE" == Incident* ]]; then
echo "✅ PR #$PR_NUMBER is in milestone '$PR_MILESTONE' - exempt from code freeze"
echo "pr_exempt=true" >> "$GITHUB_OUTPUT"
else
echo "❌ PR #$PR_NUMBER is NOT in a code freeze/incident milestone"
echo "pr_exempt=false" >> "$GITHUB_OUTPUT"
fi

- name: Set commit status
run: |
SHA="${{ github.event.pull_request.head.sha }}"
REPO="${{ github.repository }}"
CONTEXT="code-freeze"

if [ "${{ steps.check.outputs.freeze_active }}" = "false" ]; then
STATE="success"
DESCRIPTION="No active code freeze - merging allowed"
elif [ "${{ steps.check.outputs.pr_exempt }}" = "true" ]; then
STATE="success"
DESCRIPTION="PR is in Code Freeze/Incident milestone - exempt from code freeze"
else
STATE="failure"
DESCRIPTION="Code freeze is active - add PR to Code Freeze/Incident milestone to merge"
fi

gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$REPO/statuses/$SHA" \
Comment on lines +80 to +84

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid failing forked PRs when posting status

This workflow runs on pull_request events but always posts a commit status via gh api to /statuses/$SHA. On forked PRs, GitHub Actions provides a read‑only GITHUB_TOKEN that cannot write commit statuses, so this POST returns 403 and the step fails. That means the workflow itself fails even when no code freeze is active, blocking merges for external contributors. Consider skipping the status update for forked PRs or using a safer event/permissions model.

Useful? React with 👍 / 👎.

-f state="$STATE" \
-f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
-f description="$DESCRIPTION" \
-f context="$CONTEXT"

echo "Set status '$STATE' on commit ${SHA:0:7}"

# Fail the job if PR is blocked
if [ "$STATE" = "failure" ]; then
echo "::error::Code freeze is active. Add this PR to a 'Code Freeze' or 'Incident' milestone to allow merging."
exit 1
fi
74 changes: 74 additions & 0 deletions .github/workflows/code_freeze_end.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: End code freeze

on:
workflow_dispatch:
milestone:
types: [closed, deleted]

jobs:
end_code_freeze:
# Trigger on:
# - Manual dispatch (unfreezes all PRs)
# - Milestone events where title starts with "Code Freeze" or "Incident" and state is closed
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'milestone' &&
(startsWith(github.event.milestone.title, 'Code Freeze') ||
startsWith(github.event.milestone.title, 'Incident')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read # Fetches PRs
statuses: write # add a commit status check
issues: read # Required to list milestones
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

steps:
- name: Log code freeze end
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "🌡️ Code freeze ended manually"
else
echo "🌡️ Code freeze ended by closing milestone: ${{ github.event.milestone.title }}"
fi

- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0

- name: Check for remaining freeze milestones
id: check_remaining
run: |
REPO="${{ github.repository }}"

# Check if any other Code Freeze or Incident milestones are still open
REMAINING=$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$REPO/milestones?state=open" \
--jq '.[] | select(.title | startswith("Code Freeze") or startswith("Incident")) | .title')

if [ -z "$REMAINING" ]; then
echo "✅ No remaining freeze milestones - proceeding with unfreeze"
echo "should_unfreeze=true" >> "$GITHUB_OUTPUT"
else
echo "⚠️ Other freeze milestones are still open:"
echo "$REMAINING"

# For manual dispatch, warn but allow unfreezing all PRs
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "🔓 Manual dispatch - unfreezing all PRs despite active freeze milestones"
echo "should_unfreeze=true" >> "$GITHUB_OUTPUT"
else
echo "🔒 Skipping unfreeze because other freeze milestones remain open"
echo "should_unfreeze=false" >> "$GITHUB_OUTPUT"
fi
fi

- name: Unfreeze PRs
if: steps.check_remaining.outputs.should_unfreeze == 'true'
uses: ./.github/actions/pr-status-updater
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
state: success
workflow_name: code_freeze_end.yml
52 changes: 52 additions & 0 deletions .github/workflows/code_freeze_start.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Start code freeze

on:
workflow_dispatch:
inputs:
milestone_title:
description: 'Milestone title (must start with "Code Freeze" or "Incident")'
required: true
type: string
milestone:
types: [opened, created, edited]

jobs:
start_code_freeze:
# Trigger on:
# - Manual dispatch with a valid milestone title
# - Milestone events where title starts with "Code Freeze" or "Incident" and state is open
if: |
(github.event_name == 'workflow_dispatch' &&
(startsWith(github.event.inputs.milestone_title, 'Code Freeze') ||
startsWith(github.event.inputs.milestone_title, 'Incident'))) ||
(github.event_name == 'milestone' &&
github.event.milestone.state == 'open' &&
(startsWith(github.event.milestone.title, 'Code Freeze') ||
startsWith(github.event.milestone.title, 'Incident')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read # Fetches PRs
statuses: write # add a commit status check
issues: read # Required to query milestone information
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

steps:
- name: Log code freeze trigger
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "🥶 Code freeze triggered manually for milestone: ${{ github.event.inputs.milestone_title }}"
else
echo "🥶 Code freeze triggered by milestone: ${{ github.event.milestone.title }}"
fi

- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0

- name: Freeze PRs
uses: ./.github/actions/pr-status-updater
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
state: failure
workflow_name: code_freeze_start.yml
Loading