Skip to content

Commit 8b844b7

Browse files
authored
Merge pull request #215 from faetalize/feature/transition-to-pinned-prod-branch
Establish protected production release workflow
2 parents a6b9e11 + 1077c3d commit 8b844b7

5 files changed

Lines changed: 118 additions & 17 deletions

File tree

.github/release-drafter.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name-template: "v$NEXT_PATCH_VERSION"
2+
tag-template: "v$NEXT_PATCH_VERSION"
3+
commitish: production
4+
change-template: "- $TITLE (#$NUMBER)"
5+
change-title-escapes: "<*_&"
6+
7+
categories:
8+
- title: "Features"
9+
labels:
10+
- feature
11+
- title: "Improvements"
12+
labels:
13+
- enhancement
14+
- title: "Fixes"
15+
labels:
16+
- bug
17+
18+
include-labels:
19+
- feature
20+
- enhancement
21+
- bug
22+
23+
exclude-labels:
24+
- code improvement
25+
- documentation
26+
- skip-changelog
27+
28+
template: |
29+
## What's New
30+
31+
$CHANGES
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { readFileSync } from "node:fs";
2+
import process from "node:process";
3+
4+
const classificationLabels = new Set([
5+
"feature",
6+
"enhancement",
7+
"bug",
8+
"code improvement",
9+
"documentation",
10+
"skip-changelog"
11+
]);
12+
13+
const eventPath = process.env.GITHUB_EVENT_PATH;
14+
if (!eventPath) {
15+
throw new Error("GITHUB_EVENT_PATH is required to validate pull request labels.");
16+
}
17+
18+
const event = JSON.parse(readFileSync(eventPath, "utf8"));
19+
const pullRequestLabels = event.pull_request?.labels?.map((label) => label.name) ?? [];
20+
const classifications = pullRequestLabels.filter((label) => classificationLabels.has(label));
21+
22+
if (classifications.length !== 1) {
23+
process.stderr.write(
24+
`Pull requests must have exactly one release classification label. Found ${classifications.length}: ${
25+
classifications.join(", ") || "none"
26+
}. Choose one of: ${[...classificationLabels].join(", ")}.\n`
27+
);
28+
process.exit(1);
29+
}
30+
31+
process.stdout.write(`Release classification: ${classifications[0]}\n`);

.github/workflows/merge-gate.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ on:
44
pull_request:
55
branches:
66
- main
7+
- production
8+
types:
9+
- opened
10+
- reopened
11+
- synchronize
12+
- labeled
13+
- unlabeled
714
workflow_dispatch:
815

916
jobs:
@@ -14,6 +21,10 @@ jobs:
1421
- name: Checkout repository
1522
uses: actions/checkout@v6
1623

24+
- name: Validate release classification
25+
if: github.event_name == 'pull_request' && github.base_ref == 'main'
26+
run: node .github/scripts/validate-release-label.mjs
27+
1728
- name: Setup Node.js
1829
uses: actions/setup-node@v6
1930
with:
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Release Drafter
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
pull-requests: read
12+
13+
jobs:
14+
update-release-draft:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Update draft release
19+
uses: release-drafter/release-drafter@v7
20+
with:
21+
config-name: release-drafter.yml

AGENTS.md

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,19 @@ npm run sync-db-types # Sync Supabase types to src/types/database.types.ts
4141

4242
### Branching and deployment flow
4343

44-
- Production deploys are published from a dedicated release branch because Cloudflare Pages is configured to deploy a specific branch for releases.
44+
- Cloudflare Pages production deployments are pinned to the permanent `production` branch.
4545
- `main` is the ongoing development branch.
46-
- A release branch is created from `main` after the intended features/fixes have already been merged there.
47-
- Release-only preparation happens on that release branch first.
48-
- After the release is deployed, the release branch must be backmerged into `main` so `main` also contains the final version string and in-app changelog for that release.
46+
- Feature and fix pull requests merge into `main`; do not commit feature work directly to `production`.
47+
- A temporary `release/vX.Y.Z` branch is created from `main` for the version bump and other release-only preparation. Build the in-app changelog from the current draft GitHub Release, polishing and combining its entries into user-facing copy.
48+
- Merge the prepared release branch back into `main` with the `skip-changelog` label on the PR, then promote `main` to `production` through a second pull request.
49+
- Cloudflare deploys the merge into `production`. No release backmerge is needed because release preparation entered `main` before promotion.
50+
51+
### Pull request release classification
52+
53+
- Every pull request targeting `main` must have exactly one release classification label. The merge gate enforces this rule. Promotion pull requests targeting `production` still run the full merge gate but do not need a release classification.
54+
- Use `feature`, `enhancement`, or `bug` for user-facing changes. Write those pull request titles as concise release-note candidates because Release Drafter uses them directly.
55+
- Use `code improvement` for internal refactors, `documentation` for documentation-only work, and `skip-changelog` for release preparation or other administrative changes targeting `main`. These labels are excluded from user-facing release notes.
56+
- Release Drafter runs after merges into `main` and continuously maintains the next draft GitHub Release from included pull requests. The draft targets `production` so publishing it after deployment tags the deployed branch.
4957

5058
### Pro request edge function slots
5159

@@ -54,16 +62,17 @@ npm run sync-db-types # Sync Supabase types to src/types/database.types.ts
5462
- `handle-pro-request` and `handle-pro-request-x` are equivalent rotating production slots. The currently released frontend points at one slot, while the other slot is available for the next synced frontend/backend release.
5563
- Only change the committed `PRO_REQUEST_FUNCTION_NAME` target as part of a new release workflow. Do not change the production target for ordinary feature work, fixes, local testing, or short-lived validation.
5664
- When a frontend/backend sync release is required, deploy the backend update to the production slot that the currently released frontend is not using, update `PRO_REQUEST_FUNCTION_NAME` to point the new frontend bundle at that slot, then deploy the frontend. This keeps old loaded clients on the old function and new clients on the new function.
57-
- Use `handle-pro-request-test` for quick backend iteration and local/manual validation. Point `PRO_REQUEST_FUNCTION_NAME` at the test slot only for local test builds or short-lived validation branches; do not leave production release branches pointed at the test slot.
65+
- Use `handle-pro-request-test` for quick backend iteration and local/manual validation. Point `PRO_REQUEST_FUNCTION_NAME` at the test slot only for local test builds or short-lived validation branches; do not promote a release to `production` while it points at the test slot.
5866
- Before changing the production target, verify every premium caller uses `PRO_REQUEST_ENDPOINT` rather than hardcoding a function URL.
5967

6068
### Preparing a new release
6169

62-
- Identify the last release backmerge commit on `main`, then inspect all mainline commits after that point up to `HEAD`.
63-
- Use those commits to determine what actually shipped in the new release.
70+
- Open the continuously maintained draft GitHub Release before creating the release branch. Its categorized entries are the release-note candidates collected since the previous published release.
71+
- Create `release/vX.Y.Z` from `main`, then polish the draft entries into the final user-facing copy. Combine related pull requests and remove implementation detail rather than reconstructing the release from commit history.
6472
- Update the user-facing changelog in [src/index.html](src/index.html) under the `#whats-new` section.
6573
- Update the version string in [src/utils/helpers.ts](src/utils/helpers.ts) so the badge and changelog header display the new version.
66-
- Keep the release branch and deployed artifact aligned before any tag is created.
74+
- Update the draft GitHub Release to mirror the final in-app changelog.
75+
- Merge the release branch into `main`, promote `main` to `production`, and verify the Cloudflare deployment before publishing the draft.
6776

6877
### How to build the changelog well
6978

@@ -76,17 +85,15 @@ npm run sync-db-types # Sync Supabase types to src/types/database.types.ts
7685

7786
### Tagging guidance
7887

79-
- Default release tags should point at the `main` merge commit created when the release branch is backmerged into `main`.
80-
- Do not create the release tag while the release PR is still open unless the user explicitly chooses to tag the release branch artifact instead.
81-
- If tags are meant to represent what is on `main`, create the tag after the release branch has been backmerged into `main`.
82-
- If tags are meant to represent the exact commit deployed by Cloudflare Pages, tag the release branch commit that was actually deployed.
83-
- Do not tag `main` before the backmerge if `main` does not yet contain the final release changelog/version bump.
88+
- Release tags must point at the exact commit deployed from `production`.
89+
- Do not create or publish the tag while either the release-preparation PR into `main` or the promotion PR into `production` is still open.
90+
- Verify the Cloudflare production deployment first, then publish the draft GitHub Release. Its `production` target creates the `vX.Y.Z` tag at the deployed branch head.
8491

8592
### Creating the GitHub Release
8693

87-
- After the release tag has been pushed, create a GitHub Release from that tag so every shipped version has a published release entry on GitHub.
88-
- Create the release only once the tag exists, and respect the same constraints as the tagging step: do not publish the release while the release PR is still open unless the user explicitly chose to tag/release the deployed release-branch artifact instead.
89-
- Point the GitHub Release at the same tag chosen in the tagging step. By default that is the `vX.Y.Z` tag on the `main` backmerge commit; use the release-branch commit only if the user opted to tag the exact deployed artifact.
94+
- Release Drafter creates and maintains the upcoming GitHub Release as a draft; do not create a second release manually.
95+
- Publish the existing draft only after `main` has been promoted to `production` and the Cloudflare deployment has been verified.
96+
- Confirm that the draft targets `production` and that its version tag matches the version in [src/utils/helpers.ts](src/utils/helpers.ts).
9097
- Title the release to match that version tag (for example, `v1.8.5`).
9198
- The GitHub Release notes should mirror the in-app changelog from the `#whats-new` section in [src/index.html](src/index.html). Do not use raw PR titles or commit messages.
9299
- Reformat the changelog entries as a Markdown list under a `## What's New` heading, keeping the same user-facing, product-focused phrasing (bold the feature name, then the benefit):
@@ -98,7 +105,7 @@ npm run sync-db-types # Sync Supabase types to src/types/database.types.ts
98105
- **More model choices:** New OpenRouter options are available, including expanded Gemini, Grok, Qwen, DeepSeek, and Inception models.
99106
```
100107

101-
- Create the release with `gh release create <tag> --title <tag> --notes "..."`, reusing the changelog copy already written for the in-app `#whats-new` section so the GitHub Release and the in-app changelog stay in sync.
108+
- Publish the prepared draft with `gh release edit <tag> --draft=false --latest`, after confirming its title, notes, and `production` target.
102109

103110
## Conventions
104111

0 commit comments

Comments
 (0)