Skip to content

fix(ci): replace PR promotion with squash push in promote-to-lts#1177

Merged
castrojo merged 2 commits intoublue-os:mainfrom
castrojo:fix/squash-promote-to-lts
Mar 10, 2026
Merged

fix(ci): replace PR promotion with squash push in promote-to-lts#1177
castrojo merged 2 commits intoublue-os:mainfrom
castrojo:fix/squash-promote-to-lts

Conversation

@castrojo
Copy link
Collaborator

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 ltsmain, 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 checkgit 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: writecontents: 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.

- Replace gh pr create approach with direct squash merge push
- Add pre-flight check: fail if lts has commits not in main
- Use git merge --squash + single commit to keep lts history linear
- No merge commits means no embedded cross-branch history pollution
- workflow_dispatch trigger remains as the human approval gate
- Add NEVER commit directly to lts rule to AGENTS.md
- Update promote-to-lts.yml workflow description in AGENTS.md

The previous PR-based approach created merge commits that embedded
main's full history (including old circular lts→main references from
ffa30fe) into lts on every promotion. Each promotion deepened the
diamond graph. Squash merge creates one flat commit per promotion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@castrojo castrojo requested a review from tulilirockz as a code owner March 10, 2026 02:30
Copilot AI review requested due to automatic review settings March 10, 2026 02:30
@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Mar 10, 2026
@dosubot dosubot bot added kind/github-action Anything having to do with GHA and automation promotion This PR is a candidate to promote to `lts`. labels Mar 10, 2026
@dosubot
Copy link

dosubot bot commented Mar 10, 2026

Documentation Updates

4 document(s) were updated by changes in this PR:

Bluefin Newsletter 2.0
View Changes
@@ -1,7 +1,7 @@
 ## Recent Completed Work
 | Date Completed | Task | Overview | Impact |
 |---|---|---|---|
-| 2026-03-02 | Prevent Branch Pollution with Manual LTS Promotion | A comprehensive three-layer defense was implemented to prevent branch pollution. **Layer 1:** The automated Pull app (`.github/pull.yml`) was removed and replaced with a manual `.github/workflows/promote-to-lts.yml` workflow triggered via workflow_dispatch, requiring manual operator approval for promotions. **Layer 2:** Renovate configuration updated with `baseBranchPatterns: ["main"]` to restrict automated PRs to the main branch only, preventing dependency updates on lts. **Layer 3:** Added `lts` to push triggers in 5 build workflows (build-dx-hwe.yml, build-dx.yml, build-gdx.yml, build-regular-hwe.yml, build-regular.yml) to enable validation builds that catch accidental merges. | Protects production lts branch from automated updates and prevents reverse merges. Ensures controlled, deliberate promotions from main to lts via explicit human action. Validation builds provide safety net against accidental merges without publishing. [Details](https://github.com/ublue-os/bluefin-lts/pull/1152) |
+| 2026-03-02 | Prevent Branch Pollution with Manual LTS Promotion | A comprehensive three-layer defense was implemented to prevent branch pollution caused by merge commits and circular history. **Layer 1:** The automated Pull app (`.github/pull.yml`) was removed and replaced with a manual `.github/workflows/promote-to-lts.yml` workflow triggered via workflow_dispatch. The workflow performs a pre-flight divergence check that blocks promotion if lts has commits not in main, then performs a squash push from main to lts after validation. All changes must land in main first; direct commits to lts are prohibited. This replaced the previous PR-based approach that created merge commits and polluted the git tree. **Layer 2:** Renovate configuration updated with `baseBranchPatterns: ["main"]` to restrict automated PRs to the main branch only, preventing dependency updates on lts. **Layer 3:** Added `lts` to push triggers in 5 build workflows (build-dx-hwe.yml, build-dx.yml, build-gdx.yml, build-regular-hwe.yml, build-regular.yml) to enable validation builds that catch accidental merges. | Protects production lts branch from automated updates and git tree pollution. Ensures controlled, deliberate promotions from main to lts via explicit human action with squash merge strategy for linear history. Validation builds provide safety net against accidental merges without publishing. [Details](https://github.com/ublue-os/bluefin-lts/pull/1152) |
 | 2026-02-12 | Simplify Renovate Automerge Configuration | Renovate automerge configuration refactored from an allowlist approach to a universal pattern-based approach. All digest/pin/pinDigest updates across all managers (Dockerfile, GitHub Actions, regex/Justfile) are enabled for automerge, replacing fragmented per-container rules. GitHub Actions minor/patch version bumps also automerge. NVIDIA driver digests remain manual for compatibility review. | Reduces manual PR review for all dependency digest updates with simpler, more comprehensive automation rules. Improves security posture and reduces maintenance overhead. [Details](https://github.com/ublue-os/bluefin-lts/pull/1105) |
 | 2026-01-31 | Add ibus-chewing to Bluefin LTS for zh_TW | The ibus-chewing input method is now included in Bluefin LTS, matching the default for the zh_TW locale in Bluefin Stable. This resolves issues where Chewing was configured but not available when installing from older ISOs, and improves the Traditional Chinese typing experience out of the box. | Ensures consistent and functional Traditional Chinese input for zh_TW users on both Stable and LTS. Reduces manual configuration and improves i18n parity. [Details](https://github.com/ublue-os/bluefin-lts/pull/1076) |
 | 2025-12-16 | Reintroduce Renovate Automation | The Renovate dependency automation was restored. The `.github/renovate.json5` configuration was updated to include `ghcr.io/projectbluefin/common` in the automerge dependencies, ensuring that updates to this key dependency are now automatically merged. | Keeps dependencies up to date with less manual intervention, improving security and reliability. [Details](https://github.com/ublue-os/bluefin/pull/3853) |
@@ -24,7 +24,7 @@
 ## Summary Table
 | Date       | Change Summary |
 |------------|---------------|
-| 2026-03-02 | Renovate restricted to main branch; Pull app replaced with manual promotion workflow; validation builds added to lts branch |
+| 2026-03-02 | Renovate restricted to main branch; Pull app replaced with squash-push promotion workflow with pre-flight divergence check; validation builds added to lts branch |
 | 2026-02-12 | Renovate automerge refactored from allowlist to universal pattern-based rules covering all digest/pin updates |
 | 2026-01-31 | ibus-chewing input method added to Bluefin LTS for zh_TW locale parity |
 | 2025-12-16 | Renovate automation restored for `ghcr.io/projectbluefin/common` |
Bluefin OS
View Changes
@@ -101,14 +101,16 @@
 
 **Promotion Process:**
 
-Changes flow from `main` to `lts` via the `.github/workflows/promote-to-lts.yml` workflow, which is manually triggered via `workflow_dispatch`. The workflow creates a pull request directly using `gh pr create --base lts --head main`, allowing maintainers to review and approve the promotion. This simplified approach replaced the previous checkout+merge+intermediate-branch method and requires only `pull-requests: write` and `issues: write` permissions (no longer needs `contents: write`).
+Changes flow from `main` to `lts` via the `.github/workflows/promote-to-lts.yml` workflow, which is manually triggered via `workflow_dispatch`. The workflow performs a direct squash push from main to lts after running pre-flight divergence checks. The workflow includes a divergence check that blocks promotion if the lts branch contains commits not present in main, preventing circular history and git tree pollution. This approach requires `contents: write` permission to push directly to the lts branch.
+
+All changes, including emergency CI hotfixes, must land in main first before promotion to lts—direct commits to lts are not allowed. Triggering `workflow_dispatch` serves as the human approval gate for promotion.
 
 **Build Triggers:**
 
 All LTS build workflows (build-dx.yml, build-dx-hwe.yml, build-gdx.yml, build-regular.yml, build-regular-hwe.yml) trigger on both `main` and `lts` branches:
 
 - **Builds on `main`**: Triggered by pushes, pull requests, and merge groups for validation and testing
-- **Builds on `lts`**: Triggered by pushes (including merged promotion PRs) for production releases. Weekly scheduled builds (Sundays at 2 AM UTC) are centralized through a dispatcher workflow (`scheduled-lts-release.yml`), with individual workflow `schedule:` triggers removed to prevent duplicate builds
+- **Builds on `lts`**: Triggered by pushes (including automatic squash pushes from promotion) for production releases. Weekly scheduled builds (Sundays at 2 AM UTC) are centralized through a dispatcher workflow (`scheduled-lts-release.yml`), with individual workflow `schedule:` triggers removed to prevent duplicate builds
 
 **Dependency Management:**
 
@@ -180,7 +182,7 @@
 Build workflows trigger on multiple events to support both development and production workflows:
 
 - **main branch**: Builds triggered by push events, pull requests, and merge groups for validation and testing
-- **lts branch**: Builds triggered by push events (including merged promotion PRs) for production releases. Weekly scheduled builds (Sundays at 2 AM UTC) are centralized through a dispatcher workflow (`scheduled-lts-release.yml`), with individual workflow `schedule:` triggers removed to prevent duplicate builds
+- **lts branch**: Builds triggered by push events (including automatic squash pushes from promotion) for production releases. Weekly scheduled builds (Sundays at 2 AM UTC) are centralized through a dispatcher workflow (`scheduled-lts-release.yml`), with individual workflow `schedule:` triggers removed to prevent duplicate builds
 
 All LTS variant build workflows (build-dx.yml, build-dx-hwe.yml, build-gdx.yml, build-regular.yml, build-regular-hwe.yml) trigger on both branches to ensure changes are validated before promotion to production.
 
Kernel Version Management
View Changes
@@ -165,6 +165,16 @@
 
 - **main branch**: [Continuous testing builds published to `:lts-testing` with every push](https://github.com/ublue-os/bluefin-lts/pull/1138)
 - **lts branch**: [Production releases only on scheduled runs or manual workflow_dispatch](https://github.com/ublue-os/bluefin-lts/pull/1138)
+
+**Promotion Workflow:**
+
+[The promote-to-lts.yml workflow performs a squash push from main → lts with pre-flight divergence checks](https://github.com/ublue-os/bluefin-lts/pull/1177). The workflow:
+
+1. Runs a pre-flight check that blocks promotion if lts has diverged from main
+2. Performs a `git merge --squash origin/main` to collapse all pending changes into a single staged changeset
+3. Pushes one clean commit to lts per promotion event, maintaining linear history
+
+[The workflow requires `contents: write` permissions](https://github.com/ublue-os/bluefin-lts/pull/1177) and uses `workflow_dispatch` as the human approval gate—a maintainer must explicitly trigger promotion. **Never commit directly to `lts`**: all changes including CI hotfixes must land in main first, preventing circular history and git tree pollution.
 
 **Scheduling Architecture:**
 
@@ -292,7 +302,7 @@
 | [build_files/post/dual-sign.sh](https://github.com/ublue-os/akmods/blob/c24805395e7e057130ef94a099920f554fa0c1ae/build_files/post/dual-sign.sh) | Module dual-signature implementation | ublue-os/akmods |
 | [build_files/prep/dual-sign-check.sh](https://github.com/ublue-os/akmods/blob/c24805395e7e057130ef94a099920f554fa0c1ae/build_files/prep/dual-sign-check.sh) | Module signature verification | ublue-os/akmods |
 | [Containerfile.in](https://github.com/ublue-os/akmods/blob/c24805395e7e057130ef94a099920f554fa0c1ae/Containerfile.in) | Multi-stage akmods build container | ublue-os/akmods |
-| [.github/workflows/promote-to-lts.yml](https://github.com/ublue-os/bluefin-lts/blob/main/.github/workflows/promote-to-lts.yml) | LTS promotion workflow | ublue-os/bluefin-lts |
+| [.github/workflows/promote-to-lts.yml](https://github.com/ublue-os/bluefin-lts/blob/main/.github/workflows/promote-to-lts.yml) | Performs squash push from main → lts with pre-flight divergence checks | ublue-os/bluefin-lts |
 | [.github/workflows/scheduled-lts-release.yml](https://github.com/ublue-os/bluefin-lts/blob/main/.github/workflows/scheduled-lts-release.yml) | Weekly LTS release dispatcher | ublue-os/bluefin-lts |
 
 ## Related Topics
Universal Blue Build and Update System
View Changes
@@ -53,7 +53,7 @@
 - Manual `workflow_dispatch` triggers (for emergency releases)
 - Individual build workflows no longer have `schedule:` triggers—the centralized dispatcher eliminates 10 duplicate no-op builds that previously ran on `main` every Tuesday alongside the 5 dispatcher runs on `lts`, working around GitHub Actions limitation that scheduled triggers always run on the default branch
 
-This architecture prevents accidental production tag publishing while maintaining validation builds on merges. The validation builds on `lts` push events provide a third layer of defense against branch pollution by verifying code integrity when promotion PRs merge.
+This architecture prevents accidental production tag publishing while maintaining validation builds on merges. The validation builds on `lts` push events provide a third layer of defense against branch pollution by verifying code integrity when promotions complete.
 
 **Dispatcher Workflow Architecture:**
 
@@ -65,15 +65,16 @@
 
 **Layer 1: Manual Promotion Workflow** - A manual `promote-to-lts.yml` workflow enables explicit promotion of tested changes from `main` to `lts`. The workflow:
 - Requires manual trigger via GitHub Actions UI (`workflow_dispatch`)
-- Uses `gh pr create --base lts --head main` to directly create promotion PRs without intermediate branches
-- Opens pull request with documentation warning against reverse merges
-- Requires operator review and approval before merging
-- Only requires `pull-requests: write` and `issues: write` permissions (no `contents: write` needed)
+- Performs a direct squash push from `main` to `lts` (no intermediate branches or PRs)
+- Includes a pre-flight divergence check that blocks promotion if `lts` has commits not reachable from `main`
+- Automatically performs the squash push after pre-flight checks pass
+- Requires `contents: write` permission for direct push to `lts` branch
 - Replaces the previous automatic Pull app (`.github/pull.yml` has been deleted)
+- **CRITICAL RULE**: NEVER commit directly to `lts` — all changes must land in `main` first
 
 **Layer 2: Renovate Restriction** - Renovate configuration restricts dependency updates to target only the `main` branch via `baseBranchPatterns: ["main"]`. This prevents automated dependency PRs from targeting the `lts` branch, ensuring the production branch receives updates only through manual promotion.
 
-**Layer 3: Validation Build Triggers** - All 5 build workflows (build-dx-hwe.yml, build-dx.yml, build-gdx.yml, build-regular-hwe.yml, build-regular.yml) include `lts` in their push triggers. When promotion PRs merge to `lts`, validation builds execute to verify code integrity, but these builds do NOT publish production tags. Publishing only occurs via `workflow_dispatch` events (weekly dispatcher or manual triggers).
+**Layer 3: Validation Build Triggers** - All 5 build workflows (build-dx-hwe.yml, build-dx.yml, build-gdx.yml, build-regular-hwe.yml, build-regular.yml) include `lts` in their push triggers. When promotions push to `lts`, validation builds execute to verify code integrity, but these builds do NOT publish production tags. Publishing only occurs via `workflow_dispatch` events (weekly dispatcher or manual triggers).
 
 This three-layer architecture prevents branch pollution incidents where AI agents or automated tools merge `lts` → `main` (wrong direction) when detecting divergence in `image-versions.yaml` digests. Such divergence is intentional—`main` has newer versions for testing while `lts` maintains stable production versions.
 

How did I do? Any feedback?  Join Discord

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the mainlts promotion mechanism to avoid merge-commit history pollution on lts by replacing PR-based promotion with a controlled squash-merge-and-push workflow, plus a divergence guard.

Changes:

  • Replace promote-to-lts.yml PR creation with: checkout lts → pre-flight divergence check → git merge --squash origin/main → commit → push to lts.
  • Adjust workflow permissions from PR/issue write to contents: write for direct branch updates.
  • Update AGENTS.md to document the new promotion flow and add a “NEVER commit directly to lts” rule.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
AGENTS.md Documents squash-push promotion flow and adds guidance to prevent lts divergence.
.github/workflows/promote-to-lts.yml Implements pre-flight divergence check and squash-push promotion into lts without PRs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +60
run: git merge --squash origin/main

- name: Commit promotion
run: |
git commit \
-m "${{ inputs.commit_title }}" \
-m "${{ inputs.commit_body }}"

- name: Push to lts
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If origin/main is already fully promoted into lts, git merge --squash origin/main will leave nothing staged and the subsequent git commit will fail with "nothing to commit". Decide on the intended behavior for a no-op promotion (e.g., detect an empty index after the squash merge and exit 0 with a clear message, or exit 1 with an explicit "nothing to promote" error) to avoid a confusing failure mode.

Suggested change
run: git merge --squash origin/main
- name: Commit promotion
run: |
git commit \
-m "${{ inputs.commit_title }}" \
-m "${{ inputs.commit_body }}"
- name: Push to lts
id: squash
run: |
set -e
git merge --squash origin/main
if git diff --cached --quiet; then
echo "No changes to promote: origin/main is already fully merged into lts."
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "Prepared squash merge of origin/main into lts."
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Commit promotion
if: steps.squash.outputs.has_changes == 'true'
run: |
git commit \
-m "${{ inputs.commit_title }}" \
-m "${{ inputs.commit_body }}"
- name: Push to lts
if: steps.squash.outputs.has_changes == 'true'

Copilot uses AI. Check for mistakes.
- `build-dx-hwe.yml` — HWE kernel variant of `bluefin-dx`
- `scheduled-lts-release.yml` — Weekly production release dispatcher (sole owner of Sunday builds)
- `promote-to-lts.yml` — Opens a one-way `main` `lts` promotion PR
- `promote-to-lts.yml` — Squash-pushes `main` into `lts` (with pre-flight divergence check)
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENTS.md still describes promote-to-lts.yml as "Creates a PR to merge mainlts" in the earlier "Workflow Files and Their Roles" table, but this PR changes the workflow to squash-push directly. Please update that table entry as well so the documentation is internally consistent.

Copilot uses AI. Check for mistakes.
- Guard commit/push steps on has_changes output to avoid 'nothing to
  commit' failure when main is already fully merged into lts
- Fix promote-to-lts.yml table row in AGENTS.md workflow roles table

Addresses Copilot review feedback on PR ublue-os#1177.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@castrojo castrojo enabled auto-merge March 10, 2026 11:26
@castrojo castrojo disabled auto-merge March 10, 2026 11:26
@castrojo castrojo merged commit ff85922 into ublue-os:main Mar 10, 2026
23 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/github-action Anything having to do with GHA and automation promotion This PR is a candidate to promote to `lts`. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants