diff --git a/.github/workflows/create-lts-pr.yml b/.github/workflows/create-lts-pr.yml new file mode 100644 index 00000000..1bc753fe --- /dev/null +++ b/.github/workflows/create-lts-pr.yml @@ -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<> "$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 diff --git a/.github/workflows/promote-to-lts.yml b/.github/workflows/promote-to-lts.yml deleted file mode 100644 index 8fb2c503..00000000 --- a/.github/workflows/promote-to-lts.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Promote Main to LTS - -on: - workflow_dispatch: - inputs: - commit_title: - description: 'Commit title for the squash promotion commit' - required: false - default: 'promote: main to lts' - commit_body: - description: 'Commit body (optional)' - required: false - default: | - Squash promotion of tested changes from `main` to `lts`. - -permissions: - contents: write - -jobs: - promote: - runs-on: ubuntu-latest - steps: - - name: Checkout lts - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - ref: lts - fetch-depth: 0 - token: ${{ github.token }} - - - name: Fetch main - run: git fetch origin main - - - name: Pre-flight check - run: | - UNIQUE=$(git rev-list origin/lts ^origin/main --count) - if [ "$UNIQUE" -gt 0 ]; then - echo "ERROR: lts has $UNIQUE commit(s) that are not in main:" - git log --oneline origin/lts ^origin/main - echo "" - echo "All changes must land in main before promoting to lts." - echo "Land the above commits in main first, then re-run this workflow." - exit 1 - fi - echo "Pre-flight passed: lts has no commits outside of main." - - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Squash merge main into lts - id: squash - run: | - 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 "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' - run: git push origin lts diff --git a/.github/workflows/reusable-build-image.yml b/.github/workflows/reusable-build-image.yml index 520b1dcd..27d4e349 100644 --- a/.github/workflows/reusable-build-image.yml +++ b/.github/workflows/reusable-build-image.yml @@ -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 @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 58207ff0..1a3b4c4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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 @@ -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. @@ -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 diff --git a/image-versions.yaml b/image-versions.yaml index ec9144a9..069c5c1f 100644 --- a/image-versions.yaml +++ b/image-versions.yaml @@ -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 \ No newline at end of file + digest: sha256:fef8b4728cb042f6b69ad9be90a43095261703103fe6c0735c9d6f035065c052 \ No newline at end of file diff --git a/system_files/usr/share/gnome-shell/extensions/search-light@icedman.github.com b/system_files/usr/share/gnome-shell/extensions/search-light@icedman.github.com index 2070cd42..4e93e0e3 160000 --- a/system_files/usr/share/gnome-shell/extensions/search-light@icedman.github.com +++ b/system_files/usr/share/gnome-shell/extensions/search-light@icedman.github.com @@ -1 +1 @@ -Subproject commit 2070cd42271eae5aebe64045ec9cbbe8a10b74e6 +Subproject commit 4e93e0e3e2fba8512dfd588177b7a6a2a71c9f1e