You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ci(promote): replace push-based promotion with PR gate (ublue-os#1195)
Ok I think I got it this time, I can confirm this works via testing in
my personal repo. I am likely going to force this because we need the
action in the LTS branch anyway.
## Agent Notes follow:
## Solution
Replace with `create-lts-pr.yml`, a PR-gate workflow:
- Fires on every push to `main` (and `workflow_dispatch`)
- Uses `git diff --quiet` (content diff, not commit graph) to detect new
content — survives squash-merges without false positives
- Passes the commit list as a `COMMIT_LIST` env var and uses `printf` to
build the PR body, safely handling commit messages containing double
quotes (e.g. `Revert "..."`)
- Auto-creates a draft PR from `main` → `lts`, or updates the existing
one
- Maintainer squash-merges the PR as the human approval gate
- No pre-flight check: branch protection is the guard against direct
`lts` commits
Also fixes `AGENTS.md`: corrects the release schedule (cron `0 6 * * 2`
= Tuesday 6am UTC, not Sunday 2am UTC) and updates all references to the
old workflow.
## Testing
Tested on the `castrojo/bluefin-lts` fork:
- ✅ Workflow fires on push and creates draft PR correctly
- ✅ PR body auto-updates on subsequent pushes without creating duplicate
PRs
- ✅ Commit messages with double quotes handled safely (the `Revert
"..."` case)
- ✅ Force-push after squash: workflow correctly updates existing PR
Assisted-by: Claude Sonnet 4.6 via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
echo "No content difference between lts and main. Nothing to promote."
34
+
echo "has_diff=false" >> "$GITHUB_OUTPUT"
35
+
else
36
+
echo "has_diff=true" >> "$GITHUB_OUTPUT"
37
+
fi
38
+
39
+
- name: Build commit list
40
+
if: steps.diff.outputs.has_diff == 'true'
41
+
id: commits
42
+
run: |
43
+
LIST=$(git log origin/lts..origin/main --oneline)
44
+
{
45
+
echo "list<<EOF"
46
+
echo "$LIST"
47
+
echo "EOF"
48
+
} >> "$GITHUB_OUTPUT"
49
+
50
+
- name: Create or update promote PR
51
+
if: steps.diff.outputs.has_diff == 'true'
52
+
env:
53
+
GH_TOKEN: ${{ github.token }}
54
+
COMMIT_LIST: ${{ steps.commits.outputs.list }}
55
+
run: |
56
+
# Build body with printf so commit messages containing quotes are safe
57
+
BODY=$(printf '## Commits pending promotion to `lts`\n\n%s\n\n---\n_Squash-merge this PR to promote. The PR body updates automatically as `main` advances._\n' "${COMMIT_LIST}")
Copy file name to clipboardExpand all lines: AGENTS.md
+14-13Lines changed: 14 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -120,8 +120,8 @@ This section is the authoritative reference for all CI/CD behavior. Read it comp
120
120
|`build-regular-hwe.yml`| Caller — builds `bluefin` with HWE kernel |
121
121
|`build-dx-hwe.yml`| Caller — builds `bluefin-dx` with HWE kernel |
122
122
|`reusable-build-image.yml`| Reusable workflow — all 5 callers invoke this |
123
-
|`scheduled-lts-release.yml`| Dispatcher — owns the weekly Sunday production release |
124
-
|`promote-to-lts.yml`|Squash-pushes `main` → `lts`with pre-flight divergence check (see below)|
123
+
|`scheduled-lts-release.yml`| Dispatcher — owns the weekly Tuesday production release |
124
+
|`create-lts-pr.yml`|Opens a draft PR from `main` → `lts`when content differs; maintainer squash-merges as approval gate|
125
125
|`generate-release.yml`| Creates a GitHub Release when `build-gdx.yml` completes on `lts`|
126
126
127
127
### Two Branches, Two Tag Namespaces
@@ -137,23 +137,24 @@ This section is the authoritative reference for all CI/CD behavior. Read it comp
137
137
138
138
Promotion and production release are **intentionally decoupled**. There are two separate phases:
139
139
140
-
**Phase 1 — Promotion (manual, no publishing):**
141
-
1. A maintainer triggers `promote-to-lts.yml` via `workflow_dispatch`
142
-
2. The workflow runs a **pre-flight check**: fails immediately if `lts` has any commits not reachable from `main`, printing those commits with instructions to land them in `main` first.
143
-
3. The workflow performs a **squash merge** (`git merge --squash origin/main`) and pushes one clean commit to `lts`. There is no PR. Triggering `workflow_dispatch` is the human approval step.
144
-
4. The push triggers a `push` event on `lts` — all 5 build workflows run as **validation builds** (`publish=false`). No images are published. This confirms the promoted code builds cleanly on `lts` before the next production release.
140
+
**Phase 1 — Promotion (human-gated via PR):**
141
+
1. Every push to `main` triggers `create-lts-pr.yml`
142
+
2. The workflow checks `git diff --quiet origin/lts origin/main` (content diff, not commit graph — survives squash-merges)
143
+
3. If content differs: a draft PR from `main` → `lts` is created (or the existing one is updated with the latest commit list)
144
+
4. A maintainer reviews and **squash-merges** the PR — this is the human approval gate
145
+
5. The squash-merge triggers a `push` event on `lts` — all 5 build workflows run as **validation builds** (`publish=false`). No images are published.
145
146
146
147
**Phase 2 — Production release (automated or manual publishing):**
147
-
1.`scheduled-lts-release.yml` fires at `0 2 * * 0` (Sunday 2am UTC), OR a maintainer manually triggers it
148
+
1.`scheduled-lts-release.yml` fires at `0 6 * * 2` (Tuesday 6am UTC), OR a maintainer manually triggers it
148
149
2. It dispatches all 5 build workflows via `gh workflow run --ref lts`
149
150
3. Those are `workflow_dispatch` events on `lts` → `publish=true` → production tags pushed
150
151
4. After `build-gdx.yml` completes on `lts`, `generate-release.yml` creates a GitHub Release
151
152
152
-
**Why `promote-to-lts.yml` exists:** Automated tools (the old Pull app, AI agents) cannot distinguish merge direction — when they see `lts` is behind `main`, they attempt to "sync" and sometimes merge `lts` → `main`, polluting `main` with old production commits. The workflow enforces the correct direction by always targeting `lts` as the base.
153
+
**Why `create-lts-pr.yml` exists:** Automated tools (the old Pull app, AI agents) cannot distinguish merge direction — when they see `lts` is behind `main`, they attempt to "sync" and sometimes merge `lts` → `main`, polluting `main` with old production commits. The PR-gate workflow enforces the correct direction: `main` → `lts`only, with a human squash-merge as the approval step.
153
154
154
155
**NEVER merge `lts` into `main`.** The flow is always one-way: `main` → `lts`.
155
156
156
-
**NEVER commit directly to `lts`.** All changes — including CI hotfixes — must land in `main` first. Direct commits to `lts`create divergence that causes the pre-flight check to fail and blocks future promotions.
157
+
**NEVER commit directly to `lts`.** All changes — including CI hotfixes — must land in `main` first. Direct commits to `lts`will appear as phantom content in the PR diff and confuse reviewers.
157
158
158
159
### `publish` Input — How It Is Evaluated
159
160
@@ -252,7 +253,7 @@ When touching any condition in `reusable-build-image.yml`, use this reference:
252
253
253
254
### `schedule:` Triggers — Ownership Rule
254
255
255
-
**`scheduled-lts-release.yml` is the sole owner of Sunday 2am UTC production builds.**
256
+
**`scheduled-lts-release.yml` is the sole owner of Tuesday 6am UTC production builds.**
256
257
257
258
The 5 build caller workflows (`build-regular.yml`, `build-dx.yml`, `build-gdx.yml`, `build-regular-hwe.yml`, `build-dx-hwe.yml`) must NOT have `schedule:` triggers. Any `schedule:` event on those workflows fires on `main` (the default branch), evaluates `publish=false`, publishes nothing, and wastes runner time.
258
259
@@ -264,8 +265,8 @@ If you see `schedule:` in any of the 5 build callers, remove it entirely. Do not
0 commit comments