Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7316e19
chore(deps): update cgr.dev/chainguard/wolfi-base:latest docker diges…
ubot-7274[bot] Mar 10, 2026
13f9b46
chore(deps): update quay.io/centos-bootc/centos-bootc:c10s docker dig…
ubot-7274[bot] Mar 11, 2026
b23f809
chore(deps): update actions/download-artifact digest to 3e5f45b (#1183)
ubot-7274[bot] Mar 11, 2026
90132e8
chore(deps): update cgr.dev/chainguard/wolfi-base:latest docker diges…
ubot-7274[bot] Mar 12, 2026
8e1c75f
chore(deps): update ghcr.io/projectbluefin/common:latest docker diges…
ubot-7274[bot] Mar 12, 2026
6a0ad87
chore(deps): update cgr.dev/chainguard/wolfi-base:latest docker diges…
ubot-7274[bot] Mar 12, 2026
4e13431
chore(deps): update ghcr.io/projectbluefin/common:latest docker diges…
ubot-7274[bot] Mar 13, 2026
1339bc4
chore(deps): update cgr.dev/chainguard/wolfi-base:latest docker diges…
ubot-7274[bot] Mar 15, 2026
914432d
chore(deps): update ghcr.io/ublue-os/brew:latest docker digest to fef…
ubot-7274[bot] Mar 15, 2026
bc65f2a
chore(deps): update system_files/usr/share/gnome-shell/extensions/sea…
ubot-7274[bot] Mar 15, 2026
24765e4
feat(GNOME) : gnome 49 backport (#1187)
hanthor Mar 15, 2026
18bb989
chore(deps): update quay.io/centos-bootc/centos-bootc:c10s docker dig…
ubot-7274[bot] Mar 16, 2026
1ff0c7e
Revert "feat(GNOME) : gnome 49 backport" (#1192)
hanthor Mar 16, 2026
aa14da4
chore(deps): update quay.io/centos-bootc/centos-bootc:c10s docker dig…
ubot-7274[bot] Mar 16, 2026
1658526
chore(deps): update system_files/usr/share/gnome-shell/extensions/sea…
ubot-7274[bot] Mar 17, 2026
dd4152f
chore(deps): update quay.io/centos-bootc/centos-bootc:c10s docker dig…
ubot-7274[bot] Mar 17, 2026
6462f99
ci(promote): replace push-based promotion with PR gate (#1195)
castrojo Mar 17, 2026
a764cfc
fix(ci): use tree-hash anchor for accurate promotion commit list (#1197)
castrojo Mar 17, 2026
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
94 changes: 94 additions & 0 deletions .github/workflows/create-lts-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Create LTS Promotion PR

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: create-lts-pr
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
create-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: main
fetch-depth: 0

- name: Fetch lts
run: git fetch origin lts

- name: Check content diff
id: diff
run: |
if git diff --quiet origin/lts origin/main; then
echo "No content difference between lts and main. Nothing to promote."
echo "has_diff=false" >> "$GITHUB_OUTPUT"
elif [ -z "$(git log origin/lts..origin/main --oneline)" ]; then
echo "lts is ahead of or diverged from main with no commits to promote. Nothing to promote."
echo "has_diff=false" >> "$GITHUB_OUTPUT"
else
echo "has_diff=true" >> "$GITHUB_OUTPUT"
fi

- name: Build commit list
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}')

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).
LIST=$(git diff --name-status origin/lts origin/main)
fi

{
echo "list<<EOF"
echo "$LIST"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Create or update promote PR
if: steps.diff.outputs.has_diff == 'true'
env:
GH_TOKEN: ${{ github.token }}
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}")

EXISTING=$(gh pr list \
--base lts \
--head main \
--state open \
--json number \
--jq '.[0].number' \
2>/dev/null || echo "")

if [ -n "$EXISTING" ]; then
echo "Updating existing promote PR #${EXISTING}"
printf '%s\n' "${BODY}" | gh pr edit "$EXISTING" --body-file -
else
echo "Creating new draft promote PR"
printf '%s\n' "${BODY}" | gh pr create \
--draft \
--base lts \
--head main \
--title "promote: main → lts" \
--body-file -
fi
71 changes: 0 additions & 71 deletions .github/workflows/promote-to-lts.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .github/workflows/reusable-build-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ jobs:
- generate_matrix
- build_push
container:
image: cgr.dev/chainguard/wolfi-base:latest@sha256:786c4d16fa02447c89409d4c0a0c0d3ff48f6886ab5e6350e95af62d876e2373
image: cgr.dev/chainguard/wolfi-base:latest@sha256:2a43204178a08b8c7f5e881c550bb52733364beff904ed36eeabe33cc656c749
options: --privileged --security-opt seccomp=unconfined
permissions:
contents: read
Expand Down Expand Up @@ -428,7 +428,7 @@ jobs:
- name: Fetch Build Outputs
if: ${{ inputs.publish }}
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: ${{ env.IMAGE_NAME }}-*
merge-multiple: true
Expand Down
27 changes: 14 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ This section is the authoritative reference for all CI/CD behavior. Read it comp
| `build-regular-hwe.yml` | Caller — builds `bluefin` with HWE kernel |
| `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 Sunday production release |
| `promote-to-lts.yml` | Squash-pushes `main``lts` with pre-flight divergence check (see below) |
| `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 |
| `generate-release.yml` | Creates a GitHub Release when `build-gdx.yml` completes on `lts` |

### Two Branches, Two Tag Namespaces
Expand All @@ -137,23 +137,24 @@ This section is the authoritative reference for all CI/CD behavior. Read it comp

Promotion and production release are **intentionally decoupled**. There are two separate phases:

**Phase 1 — Promotion (manual, no publishing):**
1. A maintainer triggers `promote-to-lts.yml` via `workflow_dispatch`
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.
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.
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.
**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.

**Phase 2 — Production release (automated or manual publishing):**
1. `scheduled-lts-release.yml` fires at `0 2 * * 0` (Sunday 2am UTC), OR a maintainer manually triggers it
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 `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.
**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.

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

**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.
**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.

### `publish` Input — How It Is Evaluated

Expand Down Expand Up @@ -252,7 +253,7 @@ When touching any condition in `reusable-build-image.yml`, use this reference:

### `schedule:` Triggers — Ownership Rule

**`scheduled-lts-release.yml` is the sole owner of Sunday 2am UTC production builds.**
**`scheduled-lts-release.yml` is the sole owner of Tuesday 6am UTC production builds.**

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.

Expand All @@ -264,8 +265,8 @@ If you see `schedule:` in any of the 5 build callers, remove it entirely. Do not
- `build-gdx.yml` — GPU/AI Developer Experience (`bluefin-gdx` image)
- `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 Sunday builds)
- `promote-to-lts.yml`Squash-pushes `main` into `lts` (with pre-flight divergence check)
- `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
- `generate-release.yml` — Creates GitHub Release after successful GDX build on `lts`

## Validation Scenarios
Expand Down
6 changes: 3 additions & 3 deletions image-versions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ images:
- name: centos-bootc
image: quay.io/centos-bootc/centos-bootc
tag: c10s
digest: sha256:226b06fa4104bed3547897f41d2a934bcc1ba8a5c587eab5c39d4a758c2d1c61
digest: sha256:7b1e3d109d928b296c39b9dd2c73ae337bb569537ce97eed8adb55c14c90c5a0
- name: common
image: ghcr.io/projectbluefin/common
tag: latest
digest: sha256:b9a75b68a14211b36389402564a2cf2f9369290027ecf5f05df2d5f9cf36450a
digest: sha256:9409d0c08bf76bdfef52812db61a68453b20b23b52042e810a447ada3c72c9c1
- name: brew
image: ghcr.io/ublue-os/brew
tag: latest
digest: sha256:2eca44f5b4b58b8271a625d61c2c063b7c8776f68d004ae67563e2a79450be9c
digest: sha256:fef8b4728cb042f6b69ad9be90a43095261703103fe6c0735c9d6f035065c052
Loading