Skip to content
Merged
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
18 changes: 7 additions & 11 deletions .github/workflows/create-lts-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,13 @@ jobs:
if: steps.diff.outputs.has_diff == 'true'
id: commits
run: |
# Find the most-recent commit on main whose tree hash matches the current lts tree.
# This is the anchor point from which we show only genuinely new commits, even after
# squash-merge promotions (which lose individual commit provenance in lts history).
LTS_TREE=$(git rev-parse origin/lts^{tree})
ANCHOR=$(git log origin/main --format="%H %T" --max-count=500 \
| awk -v t="$LTS_TREE" '$2==t{print $1; exit}')
# Show commits on main that are not reachable from lts.
# With regular-merge promotions the merge base advances automatically,
# so this list contains only genuinely new commits.
LIST=$(git log origin/lts..origin/main --oneline)

if [ -n "$ANCHOR" ]; then
LIST=$(git log "${ANCHOR}..origin/main" --oneline)
else
# Fallback when the tree match isn't in recent history (e.g., first ever promotion).
if [ -z "$LIST" ]; then
# Fallback when the commit graph can't resolve (e.g., first ever promotion).
LIST=$(git diff --name-status origin/lts origin/main)
fi

Comment on lines +51 to 55
Expand All @@ -70,7 +66,7 @@ jobs:
COMMIT_LIST: ${{ steps.commits.outputs.list }}
run: |
# Build body with printf so commit messages containing quotes are safe
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}")
BODY=$(printf '## Commits pending promotion to `lts`\n\n%s\n\n---\n_**Merge this PR** (Create a merge commit) to promote. Do NOT squash — squash-merge breaks the merge base and causes PR bloat. The PR body updates automatically as `main` advances._\n' "${COMMIT_LIST}")

EXISTING=$(gh pr list \
--base lts \
Expand Down
16 changes: 9 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ This section is the authoritative reference for all CI/CD behavior. Read it comp
| `build-dx-hwe.yml` | Caller — builds `bluefin-dx` with HWE kernel |
| `reusable-build-image.yml` | Reusable workflow — all 5 callers invoke this |
| `scheduled-lts-release.yml` | Dispatcher — owns the weekly Tuesday production release |
| `create-lts-pr.yml` | Opens a draft PR from `main` → `lts` when content differs; maintainer squash-merges as approval gate |
| `create-lts-pr.yml` | Opens a draft PR from `main` → `lts` when content differs; maintainer merges as approval gate |
| `generate-release.yml` | Creates a GitHub Release when `build-gdx.yml` completes on `lts` |

### Two Branches, Two Tag Namespaces
Expand All @@ -139,18 +139,20 @@ Promotion and production release are **intentionally decoupled**. There are two

**Phase 1 — Promotion (human-gated via PR):**
1. Every push to `main` triggers `create-lts-pr.yml`
2. The workflow checks `git diff --quiet origin/lts origin/main` (content diff, not commit graph — survives squash-merges)
3. If content differs: a draft PR from `main` → `lts` is created (or the existing one is updated). The PR body lists only the commits since the last promotion by anchoring to the `main` commit whose tree hash matches the current `lts` tree — this survives squash-merge history and prevents the list from bloating.
4. A maintainer reviews and **squash-merges** the PR — this is the human approval gate
5. The squash-merge triggers a `push` event on `lts` — all 5 build workflows run as **validation builds** (`publish=false`). No images are published.
2. The workflow checks `git diff --quiet origin/lts origin/main` (content diff, not commit graph)
3. If content differs: a draft PR from `main` → `lts` is created (or the existing one is updated)
4. A maintainer reviews and **merges** the PR ("Create a merge commit") — this is the human approval gate
5. The merge triggers a `push` event on `lts` — all 5 build workflows run as **validation builds** (`publish=false`). No images are published.

**NEVER squash-merge promotion PRs.** Squash-merge creates orphan commits that permanently break the merge base between `main` and `lts`, causing every future promotion PR to accumulate all historical commits in its diff. Regular merge preserves the merge base and keeps future PRs clean.

**Phase 2 — Production release (automated or manual publishing):**
1. `scheduled-lts-release.yml` fires at `0 6 * * 2` (Tuesday 6am UTC), OR a maintainer manually triggers it
2. It dispatches all 5 build workflows via `gh workflow run --ref lts`
3. Those are `workflow_dispatch` events on `lts` → `publish=true` → production tags pushed
4. After `build-gdx.yml` completes on `lts`, `generate-release.yml` creates a GitHub Release

**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.
**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 merge as the approval step.

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

Expand Down Expand Up @@ -266,7 +268,7 @@ If you see `schedule:` in any of the 5 build callers, remove it entirely. Do not
- `build-regular-hwe.yml` — HWE kernel variant of `bluefin`
- `build-dx-hwe.yml` — HWE kernel variant of `bluefin-dx`
- `scheduled-lts-release.yml` — Weekly production release dispatcher (sole owner of Tuesday builds)
- `create-lts-pr.yml` — Opens a draft PR from `main` → `lts` when content differs; maintainer squash-merges as approval gate
- `create-lts-pr.yml` — Opens a draft PR from `main` → `lts` when content differs; maintainer merges (not squash) as approval gate
- `generate-release.yml` — Creates GitHub Release after successful GDX build on `lts`

## Validation Scenarios
Expand Down
Loading