Skip to content

Commit 0656888

Browse files
committed
docs(skills): rewrite release-promotion — automated factory, no manual gates
- use_merge_queue: true is unconditional and required, not event-conditional - action_required on promotion PR checks is expected and normal (org blocks github-actions[bot] PR runs); merge_group events bypass this - documents why merge queue is required: gh pr merge --auto is blocked by the merge queue ruleset, enqueuePullRequest is the only working path - ruleset updated: main-merge-queue-no-review (id 18008292) replaces main-review-required-with-renovate-bypass (deleted) - required_approving_review_count is 0 — promotion is fully automated - adds DELETE+POST pattern for ruleset management (PATCH returns 404) - removes all guidance saying use_merge_queue should be conditional - removes 'preserves human approval gates' from verification (wrong frame) - ci-reference.md: ruleset table updated, stale required_approving_review_count=1 example corrected to 0 Assisted-by: Claude Sonnet 4.6 via GitHub Copilot
1 parent fbe0aa4 commit 0656888

2 files changed

Lines changed: 170 additions & 61 deletions

File tree

docs/skills/ci-reference.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,15 +235,17 @@ PRs created by a workflow using `GITHUB_TOKEN` do NOT fire `pull_request` events
235235

236236
## Ruleset
237237

238-
Ruleset: `main-review-required-with-renovate-bypass`
238+
Ruleset: `main-merge-queue-no-review` (id: 18008292)
239239

240240
| Rule | Value |
241241
|---|---|
242-
| Required reviews | 1 approving review |
243-
| Required status checks | `validate` + `e2e` |
244-
| Merge queue | ALLGREEN, max_entries_to_build=1, check_response_timeout=120 min |
242+
| Required reviews | 0 (fully automated — no human approval) |
243+
| Required status checks | `validate` (strict) |
244+
| Merge queue | SQUASH, ALLGREEN, max_entries_to_build=2, timeout=120 min |
245245
| Bypass actors | OrganizationAdmin, Renovate, mergeraptor |
246246

247+
**`action_required` on promotion PR checks is expected.** The org blocks `github-actions[bot]`-triggered `pull_request` workflow runs. `validate` and all other PR checks show `action_required` on every promotion PR — this is normal and not a failure. The merge queue fires `merge_group` events which bypass the bot approval policy and run `validate` cleanly.
248+
247249
**e2e change detection:** `e2e` only tests PRs touching `elements/`, `files/`, `patches/`, `Justfile`, or `project.conf`. For all other paths (e.g. workflow pin bumps) the `e2e` job is skipped, which satisfies the required check. The `should-run` job uses `git diff` against the PR base — no `paths:` filter on the trigger.
248250

249251
**Critical:** Required status checks must only include checks that fire on `pull_request`. A check that only fires on `merge_group` will permanently block the "Add to merge queue" button.
@@ -1818,8 +1820,8 @@ gh api repos/projectbluefin/dakota/branches/main/protection \
18181820
"required_status_checks": {"strict": false, "checks": [{"context": "validate", "app_id": -1}]},
18191821
"enforce_admins": false,
18201822
"required_pull_request_reviews": {
1821-
"dismiss_stale_reviews": true, "require_code_owner_reviews": true,
1822-
"required_approving_review_count": 1
1823+
"dismiss_stale_reviews": false, "require_code_owner_reviews": false,
1824+
"required_approving_review_count": 0
18231825
},
18241826
"restrictions": null
18251827
}

docs/skills/release-promotion.md

Lines changed: 162 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: release-promotion
3-
description: Dakota publish and promotion flow from main to testing to stable, including promotion PRs, release gate behavior, and manual recovery. Use when working on promotion workflows, action_required gates, release execution, or stable-cut logic.
3+
description: Dakota publish and promotion flow from testing to main to stable, including promotion PRs, merge queue wiring, release gate behavior, and manual recovery. Use when working on promote-testing-to-main.yml, action_required on promotion PRs, merge queue setup, branch protection, or stable-cut logic.
44
metadata:
55
context7-sources:
66
- /websites/github_en_actions
@@ -12,8 +12,10 @@ metadata:
1212
## Overview
1313

1414
Dakota has **two promotion layers**:
15-
1. `main` merge → publish → `:testing`
16-
2. `testing` → promotion PR → stable release execution
15+
1. `testing` → promotion PR → merge → `main` → publish → `:testing` / `:latest` / `:stable`
16+
2. Weekly human-gated stable cut via `production` Environment (2 approvals)
17+
18+
Promotion from `testing` to `main` is **fully automated** — no human approval required at the PR level. The only human gate is the weekly `production` Environment for stable.
1719

1820
Do not conflate "publish is healthy" with "stable promotion is healthy".
1921

@@ -24,7 +26,9 @@ Use when the task mentions:
2426
- `pr-release-gate.yml`
2527
- `execute-release.yml`
2628
- promotion PRs from `auto/promote-testing-to-main`
27-
- `action_required` release gates
29+
- `action_required` on promotion PR checks
30+
- merge queue, `use_merge_queue`, `enqueuePullRequest`
31+
- branch protection or ruleset on `main`
2832
- stable release, `:latest`, `:stable`, or promotion PR flow
2933

3034
## When NOT to Use
@@ -40,19 +44,20 @@ Use when the task mentions:
4044
2. **Identify the stage.**
4145
- publish to `:testing`
4246
- open/update promotion PR
43-
- gate the promotion PR
47+
- gate the promotion PR (cosign verify)
4448
- execute stable release after merge
45-
3. **Check whether the gate is real or infrastructure.**
46-
- `action_required` on a promotion PR often means the gate is waiting on policy or verification, not that the YAML crashed.
47-
4. **For promotion PR workflows, inspect reusable caller permissions first.**
49+
3. **`action_required` on promotion PR checks is expected and normal.**
50+
The org blocks `github-actions[bot]`-triggered PR workflow runs. This means
51+
`validate` and all other `pull_request` checks show `action_required` on every
52+
promotion PR. This is NOT a failure — the merge queue bypasses it.
53+
4. **The merge queue is the only correct auto-merge path for dakota.**
54+
`gh pr merge --auto` (`enablePullRequestAutoMerge`) is blocked by the merge queue
55+
ruleset. Only `enqueuePullRequest` (triggered by `use_merge_queue: true`) works.
4856
5. **Do not add e2e back into the promotion PR path.**
4957
Dakota intentionally gates stable at the later human-approved release stage.
5058
6. **Automatic promotion cadence is Tuesday 04:00 UTC.**
5159
That schedule re-evaluates the promotion PR for the weekly stable cut.
52-
7. **Manual `workflow_dispatch` must preserve queue behavior.**
53-
`use_merge_queue` should be true for `schedule` and `workflow_dispatch`, but
54-
not for ordinary `push` refreshes from `testing`.
55-
8. **For manual recovery, re-run the failed publish/promote workflow that owns the stage, not some nearby check.**
60+
7. **For manual recovery, re-run the failed publish/promote workflow that owns the stage.**
5661

5762
## Promotion Map
5863

@@ -64,20 +69,58 @@ push to testing (BST-affecting paths)
6469
→ promote-testing-to-main.yml
6570
→ auto/promote-testing-to-main PR
6671
→ pr-release-gate.yml (cosign verify :testing)
67-
→ auto-merge → push to main
68-
→ execute-release.yml (commit message gate)
69-
→ :latest / :stable + GitHub Release
72+
→ enqueuePullRequest → merge queue
73+
→ merge_group event → validate check (bypasses action_required)
74+
→ merge to main
75+
→ execute-release.yml (commit message gate)
76+
→ :latest / :stable + GitHub Release
7077
```
7178

79+
## Branch Protection and Ruleset State
80+
81+
Ruleset: `main-merge-queue-no-review` (id: 18008292)
82+
83+
| Rule | Value |
84+
|---|---|
85+
| Required reviews | 0 (fully automated) |
86+
| Required status checks | `validate` (strict) |
87+
| Merge queue | SQUASH, ALLGREEN, max_entries_to_build=2, timeout=120 min |
88+
| Bypass actors | OrganizationAdmin (always), Renovate (PR), mergeraptor (PR) |
89+
| Non-fast-forward | enforced |
90+
| Deletion | blocked |
91+
92+
Classic branch protection on `main`: `required_approving_review_count: 0`.
93+
94+
**Never re-add a required review count to classic protection or the ruleset.** It blocks every automated promotion PR permanently — the GHA bot cannot approve its own PRs.
95+
96+
## Workflow Configuration
97+
98+
`promote-testing-to-main.yml` must always have `use_merge_queue: true`:
99+
100+
```yaml
101+
jobs:
102+
promote:
103+
uses: projectbluefin/actions/.github/workflows/reusable-promote-squash.yml@v1
104+
with:
105+
variants: '[{"image":"dakota"},{"image":"dakota-nvidia"}]'
106+
cosign_identity_regexp: >-
107+
^https://github\.com/projectbluefin/(dakota|actions)/\.github/workflows/
108+
run_e2e: false
109+
use_merge_queue: true
110+
```
111+
112+
Do not make `use_merge_queue` conditional on event type. It must always be `true`.
113+
72114
## Hard Rules
73115

74116
- `promote-testing-to-main.yml` is a thin caller. Treat caller-level `permissions:` as critical.
75117
- `pr-release-gate.yml` must not starve the reusable gate token.
76-
- Promotion PRs do **not** run the full e2e quality gate; that belongs at the later stable promotion stage.
77-
- When a gate is `action_required`, inspect what policy condition it is waiting on before editing workflow code.
118+
- Promotion PRs do **not** run the full e2e quality gate; that belongs at the weekly stable gate.
119+
- `use_merge_queue: true` — unconditional, always. Not conditional on `github.event_name`.
120+
- The merge queue ruleset (`merge_queue` rule type) must exist on `main`. Without it, `enqueuePullRequest` fails silently.
121+
- `required_approving_review_count` must be 0 in both ruleset and classic branch protection.
78122
- Weekly automatic stable evaluation runs Tuesday at `0 4 * * 2`.
79123
- Keep `run_e2e: false` in dakota's promotion caller.
80-
- `use_merge_queue` should only enqueue on `schedule` or `workflow_dispatch`.
81124

82125
## Manual Recovery Shortcuts
83126

@@ -91,75 +134,139 @@ gh run list --repo projectbluefin/dakota --workflow 'PR Release Gate' --limit 10
91134
92135
# recent promote runs
93136
gh run list --repo projectbluefin/dakota --workflow 'Promote testing to main' --limit 10
137+
138+
# verify ruleset is correct
139+
gh api repos/projectbluefin/dakota/rulesets | jq '[.[] | {id, name}]'
140+
gh api repos/projectbluefin/dakota/rulesets/18008292 | jq '[.rules[].type]'
141+
142+
# verify classic branch protection has 0 reviews
143+
gh api repos/projectbluefin/dakota/branches/main/protection \
144+
| jq '.required_pull_request_reviews.required_approving_review_count'
145+
```
146+
147+
## Why `use_merge_queue: true` is required (not optional)
148+
149+
The `projectbluefin` org blocks `github-actions[bot]`-triggered PR workflow runs.
150+
Every workflow fired by the promotion PR (`pull_request` event) gets `action_required`
151+
conclusion — the run is paused waiting for org approval. This means `validate` is never
152+
posted as a passing check, so `gh pr merge --auto` waits forever.
153+
154+
The merge queue fires `merge_group` events instead of `pull_request` events. `merge_group`
155+
is NOT subject to the bot approval policy. `validate` runs clean, passes, and the queue
156+
merges. This is why `use_merge_queue: true` is unconditional — without it, the promotion
157+
PR never merges automatically.
158+
159+
Bluefin uses exactly this pattern. Dakota must match it.
160+
161+
## Ruleset Management
162+
163+
The ruleset cannot be updated via `PATCH /repos/.../rulesets/{id}` with a standard
164+
`repo`-scoped PAT — returns 404. Use DELETE + POST to replace it:
165+
166+
```bash
167+
# Delete old ruleset
168+
curl -X DELETE \
169+
-H "Authorization: Bearer $(gh auth token)" \
170+
-H "Accept: application/vnd.github+json" \
171+
"https://api.github.com/repos/projectbluefin/dakota/rulesets/{OLD_ID}"
172+
173+
# Create new ruleset
174+
curl -X POST \
175+
-H "Authorization: Bearer $(gh auth token)" \
176+
-H "Accept: application/vnd.github+json" \
177+
-H "Content-Type: application/json" \
178+
"https://api.github.com/repos/projectbluefin/dakota/rulesets" \
179+
-d '{
180+
"name": "main-merge-queue-no-review",
181+
"target": "branch",
182+
"enforcement": "active",
183+
"conditions": {"ref_name": {"include": ["refs/heads/main"], "exclude": []}},
184+
"bypass_actors": [
185+
{"actor_id": null, "actor_type": "OrganizationAdmin", "bypass_mode": "always"},
186+
{"actor_id": 2740, "actor_type": "Integration", "bypass_mode": "pull_request"},
187+
{"actor_id": 3069633, "actor_type": "Integration", "bypass_mode": "pull_request"}
188+
],
189+
"rules": [
190+
{"type": "pull_request", "parameters": {
191+
"required_approving_review_count": 0,
192+
"dismiss_stale_reviews_on_push": true,
193+
"require_code_owner_review": false,
194+
"require_last_push_approval": false,
195+
"required_review_thread_resolution": true,
196+
"allowed_merge_methods": ["squash"]
197+
}},
198+
{"type": "required_status_checks", "parameters": {
199+
"strict_required_status_checks_policy": true,
200+
"do_not_enforce_on_create": false,
201+
"required_status_checks": [{"context": "validate", "integration_id": 15368}]
202+
}},
203+
{"type": "merge_queue", "parameters": {
204+
"merge_method": "SQUASH",
205+
"max_entries_to_build": 2,
206+
"min_entries_to_merge": 1,
207+
"max_entries_to_merge": 5,
208+
"min_entries_to_merge_wait_minutes": 5,
209+
"grouping_strategy": "ALLGREEN",
210+
"check_response_timeout_minutes": 120
211+
}},
212+
{"type": "non_fast_forward"},
213+
{"type": "deletion"}
214+
]
215+
}'
94216
```
95217

96218
## Common Rationalizations
97219

98220
| Rationalization | Reality |
99221
|---|---|
100222
| "Release gate is red, so publish is broken." | Different layer. Publish may be healthy while promotion is blocked. |
101-
| "Let's just add more checks to the promotion PR." | That slows humans and duplicates the real stable gate. |
102-
| "`action_required` means rerun the same gate." | Usually it means read the gate condition first. |
223+
| "Let's just add more checks to the promotion PR." | That slows the queue and duplicates the real stable gate. |
224+
| "`action_required` means rerun the same gate." | It means the org is blocking bot PR runs. Only the merge queue bypasses this. |
225+
| "`use_merge_queue` only needs to be true for schedule/dispatch." | Wrong. It must always be true. The bot approval block applies to push events too. |
226+
| "Adding a required review adds safety." | It permanently blocks every automated promotion PR. |
103227
| "This reusable caller only needs job-level permissions." | Wrong often enough to deserve a scar. Check top-level caller permissions first. |
104228

105229
## Red Flags
106230

231+
- `use_merge_queue` is conditional on `github.event_name`
232+
- `required_approving_review_count` is greater than 0 in ruleset or classic protection
233+
- The ruleset is missing the `merge_queue` rule type
107234
- editing stable-promotion logic while the real failure is earlier publish plumbing
108235
- adding full e2e to the promotion PR path
109-
- rerunning arbitrary nearby workflows instead of the owning stage
110-
- ignoring `auto/promote-testing-to-main` PR state and debugging the wrong branch
236+
- rerunning `action_required` workflow runs — they cannot be rerun, they are bot-blocked
111237

112238
## Verification
113239

114-
- [ ] You identified the exact promotion stage that is failing
115-
- [ ] You checked open promotion PR state before editing YAML
116-
- [ ] Reusable caller permissions were validated if the gate did not start
117-
- [ ] You did not collapse publish, promotion, and stable release into one mental model
118-
- [ ] Any change preserves the factory's intended human approval gates
240+
- [ ] `use_merge_queue: true` unconditionally in `promote-testing-to-main.yml`
241+
- [ ] Ruleset `main-merge-queue-no-review` exists and has `merge_queue` rule
242+
- [ ] Classic branch protection `required_approving_review_count` is 0
243+
- [ ] `run_e2e: false` unchanged in the caller
244+
- [ ] Promotion PR enqueued and merged without any human interaction
119245
- [ ] Weekly cadence remains Tuesday `04:00 UTC`
120-
- [ ] `workflow_dispatch` still supports the same enqueue path as the scheduled run
121-
- [ ] `run_e2e: false` stayed unchanged in the caller
246+
- [ ] You did not collapse publish, promotion, and stable release into one mental model
122247

123248
## Lessons Learned
124249

125-
### release/blocked after CI-only push to testing is expected (2026-06-22)
250+
### release/blocked after CI-only push to testing is expected
126251

127252
When a paths-ignored push (e.g. `.github/workflows/**` change) advances the `testing`
128253
HEAD, the promote gate runs against the new SHA and finds no CI results for it.
129254
The gate correctly sets `release/blocked` — the SHA has never been built.
130255

131-
**This is not a pipeline failure.** Do not re-trigger or debug. The resolution is:
132-
133-
1. The BST build for the prior SHA (triggered by the image-affecting push) completes.
256+
This is not a pipeline failure. The resolution is automatic:
257+
1. The BST build for the prior SHA completes.
134258
2. `publish.yml` fires → `:testing` updated.
135-
3. Tuesday 04:00 UTC schedule re-runs `promote-testing-to-main` → gate passes → auto-merge fires.
259+
3. Next promote run re-evaluates → gate passes → merge queue fires.
136260

137-
**Factory pattern:** Enable auto-merge on the promotion PR immediately, even while
138-
`release/blocked`. The merge queue waits for the gate to clear — no human follow-up needed.
261+
### One BST build at a time — cancel everything before starting a new build
139262

140-
```bash
141-
gh pr merge 970 --repo projectbluefin/dakota --squash --auto
142-
```
143-
144-
### One BST build at a time — cancel everything before starting a new build (2026-06-22)
145-
146-
**One BST build at a time.** Before triggering or landing any change that starts a new
147-
BST build, cancel ALL other in-progress BST jobs — including cache-warm runs.
148-
149-
Concurrent BST builds compete for the same `ubuntu-24.04` runners and the same remote
150-
CAS write bandwidth. Running two builds simultaneously does not halve the time — it
151-
more than doubles it, and cache-warm runs will hit the 6h job timeout with
152-
`Cached elements after warm: 0`, wasting the entire run.
263+
Before triggering or landing any change that starts a new BST build, cancel ALL
264+
in-progress BST jobs — including cache-warm runs.
153265

154266
```bash
155-
# List all in-progress runs (cache-warm, stale builds, anything)
156267
gh run list --repo projectbluefin/dakota --json databaseId,status,name \
157268
| python3 -c "import json,sys; [print(r['databaseId'], r['name']) for r in json.load(sys.stdin) if r['status'] in ('in_progress','queued','pending')]"
158-
159-
# Cancel each one before proceeding
160269
gh run cancel <run-id> --repo projectbluefin/dakota
161270
```
162271

163-
**Cache-warm runs are NOT exempt.** "Its progress is additive" is the rationalization
164-
that causes the failure. A cache-warm run competing with a real build starves both.
165-
Cancel the warm run, let the real build finish, then re-trigger cache-warm separately.
272+
Cache-warm runs are NOT exempt. Cancel everything, let one build finish, then re-trigger warm separately.

0 commit comments

Comments
 (0)