Skip to content
Closed
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
81 changes: 81 additions & 0 deletions .github/workflows/release-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Release Gate

on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]

jobs:
milestone-closure-check:
name: Verify all active milestones are complete
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- name: Check for partially complete milestones
id: gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Checking milestone completion status..."

# Fetch all open milestones; flag any that are partially complete
# (have both closed AND open issues — meaning work started but not finished)
PARTIAL=$(gh api repos/${{ github.repository }}/milestones \
--jq '[.[] | select(.open_issues > 0 and .closed_issues > 0)] |
map(" ❌ \(.title): \(.open_issues) open issue(s) remaining") | .[]')

if [ -n "$PARTIAL" ]; then
echo "GATE_PASSED=false" >> "$GITHUB_OUTPUT"
echo "PARTIAL_MILESTONES<<EOF" >> "$GITHUB_OUTPUT"
echo "$PARTIAL" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
else
echo "GATE_PASSED=true" >> "$GITHUB_OUTPUT"
fi

- name: Comment on PR if blocked
if: steps.gate.outputs.GATE_PASSED == 'false'
uses: actions/github-script@v7
with:
script: |
const partial = `${{ steps.gate.outputs.PARTIAL_MILESTONES }}`;
const body = [
'## ❌ Release Gate Blocked',
'',
'This PR cannot be merged to `main` because one or more sprint milestones',
'have open issues. All milestone issues must be **closed (Done)** before releasing.',
'',
'### Milestones with open issues',
partial,
'',
'**Next steps:**',
'- Close or move the remaining issues listed above.',
'- If an issue is deferred to a future sprint, reassign it to the next milestone.',
'- Once all active milestones are complete, this check will pass automatically.',
].join('\n');

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});

- name: Fail if gate did not pass
if: steps.gate.outputs.GATE_PASSED == 'false'
run: |
echo ""
echo "❌ Release gate FAILED. Partially complete milestones detected:"
echo "${{ steps.gate.outputs.PARTIAL_MILESTONES }}"
echo ""
echo "Close or reassign all open issues before merging to main."
exit 1

- name: Gate passed
if: steps.gate.outputs.GATE_PASSED == 'true'
run: |
echo "✅ Release gate PASSED — all active milestones are complete."
18 changes: 17 additions & 1 deletion .squad/playbooks/release-myblog.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Use this playbook when MyBlog is ready to move validated work from `dev` to
| **Release Branch** | `main` | Release-only branch |
| **Hotfix Branches** | `hotfix/*` | Branch from `main`, then backport to `dev` |
| **Versioning** | `GitVersion.yml` | SemVer labels from branch + git history |
| **Active Workflows** | `ci.yml`, `hotfix-backport-reminder.yml` | No automated release/promote workflow today |
| **Active Workflows** | `ci.yml`, `release-gate.yml`, `hotfix-backport-reminder.yml` | `release-gate.yml` blocks merge to `main` if any active milestone has open issues |
| **Published Artifacts** | None automated | No NuGet, Docker, docs, or deploy workflow yet |
| **GitHub Release** | Optional manual step | Useful for notes and tags; does not deploy anything |

Expand All @@ -36,6 +36,20 @@ Use this playbook when MyBlog is ready to move validated work from `dev` to
`dev` already contains the source changes. Only hotfixes merged to `main`
need a backport to `dev`

## Release readiness gate (automated)

`release-gate.yml` runs on every PR targeting `main`. It blocks merge if any
milestone has **both open and closed issues** (i.e., a sprint that started but
is not fully done).

**Gate logic:**
- Milestone with only open issues → future sprint, not yet started → **allowed**
- Milestone with only closed issues → complete sprint → **allowed**
- Milestone with open AND closed issues → partially done sprint → **blocked**

To unblock: close the remaining open issues, or reassign them to a future
milestone. The check re-runs automatically when the PR is updated.

## Standard release path (`dev` → `main`)

### 1. Verify `dev` is release-ready
Expand All @@ -44,6 +58,8 @@ Before opening a release PR, confirm:

- All intended `squad/*` work is already merged into `dev`
- The latest `dev` commit is green in GitHub Actions (`ci.yml`)
- **Project #4 board shows zero open issues for the milestone(s) being released**
(the `release-gate.yml` workflow enforces this automatically on the PR)
- Any release notes summary is ready for the PR body or GitHub Release notes
- No emergency hotfix backports are still missing from `dev`

Expand Down
Loading