Skip to content

Commit ff85922

Browse files
castrojoCopilot
andauthored
fix(ci): replace PR promotion with squash push in promote-to-lts (#1177)
## Problem The `promote-to-lts.yml` workflow has caused repeated git tree pollution on `lts`. After digging through the full history, here is the root cause chain: 1. **Circular history reference**: `ffa30fe` (`Merge branch 'lts' into main`) accidentally merged `lts` → `main`, embedding lts's pull-bot merge commits inside main's graph. 2. **lts has diverged**: 4 commits exist in `lts` that aren't in `main` (old pull-bot merges + the `2a780d2` emergency CI sync). 3. **Default merge commit strategy**: The previous `gh pr create --head main` approach created a PR that, when merged with GitHub's default button, produced a merge commit with two parents. That commit dragged main's full cross-contaminated history into `lts` on every promotion, deepening the diamond graph each time. 4. **No pre-flight guard**: Nothing prevented the workflow from running when `lts` was already in a diverged state. ## Solution Replace the PR creation step with a direct **squash push**: 1. **Pre-flight check** — `git rev-list origin/lts ^origin/main --count`. If > 0, fail immediately and list the divergent commits with instructions to land them in `main` first. 2. **`git merge --squash origin/main`** — collapses all pending main changes into a single staged changeset. No merge commit. No embedded history. 3. **One clean commit** pushed to `lts` per promotion event → linear history. The `workflow_dispatch` trigger is the human approval gate — a maintainer must explicitly trigger the workflow to promote. ## Why not keep the PR? Previous attempts: | Approach | Why it failed | |---|---| | Old Pull bot | Created `[pull]` merge commits; also ran in reverse | | Intermediate branch + PR | Merge commit still polluted `lts` on merge | | `gh pr create --head main` (last fix) | PR merged with default strategy = merge commit | | Direct file sync `2a780d2` | Added lts-unique commits, deepening divergence | A squash push is deterministic — the workflow controls the merge strategy, not the human clicking the merge button. ## Changes - `.github/workflows/promote-to-lts.yml`: Replace `gh pr create` with checkout + pre-flight + squash merge + push. Swap `pull-requests: write` → `contents: write`. - `AGENTS.md`: Update promotion flow description; add **NEVER commit directly to `lts`** rule. ## ⚠️ Current state of lts `lts` currently has 4 commits not in `main`. The pre-flight check will **correctly block** the next promotion and print those commits. Resolution: the CI fixes from `2a780d2` are already in `main` as `6ec7dd5` — once the divergent commits are resolved and `git rev-list origin/lts ^origin/main` is empty, promotion will work cleanly. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a527168 commit ff85922

File tree

2 files changed

+63
-27
lines changed

2 files changed

+63
-27
lines changed

.github/workflows/promote-to-lts.yml

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,69 @@ name: Promote Main to LTS
33
on:
44
workflow_dispatch:
55
inputs:
6-
pr_title:
7-
description: 'Pull request title'
6+
commit_title:
7+
description: 'Commit title for the squash promotion commit'
88
required: false
9-
default: 'Promote main to lts'
10-
pr_body:
11-
description: 'Pull request body'
9+
default: 'promote: main to lts'
10+
commit_body:
11+
description: 'Commit body (optional)'
1212
required: false
1313
default: |
14-
## Summary
15-
Promotion of tested changes from `main` to `lts` production branch.
16-
17-
**IMPORTANT**: This PR should ONLY contain commits from `main` → `lts`. Never merge in the opposite direction.
14+
Squash promotion of tested changes from `main` to `lts`.
1815
1916
permissions:
20-
pull-requests: write
21-
issues: write
17+
contents: write
2218

2319
jobs:
24-
create-promotion-pr:
20+
promote:
2521
runs-on: ubuntu-latest
2622
steps:
27-
- name: Create Pull Request
28-
env:
29-
GH_TOKEN: ${{ github.token }}
23+
- name: Checkout lts
24+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
25+
with:
26+
ref: lts
27+
fetch-depth: 0
28+
token: ${{ github.token }}
29+
30+
- name: Fetch main
31+
run: git fetch origin main
32+
33+
- name: Pre-flight check
3034
run: |
31-
gh pr create \
32-
--repo ${{ github.repository }} \
33-
--base lts \
34-
--head main \
35-
--title "${{ inputs.pr_title }}" \
36-
--body "${{ inputs.pr_body }}" \
37-
--label "promotion"
35+
UNIQUE=$(git rev-list origin/lts ^origin/main --count)
36+
if [ "$UNIQUE" -gt 0 ]; then
37+
echo "ERROR: lts has $UNIQUE commit(s) that are not in main:"
38+
git log --oneline origin/lts ^origin/main
39+
echo ""
40+
echo "All changes must land in main before promoting to lts."
41+
echo "Land the above commits in main first, then re-run this workflow."
42+
exit 1
43+
fi
44+
echo "Pre-flight passed: lts has no commits outside of main."
45+
46+
- name: Configure Git
47+
run: |
48+
git config user.name "github-actions[bot]"
49+
git config user.email "github-actions[bot]@users.noreply.github.com"
50+
51+
- name: Squash merge main into lts
52+
id: squash
53+
run: |
54+
git merge --squash origin/main
55+
if git diff --cached --quiet; then
56+
echo "No changes to promote: origin/main is already fully merged into lts."
57+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
58+
else
59+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
60+
fi
61+
62+
- name: Commit promotion
63+
if: steps.squash.outputs.has_changes == 'true'
64+
run: |
65+
git commit \
66+
-m "${{ inputs.commit_title }}" \
67+
-m "${{ inputs.commit_body }}"
68+
69+
- name: Push to lts
70+
if: steps.squash.outputs.has_changes == 'true'
71+
run: git push origin lts

AGENTS.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ This section is the authoritative reference for all CI/CD behavior. Read it comp
121121
| `build-dx-hwe.yml` | Caller — builds `bluefin-dx` with HWE kernel |
122122
| `reusable-build-image.yml` | Reusable workflow — all 5 callers invoke this |
123123
| `scheduled-lts-release.yml` | Dispatcher — owns the weekly Sunday production release |
124-
| `promote-to-lts.yml` | Creates a PR to merge `main``lts` (see below) |
124+
| `promote-to-lts.yml` | Squash-pushes `main``lts` with pre-flight divergence check (see below) |
125125
| `generate-release.yml` | Creates a GitHub Release when `build-gdx.yml` completes on `lts` |
126126

127127
### Two Branches, Two Tag Namespaces
@@ -139,9 +139,9 @@ Promotion and production release are **intentionally decoupled**. There are two
139139

140140
**Phase 1 — Promotion (manual, no publishing):**
141141
1. A maintainer triggers `promote-to-lts.yml` via `workflow_dispatch`
142-
2. The workflow opens a PR from `main` targeting `lts` directly (no intermediate branch)
143-
3. A maintainer reviews and merges the PR
144-
4. The merge triggers a `push` event on `lts` — all 5 build workflows run as **validation builds** (`publish=false`). No images are published. This is intentional: it confirms that the merged code builds cleanly on `lts` before the next production release.
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.
145145

146146
**Phase 2 — Production release (automated or manual publishing):**
147147
1. `scheduled-lts-release.yml` fires at `0 2 * * 0` (Sunday 2am UTC), OR a maintainer manually triggers it
@@ -153,6 +153,8 @@ Promotion and production release are **intentionally decoupled**. There are two
153153

154154
**NEVER merge `lts` into `main`.** The flow is always one-way: `main``lts`.
155155

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+
156158
### `publish` Input — How It Is Evaluated
157159

158160
All 5 caller workflows pass the same `publish:` expression:
@@ -263,7 +265,7 @@ If you see `schedule:` in any of the 5 build callers, remove it entirely. Do not
263265
- `build-regular-hwe.yml` — HWE kernel variant of `bluefin`
264266
- `build-dx-hwe.yml` — HWE kernel variant of `bluefin-dx`
265267
- `scheduled-lts-release.yml` — Weekly production release dispatcher (sole owner of Sunday builds)
266-
- `promote-to-lts.yml`Opens a one-way `main` `lts` promotion PR
268+
- `promote-to-lts.yml`Squash-pushes `main` into `lts` (with pre-flight divergence check)
267269
- `generate-release.yml` — Creates GitHub Release after successful GDX build on `lts`
268270

269271
## Validation Scenarios

0 commit comments

Comments
 (0)