From e239a1903bcb16085250b17a6b82ed4fcde0c2c0 Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:13:26 -0500 Subject: [PATCH 01/19] chore(deps): update github artifact actions (major) (#1134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/download-artifact](https://redirect.github.com/actions/download-artifact) | action | major | `v7` → `v8` | | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | major | `v6` → `v7` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Release Notes
actions/download-artifact (actions/download-artifact) ### [`v8`](https://redirect.github.com/actions/download-artifact/compare/v7...v8) [Compare Source](https://redirect.github.com/actions/download-artifact/compare/v7...v8)
actions/upload-artifact (actions/upload-artifact) ### [`v7`](https://redirect.github.com/actions/upload-artifact/compare/v6...v7) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v6...v7)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [x] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- .github/workflows/generate-release.yml | 2 +- .github/workflows/reusable-build-image.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/generate-release.yml b/.github/workflows/generate-release.yml index ef85a33b..7d1d681e 100644 --- a/.github/workflows/generate-release.yml +++ b/.github/workflows/generate-release.yml @@ -101,7 +101,7 @@ jobs: --target lts - name: Upload changelog artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: changelog path: | diff --git a/.github/workflows/reusable-build-image.yml b/.github/workflows/reusable-build-image.yml index 374a4598..98435351 100644 --- a/.github/workflows/reusable-build-image.yml +++ b/.github/workflows/reusable-build-image.yml @@ -310,7 +310,7 @@ jobs: - name: Upload Output Artifacts if: ${{ inputs.publish }} - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: ${{ env.IMAGE_NAME }}-${{ matrix.platform }} retention-days: 1 @@ -430,7 +430,7 @@ jobs: - name: Fetch Build Outputs if: ${{ inputs.publish }} - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8 with: pattern: ${{ env.IMAGE_NAME }}-* merge-multiple: true From f7e94f53e2539037301a31b77b77bc3604c189de Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:13:37 -0500 Subject: [PATCH 02/19] chore(deps): update ghcr.io/projectbluefin/common:latest docker digest to b8fe93b (#1133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | ghcr.io/projectbluefin/common | digest | `5decea8` → `b8fe93b` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [x] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- image-versions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-versions.yaml b/image-versions.yaml index a20f1775..bb372024 100644 --- a/image-versions.yaml +++ b/image-versions.yaml @@ -6,7 +6,7 @@ images: - name: common image: ghcr.io/projectbluefin/common tag: latest - digest: sha256:5decea89ecaf8475ecec754842bdaba1efb46beb91de0b9d0e34c52440c57011 + digest: sha256:b8fe93b16674a547b4cf38493af19caa484d9575956fc3be04ca3d10faec23ff - name: brew image: ghcr.io/ublue-os/brew tag: latest From 0ba1ede183a6aae35c34860e62018ae04c634134 Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:13:44 -0500 Subject: [PATCH 03/19] chore(deps): update anchore/sbom-action digest to 17ae174 (#1132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [anchore/sbom-action](https://redirect.github.com/anchore/sbom-action) ([changelog](https://redirect.github.com/anchore/sbom-action/compare/28d71544de8eaf1b958d335707167c5f783590ad..17ae1740179002c89186b61233e0f892c3118b11)) | action | digest | `28d7154` → `17ae174` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [x] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- .github/workflows/reusable-build-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-build-image.yml b/.github/workflows/reusable-build-image.yml index 98435351..41af2a75 100644 --- a/.github/workflows/reusable-build-image.yml +++ b/.github/workflows/reusable-build-image.yml @@ -177,7 +177,7 @@ jobs: - name: Setup Syft id: setup-syft if: ${{ inputs.sbom && inputs.publish }} - uses: anchore/sbom-action/download-syft@28d71544de8eaf1b958d335707167c5f783590ad # v0 + uses: anchore/sbom-action/download-syft@17ae1740179002c89186b61233e0f892c3118b11 # v0 - name: Generate SBOM id: generate-sbom From b228dab8aaf56e85c0d3272e4ca751602c93d2b4 Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:13:56 -0500 Subject: [PATCH 04/19] chore(deps): update quay.io/centos-bootc/centos-bootc:c10s docker digest to 7dca424 (#1131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | quay.io/centos-bootc/centos-bootc | digest | `001a05c` → `7dca424` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [x] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- image-versions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-versions.yaml b/image-versions.yaml index bb372024..ee389466 100644 --- a/image-versions.yaml +++ b/image-versions.yaml @@ -2,7 +2,7 @@ images: - name: centos-bootc image: quay.io/centos-bootc/centos-bootc tag: c10s - digest: sha256:001a05c92f220c66515b0a0d1b95dc6e5ef76696e830bb8530e955709d9c16b7 + digest: sha256:7dca4245bb45147d34625493c4bb847762ae98ebc4a1ef950fb72ca1f097940e - name: common image: ghcr.io/projectbluefin/common tag: latest From 16aa2b3a9ce82bcc8b46f20880422aae7ad2164d Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Sun, 1 Mar 2026 19:16:41 -0500 Subject: [PATCH 05/19] fix(ci): restrict SBOM generation to lts branch only (#1140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR ensures SBOMs are only generated on the `lts` production branch, not on `main` branch or pull requests. ## Problem The `build-dx-hwe.yml` workflow had inconsistent SBOM generation logic compared to all other build workflows: - **build-dx-hwe.yml**: Generated SBOMs on main branch (incorrect) - **All other workflows**: Only generated SBOMs on lts branch (correct) ## Solution Aligned `build-dx-hwe.yml` SBOM logic with the other 4 workflows: ```yaml sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} ``` ## Impact After this change, SBOMs will **only** be generated when: - ✅ Event is NOT a pull request - ✅ Branch is `lts` (production branch) - ❌ Branch is `main` (testing branch) - **NO SBOMs** - ❌ Pull requests to any branch - **NO SBOMs** ## Testing - [ ] Syntax validation passes - [ ] Logic matches other workflows: - build-regular.yml ✅ - build-regular-hwe.yml ✅ - build-dx.yml ✅ - build-gdx.yml ✅ - build-dx-hwe.yml ⚠️ (fixed by this PR) --- .github/workflows/build-dx-hwe.yml | 9 +++++---- .github/workflows/build-dx.yml | 9 +++++---- .github/workflows/build-gdx.yml | 9 +++++---- .github/workflows/build-regular-hwe.yml | 9 +++++---- .github/workflows/build-regular.yml | 9 +++++---- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build-dx-hwe.yml b/.github/workflows/build-dx-hwe.yml index 33868421..17711d30 100644 --- a/.github/workflows/build-dx-hwe.yml +++ b/.github/workflows/build-dx-hwe.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -33,7 +34,7 @@ jobs: image-name: bluefin-dx flavor: dx kernel-pin: 6.17.12-200.fc42 - rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} - publish: ${{ github.event_name != 'pull_request' }} + rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} + publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} hwe: true diff --git a/.github/workflows/build-dx.yml b/.github/workflows/build-dx.yml index ecdbc959..fc336524 100644 --- a/.github/workflows/build-dx.yml +++ b/.github/workflows/build-dx.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -28,6 +29,6 @@ jobs: with: image-name: bluefin-dx flavor: dx - rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} - publish: ${{ github.event_name != 'pull_request' }} + rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + sbom: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} diff --git a/.github/workflows/build-gdx.yml b/.github/workflows/build-gdx.yml index ae6cb556..be61ce01 100644 --- a/.github/workflows/build-gdx.yml +++ b/.github/workflows/build-gdx.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -29,6 +30,6 @@ jobs: image-name: bluefin-gdx flavor: gdx kernel-pin: 6.17.12-200.fc42 - rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} - publish: ${{ github.event_name != 'pull_request' }} + rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + sbom: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} diff --git a/.github/workflows/build-regular-hwe.yml b/.github/workflows/build-regular-hwe.yml index 03a37381..9a78af76 100644 --- a/.github/workflows/build-regular-hwe.yml +++ b/.github/workflows/build-regular-hwe.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -32,8 +33,8 @@ jobs: with: image-name: bluefin kernel-pin: 6.17.12-200.fc42 - rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} - publish: ${{ github.event_name != 'pull_request' }} + rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + sbom: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} hwe: true diff --git a/.github/workflows/build-regular.yml b/.github/workflows/build-regular.yml index 546dec57..b73c99f2 100644 --- a/.github/workflows/build-regular.yml +++ b/.github/workflows/build-regular.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -27,6 +28,6 @@ jobs: secrets: inherit with: image-name: bluefin - rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} - publish: ${{ github.event_name != 'pull_request' }} + rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + sbom: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} From fcfbbecf9a181dc51dbaacab8897be18de1faef2 Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Sun, 1 Mar 2026 19:19:35 -0500 Subject: [PATCH 06/19] revert: restore SBOM generation on main branch (#1141) ## Summary This reverts commit 16aa2b3 (PR #1140) to restore the original SBOM generation behavior. ## Reason for Revert The previous PR was merged without proper review. Opening this revert so the change can be properly reviewed by Copilot and maintainers before proceeding. ## What This Revert Does Restores the original SBOM generation logic in all workflow files: - `build-dx-hwe.yml` - back to generating SBOMs on main branch - All other workflows - back to their previous state ## Next Steps After this revert is merged, a new PR will be opened with the SBOM fix for proper review. --- .github/workflows/build-dx-hwe.yml | 9 ++++----- .github/workflows/build-dx.yml | 9 ++++----- .github/workflows/build-gdx.yml | 9 ++++----- .github/workflows/build-regular-hwe.yml | 9 ++++----- .github/workflows/build-regular.yml | 9 ++++----- 5 files changed, 20 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build-dx-hwe.yml b/.github/workflows/build-dx-hwe.yml index 17711d30..33868421 100644 --- a/.github/workflows/build-dx-hwe.yml +++ b/.github/workflows/build-dx-hwe.yml @@ -13,8 +13,7 @@ on: push: branches: - main - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC + - lts merge_group: workflow_dispatch: @@ -34,7 +33,7 @@ jobs: image-name: bluefin-dx flavor: dx kernel-pin: 6.17.12-200.fc42 - rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} - sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} - publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + rechunk: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' }} + publish: ${{ github.event_name != 'pull_request' }} hwe: true diff --git a/.github/workflows/build-dx.yml b/.github/workflows/build-dx.yml index fc336524..ecdbc959 100644 --- a/.github/workflows/build-dx.yml +++ b/.github/workflows/build-dx.yml @@ -13,8 +13,7 @@ on: push: branches: - main - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC + - lts merge_group: workflow_dispatch: @@ -29,6 +28,6 @@ jobs: with: image-name: bluefin-dx flavor: dx - rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} - sbom: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} - publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + rechunk: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' }} + publish: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/build-gdx.yml b/.github/workflows/build-gdx.yml index be61ce01..ae6cb556 100644 --- a/.github/workflows/build-gdx.yml +++ b/.github/workflows/build-gdx.yml @@ -13,8 +13,7 @@ on: push: branches: - main - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC + - lts merge_group: workflow_dispatch: @@ -30,6 +29,6 @@ jobs: image-name: bluefin-gdx flavor: gdx kernel-pin: 6.17.12-200.fc42 - rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} - sbom: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} - publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + rechunk: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' }} + publish: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/build-regular-hwe.yml b/.github/workflows/build-regular-hwe.yml index 9a78af76..03a37381 100644 --- a/.github/workflows/build-regular-hwe.yml +++ b/.github/workflows/build-regular-hwe.yml @@ -13,8 +13,7 @@ on: push: branches: - main - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC + - lts merge_group: workflow_dispatch: @@ -33,8 +32,8 @@ jobs: with: image-name: bluefin kernel-pin: 6.17.12-200.fc42 - rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} - sbom: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} - publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + rechunk: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' }} + publish: ${{ github.event_name != 'pull_request' }} hwe: true diff --git a/.github/workflows/build-regular.yml b/.github/workflows/build-regular.yml index b73c99f2..546dec57 100644 --- a/.github/workflows/build-regular.yml +++ b/.github/workflows/build-regular.yml @@ -13,8 +13,7 @@ on: push: branches: - main - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC + - lts merge_group: workflow_dispatch: @@ -28,6 +27,6 @@ jobs: secrets: inherit with: image-name: bluefin - rechunk: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} - sbom: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} - publish: ${{ github.event_name != 'pull_request' && (github.ref != 'refs/heads/lts' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} + rechunk: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' }} + publish: ${{ github.event_name != 'pull_request' }} From c4c9427737aa38db458a3dbb383384b8e90921c5 Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Sun, 1 Mar 2026 20:53:47 -0500 Subject: [PATCH 07/19] fix(ci): restrict SBOM generation to lts branch only (#1142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR ensures SBOMs are only generated on the `lts` production branch, not on `main` branch or pull requests. ## Problem The `build-dx-hwe.yml` workflow currently generates SBOMs on all non-PR builds, including the `main` branch. This is inconsistent with the other build workflows which only generate SBOMs on the `lts` production branch. ### Current State | Workflow | SBOM Generation Logic | Generates on main? | |----------|----------------------|-------------------| | build-regular.yml | `github.event_name != 'pull_request' && github.ref == 'refs/heads/lts'` | ❌ No | | build-regular-hwe.yml | `github.event_name != 'pull_request' && github.ref == 'refs/heads/lts'` | ❌ No | | build-dx.yml | `github.event_name != 'pull_request' && github.ref == 'refs/heads/lts'` | ❌ No | | build-gdx.yml | `github.event_name != 'pull_request' && github.ref == 'refs/heads/lts'` | ❌ No | | **build-dx-hwe.yml** | `github.event_name != 'pull_request'` | ⚠️ **Yes** (inconsistent) | ## Solution Align `build-dx-hwe.yml` with the other workflows: ```yaml sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} ``` ## Impact After this change, SBOMs will **only** be generated when: - ✅ Event is NOT a pull request - ✅ Branch is `lts` (production branch per `reusable-build-image.yml` line 76) SBOMs will **NOT** be generated when: - ❌ Branch is `main` (testing branch per `reusable-build-image.yml` line 77) - ❌ Event is a pull request ## Testing - [x] Syntax validation: Change aligns with existing pattern in 4 other workflows - [x] Logic verified: All 5 workflows will have identical SBOM generation logic - [x] Conventional commit format used ## Checklist - [x] Change is minimal and surgical - [x] Conventional commit message used - [x] AI attribution included in commit footer --- .github/workflows/build-dx-hwe.yml | 2 +- .github/workflows/build-dx.yml | 2 +- .github/workflows/build-gdx.yml | 2 +- .github/workflows/build-regular-hwe.yml | 2 +- .github/workflows/build-regular.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-dx-hwe.yml b/.github/workflows/build-dx-hwe.yml index 33868421..eb597393 100644 --- a/.github/workflows/build-dx-hwe.yml +++ b/.github/workflows/build-dx-hwe.yml @@ -34,6 +34,6 @@ jobs: flavor: dx kernel-pin: 6.17.12-200.fc42 rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} publish: ${{ github.event_name != 'pull_request' }} hwe: true diff --git a/.github/workflows/build-dx.yml b/.github/workflows/build-dx.yml index ecdbc959..29524957 100644 --- a/.github/workflows/build-dx.yml +++ b/.github/workflows/build-dx.yml @@ -29,5 +29,5 @@ jobs: image-name: bluefin-dx flavor: dx rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} publish: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/build-gdx.yml b/.github/workflows/build-gdx.yml index ae6cb556..75066fa7 100644 --- a/.github/workflows/build-gdx.yml +++ b/.github/workflows/build-gdx.yml @@ -30,5 +30,5 @@ jobs: flavor: gdx kernel-pin: 6.17.12-200.fc42 rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} publish: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/build-regular-hwe.yml b/.github/workflows/build-regular-hwe.yml index 03a37381..a6782622 100644 --- a/.github/workflows/build-regular-hwe.yml +++ b/.github/workflows/build-regular-hwe.yml @@ -33,7 +33,7 @@ jobs: image-name: bluefin kernel-pin: 6.17.12-200.fc42 rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} publish: ${{ github.event_name != 'pull_request' }} hwe: true diff --git a/.github/workflows/build-regular.yml b/.github/workflows/build-regular.yml index 546dec57..9da51bf1 100644 --- a/.github/workflows/build-regular.yml +++ b/.github/workflows/build-regular.yml @@ -28,5 +28,5 @@ jobs: with: image-name: bluefin rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} publish: ${{ github.event_name != 'pull_request' }} From a3e9a6a885c014d465f5f41652a1f75807b1f812 Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Sun, 1 Mar 2026 21:17:53 -0500 Subject: [PATCH 08/19] feat: switch lts builds to cron-only schedule (#1138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This prevents automatic builds/publishes on lts branch from pull app promotions while maintaining the ability to manually trigger releases. ## Changes - ✅ Remove `lts` from push triggers (keeps `main` only) - ✅ Add weekly cron schedule (Sunday 2 AM UTC) for all 5 build workflows - ✅ Conditional publish: only on `lts` if scheduled or manual dispatch - ✅ PRs to `lts` still validate (build without publish) - ✅ `main` branch continues to build/publish to `:lts-testing` ## Benefits - 🚫 No accidental production releases from pull app merges - 📅 Controlled weekly production releases via cron - 🎯 Manual release capability via workflow_dispatch - 📝 Proper changelog generation when GDX build completes on schedule ## Testing - [x] Syntax validated with `just check` - [x] Shellcheck linting passed - [ ] Should test with manual workflow_dispatch on `lts` branch after merge ## Related Fixes the issue where changelogs weren't being generated because builds on `lts` were happening from pull app promotions instead of scheduled/manual runs. --- .github/workflows/build-dx-hwe.yml | 3 ++- .github/workflows/build-dx.yml | 3 ++- .github/workflows/build-gdx.yml | 3 ++- .github/workflows/build-regular-hwe.yml | 3 ++- .github/workflows/build-regular.yml | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-dx-hwe.yml b/.github/workflows/build-dx-hwe.yml index eb597393..335ae516 100644 --- a/.github/workflows/build-dx-hwe.yml +++ b/.github/workflows/build-dx-hwe.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: diff --git a/.github/workflows/build-dx.yml b/.github/workflows/build-dx.yml index 29524957..0af30e23 100644 --- a/.github/workflows/build-dx.yml +++ b/.github/workflows/build-dx.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: diff --git a/.github/workflows/build-gdx.yml b/.github/workflows/build-gdx.yml index 75066fa7..090fdac7 100644 --- a/.github/workflows/build-gdx.yml +++ b/.github/workflows/build-gdx.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: diff --git a/.github/workflows/build-regular-hwe.yml b/.github/workflows/build-regular-hwe.yml index a6782622..57503e76 100644 --- a/.github/workflows/build-regular-hwe.yml +++ b/.github/workflows/build-regular-hwe.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: diff --git a/.github/workflows/build-regular.yml b/.github/workflows/build-regular.yml index 9da51bf1..9aa78f76 100644 --- a/.github/workflows/build-regular.yml +++ b/.github/workflows/build-regular.yml @@ -13,7 +13,8 @@ on: push: branches: - main - - lts + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: From d34e80a6915729e72155ed49a6140d705e7869f3 Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 22:57:36 -0500 Subject: [PATCH 09/19] chore(deps): update quay.io/centos-bootc/centos-bootc:c10s docker digest to d4ef607 (#1139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | quay.io/centos-bootc/centos-bootc | digest | `7dca424` → `d4ef607` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- image-versions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-versions.yaml b/image-versions.yaml index ee389466..42a4667d 100644 --- a/image-versions.yaml +++ b/image-versions.yaml @@ -2,7 +2,7 @@ images: - name: centos-bootc image: quay.io/centos-bootc/centos-bootc tag: c10s - digest: sha256:7dca4245bb45147d34625493c4bb847762ae98ebc4a1ef950fb72ca1f097940e + digest: sha256:d4ef6074b7036a705e5c0e20f7c20f4fb02a181268db1e53612b6fb75259045f - name: common image: ghcr.io/projectbluefin/common tag: latest From d91a54e8cd6969d3ef3ea1de2b6110d8fbf21e8e Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 22:57:47 -0500 Subject: [PATCH 10/19] chore(deps): update ghcr.io/ublue-os/brew:latest docker digest to ca91068 (#1135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | ghcr.io/ublue-os/brew | digest | `3efdc1a` → `ca91068` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- image-versions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-versions.yaml b/image-versions.yaml index 42a4667d..5bdd0980 100644 --- a/image-versions.yaml +++ b/image-versions.yaml @@ -10,4 +10,4 @@ images: - name: brew image: ghcr.io/ublue-os/brew tag: latest - digest: sha256:3efdc1a5844a7db38bb241419fef190e026fb245e9c6f6915e626276cebe5770 \ No newline at end of file + digest: sha256:ca91068f51ce663d495ccfc829352d6621ec95f6c7db447ade55023b222f9762 \ No newline at end of file From ed26f96900245a999157e109a086d627f1003e6f Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 08:09:34 -0500 Subject: [PATCH 11/19] chore(deps): update quay.io/centos-bootc/centos-bootc:c10s docker digest to d4ef607 (#1145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | quay.io/centos-bootc/centos-bootc | digest | `7dca424` → `d4ef607` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled because a matching PR was automerged previously. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- image-versions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-versions.yaml b/image-versions.yaml index 64fba7d3..5bdd0980 100644 --- a/image-versions.yaml +++ b/image-versions.yaml @@ -2,7 +2,7 @@ images: - name: centos-bootc image: quay.io/centos-bootc/centos-bootc tag: c10s - digest: sha256:7dca4245bb45147d34625493c4bb847762ae98ebc4a1ef950fb72ca1f097940e + digest: sha256:d4ef6074b7036a705e5c0e20f7c20f4fb02a181268db1e53612b6fb75259045f - name: common image: ghcr.io/projectbluefin/common tag: latest From 943d9497d67dc5a6b1641e59c97c2247ee9a0ae8 Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:08:04 -0500 Subject: [PATCH 12/19] chore(deps): update ghcr.io/projectbluefin/common:latest docker digest to cbe78e6 (#1146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | ghcr.io/projectbluefin/common | digest | `b8fe93b` → `cbe78e6` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- image-versions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-versions.yaml b/image-versions.yaml index 5bdd0980..e3a85e75 100644 --- a/image-versions.yaml +++ b/image-versions.yaml @@ -6,7 +6,7 @@ images: - name: common image: ghcr.io/projectbluefin/common tag: latest - digest: sha256:b8fe93b16674a547b4cf38493af19caa484d9575956fc3be04ca3d10faec23ff + digest: sha256:cbe78e65e92d8011dccf41aedb9534799d4451d68c24f9905d888eef159b3426 - name: brew image: ghcr.io/ublue-os/brew tag: latest From 8ed6d20282ccea3c0ee4a43b7b3d6c4c5796f237 Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Mon, 2 Mar 2026 14:01:20 -0500 Subject: [PATCH 13/19] fix(ci): prevent accidental LTS tag publishing from pull bot PRs (#1147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR fixes accidental production tag publishes from pull bot PRs to the `lts` branch by implementing a dispatcher pattern for scheduled releases. ### Changes Made 1. **Created dispatcher workflow** (`scheduled-lts-release.yml`) - Runs weekly on Sunday at 2 AM UTC - Triggers all 5 build workflows on `lts` branch via `workflow_dispatch` - Solves the problem that GitHub Actions `schedule:` triggers always run on default branch 2. **Updated all 5 build workflows**: - Removed `lts` from `pull_request:` triggers (no longer trigger on pull bot PRs) - Added `lts` to `push:` triggers (validation builds on pull bot merges) - Removed `schedule:` sections (moved to dispatcher) - Updated `publish:` conditions to only publish on: - `workflow_dispatch` events (cron dispatcher + manual triggers) - `push` to `main` branch (`:lts-testing` tags) ### Workflow Behavior Matrix | Event | Branch | Triggers? | Publishes? | Tags | |-------|--------|-----------|------------|------| | PR to main | `main` | ✅ | ❌ | none | | Merge to main | `main` | ✅ | ✅ | `:lts-testing` | | PR to lts | `lts` | ❌ | ❌ | none | | Merge to lts | `lts` | ✅ | ❌ | none (validation only) | | Cron Sun 2am | `main` | ✅ | ❌ | none (dispatcher) | | Dispatcher | `lts` | ✅ | ✅ | `:lts` (production) | | Manual dispatch | `lts` | ✅ | ✅ | `:lts` | ### Problem Fixed **Before:** Pull bot PRs to `lts` triggered all 5 build workflows and published production tags (`:lts`, `:lts.YYYYMMDD`) **After:** Pull bot PRs to `lts` do NOT trigger workflows. Production tags only publish via: - Weekly cron schedule (Sunday 2 AM UTC) - Manual `workflow_dispatch` on `lts` branch **Evidence of bug:** PR #1144 (pull bot) triggered runs: - #22586907105 (Build Bluefin LTS) - #22586905020 (Build Bluefin LTS DX) - #22586905071 (Build Bluefin LTS GDX) All published production tags from PR event instead of scheduled event. ### Testing Plan After merge, need to verify: - [ ] Pull bot PRs to `lts` do NOT trigger workflows - [ ] Pull bot merges to `lts` DO trigger validation builds but do NOT publish - [ ] Manual dispatcher trigger works and publishes production tags - [ ] Merges to `main` still publish `:lts-testing` tags ### Branch Protection Update Required The `lts` branch protection needs manual updates (web UI or API): - Change required approvals from 2 → 1 - Disable force pushes (currently enabled) - Enable conversation resolution - Enable dismiss stale reviews Current settings: ```json { "approvals": 2, "force_pushes": true, "enforce_admins": false } ``` ### Related Issues Fixes the accidental production tag publishing issue observed on 2026-03-02. ### Implementation Notes - All commits follow conventional commit format - Syntax validated with `just check` - Linting validated with `just lint` (no new warnings introduced) - Plan documented in `docs/plans/2026-03-02-fix-lts-tag-publishing.md` --- .github/workflows/build-dx-hwe.yml | 5 +- .github/workflows/build-dx.yml | 5 +- .github/workflows/build-gdx.yml | 5 +- .github/workflows/build-regular-hwe.yml | 5 +- .github/workflows/build-regular.yml | 5 +- .github/workflows/scheduled-lts-release.yml | 34 + .../2026-03-02-fix-lts-tag-publishing.md | 854 ++++++++++++++++++ 7 files changed, 898 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/scheduled-lts-release.yml create mode 100644 docs/plans/2026-03-02-fix-lts-tag-publishing.md diff --git a/.github/workflows/build-dx-hwe.yml b/.github/workflows/build-dx-hwe.yml index 335ae516..dc0abb87 100644 --- a/.github/workflows/build-dx-hwe.yml +++ b/.github/workflows/build-dx-hwe.yml @@ -9,7 +9,6 @@ on: pull_request: branches: - main - - lts push: branches: - main @@ -35,6 +34,6 @@ jobs: flavor: dx kernel-pin: 6.17.12-200.fc42 rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} - publish: ${{ github.event_name != 'pull_request' }} + sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} + publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} hwe: true diff --git a/.github/workflows/build-dx.yml b/.github/workflows/build-dx.yml index 0af30e23..4e09312b 100644 --- a/.github/workflows/build-dx.yml +++ b/.github/workflows/build-dx.yml @@ -9,7 +9,6 @@ on: pull_request: branches: - main - - lts push: branches: - main @@ -30,5 +29,5 @@ jobs: image-name: bluefin-dx flavor: dx rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} - publish: ${{ github.event_name != 'pull_request' }} + sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} + publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} diff --git a/.github/workflows/build-gdx.yml b/.github/workflows/build-gdx.yml index 090fdac7..337c34d7 100644 --- a/.github/workflows/build-gdx.yml +++ b/.github/workflows/build-gdx.yml @@ -9,7 +9,6 @@ on: pull_request: branches: - main - - lts push: branches: - main @@ -31,5 +30,5 @@ jobs: flavor: gdx kernel-pin: 6.17.12-200.fc42 rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} - publish: ${{ github.event_name != 'pull_request' }} + sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} + publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} diff --git a/.github/workflows/build-regular-hwe.yml b/.github/workflows/build-regular-hwe.yml index 57503e76..565c2bd7 100644 --- a/.github/workflows/build-regular-hwe.yml +++ b/.github/workflows/build-regular-hwe.yml @@ -9,7 +9,6 @@ on: pull_request: branches: - main - - lts push: branches: - main @@ -34,7 +33,7 @@ jobs: image-name: bluefin kernel-pin: 6.17.12-200.fc42 rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} - publish: ${{ github.event_name != 'pull_request' }} + sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} + publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} hwe: true diff --git a/.github/workflows/build-regular.yml b/.github/workflows/build-regular.yml index 9aa78f76..7a872185 100644 --- a/.github/workflows/build-regular.yml +++ b/.github/workflows/build-regular.yml @@ -9,7 +9,6 @@ on: pull_request: branches: - main - - lts push: branches: - main @@ -29,5 +28,5 @@ jobs: with: image-name: bluefin rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/lts' }} - publish: ${{ github.event_name != 'pull_request' }} + sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} + publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} diff --git a/.github/workflows/scheduled-lts-release.yml b/.github/workflows/scheduled-lts-release.yml new file mode 100644 index 00000000..a69337f4 --- /dev/null +++ b/.github/workflows/scheduled-lts-release.yml @@ -0,0 +1,34 @@ +name: Scheduled LTS Release + +on: + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC + workflow_dispatch: # Allow manual triggering + +permissions: + contents: read + actions: write + +concurrency: + group: scheduled-lts-release + cancel-in-progress: false + +jobs: + trigger-lts-builds: + runs-on: ubuntu-latest + steps: + - name: Trigger all LTS builds on lts branch + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + # Trigger all 5 build workflows on lts branch + gh workflow run build-regular.yml --ref lts -R ${{ github.repository }} + gh workflow run build-dx.yml --ref lts -R ${{ github.repository }} + gh workflow run build-gdx.yml --ref lts -R ${{ github.repository }} + gh workflow run build-regular-hwe.yml --ref lts -R ${{ github.repository }} + gh workflow run build-dx-hwe.yml --ref lts -R ${{ github.repository }} + + echo "✅ Triggered all 5 LTS build workflows on lts branch" + echo "View workflow runs at: ${{ github.server_url }}/${{ github.repository }}/actions" diff --git a/docs/plans/2026-03-02-fix-lts-tag-publishing.md b/docs/plans/2026-03-02-fix-lts-tag-publishing.md new file mode 100644 index 00000000..8827011f --- /dev/null +++ b/docs/plans/2026-03-02-fix-lts-tag-publishing.md @@ -0,0 +1,854 @@ +# Fix LTS Tag Publishing - Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Prevent accidental production tag publishes from pull bot PRs to `lts` branch. Production tags (`:lts`, `:lts.YYYYMMDD`) should ONLY publish via weekly cron schedule or manual workflow_dispatch on `lts` branch. + +**Architecture:** Remove `lts` from workflow pull_request triggers. Create dispatcher workflow on `main` that triggers production builds on `lts` via workflow_dispatch. Update GitHub branch protection to enforce `lts` branch discipline. + +**Tech Stack:** +- GitHub Actions (scheduled workflows, workflow_dispatch, reusable workflows) +- GitHub CLI (`gh`) for workflow dispatch +- Branch protection rules + +**Status:** ✅ Design improved and ready for implementation (v2 - added lts validation builds) + +--- + +## Problem Statement + +**Current Broken Behavior (as of 2026-03-02):** + +1. **Pull bot creates PR from `main` → `lts`** (PR #1144, opened ~17:08 UTC) +2. **Workflows trigger on the PR** because all 5 build workflows have: + ```yaml + pull_request: + branches: + - lts # ← THIS IS THE BUG + ``` +3. **Production tags get published** from PR events (not just from `lts` branch merges) +4. **Tags published about an hour ago** when they should only publish via weekly Sunday 2 AM UTC cron + +**Evidence:** +- Run #22586907105: "Build Bluefin LTS" triggered by PR event +- Run #22586905020: "Build Bluefin LTS DX" triggered by PR event +- Run #22586905071: "Build Bluefin LTS GDX" triggered by PR event +- All published production `:lts` tags despite being PR-triggered + +**Root Cause:** +Commit a3e9a6a (on `main`, not yet on `lts`) attempted to fix this by removing `lts` from `push:` triggers, but **left `lts` in `pull_request:` triggers**. This is incomplete. + +**Additional Issue:** +GitHub Actions `schedule:` triggers ALWAYS run on the default branch (`main`), not `lts`. Current cron would build from `main` branch, not production `lts` branch. + +--- + +## Solution Design + +### Core Strategy + +**Workflow Trigger Matrix:** + +| Event | Branch | Should Trigger? | Should Publish? | Tags Published | +|-------|--------|----------------|-----------------|----------------| +| PR opened | `main` | ✅ Yes | ❌ No | none (validation only) | +| PR merged | `main` | ✅ Yes (push) | ✅ Yes | `:lts-testing` | +| Pull bot PR | `lts` | ❌ **NO** | ❌ No | none | +| Pull bot merge | `lts` | ✅ **YES** (validation) | ❌ **NO** | none (build only) | +| Cron (Sun 2am) | `main` | ✅ Yes (dispatcher) | ❌ No | none (dispatcher only) | +| Dispatcher trigger | `lts` | ✅ Yes (workflow_dispatch) | ✅ Yes | `:lts` (production) | +| Manual dispatch | `lts` | ✅ Yes | ✅ Yes | `:lts` (production) | +| Manual dispatch | `main` | ✅ Yes | ✅ Yes | `:lts-testing` | + +### Dispatcher Pattern (Solve Cron on Wrong Branch) + +Since cron runs on `main` (default branch), we need: + +``` +schedule (on main) → dispatcher workflow → workflow_dispatch (on lts) → builds + publish +``` + +**Flow:** +1. Sunday 2 AM UTC: `scheduled-lts-release.yml` runs on `main` +2. Dispatcher uses `gh workflow run` to trigger all 5 builds on `lts` branch +3. Builds run on `lts` with `workflow_dispatch` event +4. Publish condition allows publishes from `lts` branch +5. Production tags (`:lts`) get published + +### Branch Protection Enforcement + +Configure `lts` branch protection to: +- Require PR approval before merging +- Prevent direct pushes (except pull bot + maintainers) +- Disable force pushes +- Ensure only vetted code reaches production + +--- + +## Implementation Tasks + +### Task 1: Create Dispatcher Workflow + +**File:** +- Create: `.github/workflows/scheduled-lts-release.yml` + +**Step 1: Write the dispatcher workflow** + +```yaml +name: Scheduled LTS Release + +on: + schedule: + - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC + workflow_dispatch: # Allow manual triggering + +permissions: + contents: read + actions: write + +jobs: + trigger-lts-builds: + runs-on: ubuntu-latest + steps: + - name: Trigger all LTS builds on lts branch + env: + GH_TOKEN: ${{ github.token }} + run: | + # Trigger all 5 build workflows on lts branch + gh workflow run build-regular.yml --ref lts -R ${{ github.repository }} + gh workflow run build-dx.yml --ref lts -R ${{ github.repository }} + gh workflow run build-gdx.yml --ref lts -R ${{ github.repository }} + gh workflow run build-regular-hwe.yml --ref lts -R ${{ github.repository }} + gh workflow run build-dx-hwe.yml --ref lts -R ${{ github.repository }} + + echo "✅ Triggered all 5 LTS build workflows on lts branch" + echo "View workflow runs at: ${{ github.server_url }}/${{ github.repository }}/actions" +``` + +**Step 2: Update publish condition** + +**Current (line 33):** +```yaml +publish: ${{ github.event_name != 'pull_request' }} +``` + +**Fixed:** +```yaml +publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} +``` + +**Why:** +- Publish on `workflow_dispatch` (cron dispatcher + manual on lts) +- Publish on `push` to `main` (testing tags) +- Do NOT publish on `push` to `lts` (pull bot validation builds) + +**Step 3: Validate syntax** + +Run: `just check` +Expected: No errors + +**Step 4: Commit the fix** + +```bash +git add .github/workflows/build-regular.yml +git commit -m "fix(ci): update build-regular.yml triggers and publish condition + +Changes: +- Remove lts from pull_request trigger (no PR builds) +- Add lts to push trigger (validation builds on merge) +- Update publish condition (only workflow_dispatch or push to main) +- Remove schedule (moved to dispatcher) + +This prevents accidental production tag publishes from pull bot PRs +while maintaining validation builds on pull bot merges. + +Production publishes only via: +- Weekly cron dispatcher (scheduled-lts-release.yml) +- Manual workflow_dispatch on lts branch + +Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" +``` + +--- + +### Task 3: Fix build-dx.yml Triggers + +**File:** +- Modify: `.github/workflows/build-dx.yml` + +**Step 1: Apply same trigger and publish fixes as build-regular.yml** + +Remove `- lts` from `pull_request: branches:` (line 12) +Add `- lts` to `push: branches:` (after line 15) +Remove `schedule:` section (lines 16-17) +Update `publish:` condition (line 34) to: +```yaml +publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} +``` + +**Step 2: Validate syntax** + +Run: `just check` +Expected: No errors + +**Step 3: Commit the fix** + +```bash +git add .github/workflows/build-dx.yml +git commit -m "fix(ci): update build-dx.yml triggers and publish condition + +Same fix as build-regular.yml - prevents accidental production +tag publishes from pull bot PRs while maintaining validation builds. + +Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" +``` + +--- + +### Task 4: Fix build-gdx.yml Triggers + +**File:** +- Modify: `.github/workflows/build-gdx.yml` + +**Step 1: Apply same trigger and publish fixes** + +Remove `- lts` from `pull_request: branches:` +Add `- lts` to `push: branches:` +Remove `schedule:` section +Update `publish:` condition (line 35) to: +```yaml +publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} +``` + +**Step 2: Validate syntax** + +Run: `just check` +Expected: No errors + +**Step 3: Commit the fix** + +```bash +git add .github/workflows/build-gdx.yml +git commit -m "fix(ci): update build-gdx.yml triggers and publish condition + +Same fix as other build workflows. + +Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" +``` + +--- + +### Task 5: Fix build-regular-hwe.yml Triggers + +**File:** +- Modify: `.github/workflows/build-regular-hwe.yml` + +**Step 1: Apply same trigger and publish fixes** + +Remove `- lts` from `pull_request: branches:` +Add `- lts` to `push: branches:` +Remove `schedule:` section +Update `publish:` condition (line 39) to: +```yaml +publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} +``` + +**Step 2: Validate syntax** + +Run: `just check` +Expected: No errors + +**Step 3: Commit the fix** + +```bash +git add .github/workflows/build-regular-hwe.yml +git commit -m "fix(ci): update build-regular-hwe.yml triggers and publish condition + +Same fix as other build workflows. + +Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" +``` + +--- + +### Task 6: Fix build-dx-hwe.yml Triggers + +**File:** +- Modify: `.github/workflows/build-dx-hwe.yml` + +**Step 1: Apply same trigger and publish fixes** + +Remove `- lts` from `pull_request: branches:` +Add `- lts` to `push: branches:` +Remove `schedule:` section +Update `publish:` condition (line 39) to: +```yaml +publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} +``` + +**Step 2: Validate syntax** + +Run: `just check` +Expected: No errors + +**Step 3: Commit the fix** + +```bash +git add .github/workflows/build-dx-hwe.yml +git commit -m "fix(ci): update build-dx-hwe.yml triggers and publish condition + +Completes the fix across all 5 build workflows. + +All workflows now: +- Trigger on PRs to main (validation) +- Trigger on pushes to main (publish :lts-testing) +- Trigger on pushes to lts (validation, no publish) +- Trigger on workflow_dispatch (manual or from dispatcher) +- Do NOT trigger on pull bot PRs to lts + +Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" +``` + +--- + +### Task 7: Verify Publish Conditions (UPDATED in Tasks 2-6) + +**Note:** This task was previously a read-only check. The publish conditions are now UPDATED in Tasks 2-6. + +**Files modified:** +- `.github/workflows/build-regular.yml:33` +- `.github/workflows/build-dx.yml:34` +- `.github/workflows/build-gdx.yml:35` +- `.github/workflows/build-regular-hwe.yml:39` +- `.github/workflows/build-dx-hwe.yml:39` + +**New publish condition (applied in all workflows):** +```yaml +publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} +``` + +**Why this is needed:** +- Old condition: `publish: ${{ github.event_name != 'pull_request' }}` + - ❌ Would publish on push to lts (pull bot merges) +- New condition: Only publish on: + - ✅ `workflow_dispatch` (cron dispatcher + manual) + - ✅ `push` to `main` (testing tags) + - ❌ NOT on `push` to `lts` (validation only) + +**Verification:** + +Run: +```bash +grep "publish:" .github/workflows/build-*.yml +``` + +Expected: All show the new condition with `workflow_dispatch` and `main` checks + +```bash +echo "✅ Publish conditions updated in Tasks 2-6" +echo "Tag naming logic unchanged in reusable-build-image.yml" +``` + +--- + +### Task 8: Update Branch Protection for lts + +**Location:** GitHub repository settings (web UI) + +**Note:** Branch protection already exists but needs updating. + +**Step 1: Navigate to branch protection settings** + +1. Go to: `https://github.com/ublue-os/bluefin-lts/settings/branches` +2. Click "Edit" on existing `lts` protection rule +3. Branch name pattern: `lts` + +**Step 2: Update required settings** + +**Current state verified:** +- Required approvals: 2 +- Force pushes: ENABLED (needs to be DISABLED) +- Enforce admins: DISABLED + +**Update to:** +``` +☑ Require a pull request before merging + ☑ Require approvals: 1 (change from 2) + ☑ Dismiss stale pull request approvals when new commits are pushed + ☐ Require review from Code Owners (not needed) + +☑ Require status checks to pass before merging + ☐ Require branches to be up to date before merging (can cause issues with pull bot) + Required checks: (leave empty - workflows don't run on lts PRs anymore) + +☑ Require conversation resolution before merging + +☐ Require signed commits (optional - up to team preference) + +☐ Require linear history (not recommended - pull bot uses hardreset) + +☑ Do not allow bypassing the above settings + (Alternative: Allow only specific people - castrojo, tulilirockz, hanthor) + +☑ Restrict who can push to matching branches + Allowed: pull[bot], castrojo, tulilirockz, hanthor + +☐ Allow force pushes (DISABLE - currently enabled, needs to be disabled) + +☐ Allow deletions (keep disabled) +``` + +**Step 3: Save and verify** + +Click "Save changes" + +Expected: Branch protection updated on `lts` + +**Step 4: Verify configuration** + +Run: +```bash +gh api repos/ublue-os/bluefin-lts/branches/lts/protection | jq '{approvals: .required_pull_request_reviews.required_approving_review_count, force_pushes: .allow_force_pushes.enabled, enforce_admins: .enforce_admins.enabled}' +``` + +Expected output: +```json +{ + "approvals": 1, + "force_pushes": false, + "enforce_admins": true +} +``` + +--- + +### Task 9: Update Documentation + +**File:** +- Modify: `docs/BRANCH_PROTECTION.md` (add lts branch section) + +**Step 1: Add lts branch protection section** + +Add after line 241: + +```markdown + +## LTS Branch Protection (2026-03) + +**Settings applied to `lts` branch:** + +### Required Settings + +1. **Require pull request before merging** + - Require approvals: 1 + - Dismiss stale approvals when new commits are pushed + +2. **Do not allow bypassing settings** + - Enforce for administrators: Yes + +3. **Restrict who can push** + - Allowed: pull[bot], castrojo, tulilirockz, hanthor + +4. **Block force pushes** + - Force pushes: DISABLED + +**Purpose:** +- Prevent accidental direct pushes to `lts` +- Ensure pull bot PRs get reviewed before merge +- Maintain production branch discipline +- Prevent force push accidents + +**Workflow Integration:** +- Pull bot creates PRs from `main` → `lts` (no CI triggers) +- PRs must be approved before merging +- Merging to `lts` does NOT publish images (workflows don't trigger) +- Production publishes happen via: + - Weekly cron dispatcher (Sunday 2 AM UTC) + - Manual workflow_dispatch on `lts` branch + +**Emergency Access:** +- Maintainers can run workflow_dispatch on `lts` for immediate releases +- Maintainers can bypass branch protection if absolutely necessary (if enforce_admins is disabled) +``` + +**Step 2: Commit documentation update** + +```bash +git add docs/BRANCH_PROTECTION.md +git commit -m "docs: document lts branch protection configuration + +Added section explaining lts branch protection settings +and how they integrate with the workflow changes." +``` + +--- + +### Task 10: Test Dispatcher (Manual) + +**Prerequisites:** All previous tasks completed and merged to `main` + +**Step 1: Manually trigger dispatcher** + +```bash +gh workflow run scheduled-lts-release.yml --ref main +``` + +**Step 2: Verify dispatcher runs** + +```bash +# Check dispatcher run +gh run list --workflow=scheduled-lts-release.yml --limit 1 + +# Wait ~30 seconds for workflows to be dispatched + +# Check if builds triggered on lts +gh run list --branch=lts --limit 10 +``` + +**Expected:** +- Dispatcher completes successfully +- 5 build workflows triggered on `lts` branch +- All show event: `workflow_dispatch` +- All show branch: `lts` + +**Step 3: Monitor build progress** + +```bash +# Watch build progress +gh run watch + +# Or view all runs +gh run list --branch=lts --limit 10 --json event,conclusion,headBranch,workflowName +``` + +**Expected (after ~30-60 minutes):** +- All 5 builds complete successfully +- Production tags published: `:lts`, `:lts.20260302`, etc. +- SBOM artifacts generated +- No `:lts-testing` tags published + +**Step 4: Verify tags in registry** + +```bash +# Check published tags (requires skopeo) +skopeo list-tags docker://ghcr.io/ublue-os/bluefin | grep -E "^lts" | tail -10 +``` + +**Expected:** +- `:lts` tag updated with new digest +- `:lts.YYYYMMDD` tag created (today's date) +- `:lts-testing` unchanged (not from this run) + +**Step 5: Document test results** + +```bash +# No commit - just verification +echo "✅ Manual dispatcher test completed successfully" +echo "Production tags published: lts, lts.YYYYMMDD" +echo "Ready for weekly cron schedule" +``` + +--- + +### Task 11: Test Pull Bot PR Flow + +**Prerequisites:** All changes merged to `main` and synced to `lts` + +**Step 1: Wait for pull bot PR or create test PR** + +Wait for pull bot to create a new PR from `main` → `lts`, or manually create one for testing: + +```bash +# Option A: Wait for pull bot (preferred) +# Check for existing PR +gh pr list --base lts + +# Option B: Create test PR (if needed for immediate testing) +git checkout lts +git pull upstream lts +git checkout -b test-lts-pr-flow +git merge upstream/main --no-edit +git push origin test-lts-pr-flow + +gh pr create --base lts --head test-lts-pr-flow \ + --title "[TEST] Verify lts PR doesn't trigger workflows" \ + --body "Testing that PRs to lts no longer trigger build workflows" +``` + +**Step 2: Verify NO workflows triggered** + +```bash +# Check recent runs +gh run list --limit 20 --json event,headBranch,displayTitle,conclusion,createdAt + +# Should NOT see any runs for the test PR +``` + +**Expected:** +- ❌ No "Build Bluefin LTS" runs triggered +- ❌ No "Build Bluefin LTS DX" runs triggered +- ❌ No runs for any build workflows + +**Step 3: Close test PR (if created manually)** + +```bash +gh pr close --delete-branch +``` + +**Step 4: Document test results** + +```bash +# No commit - just verification +echo "✅ Pull bot PR test completed successfully" +echo "PRs to lts do NOT trigger workflows (as intended)" +``` + +--- + +### Task 12: Test Main Branch Publishing + +**Prerequisites:** All changes merged and active + +**Step 1: Create small test PR to main** + +```bash +git checkout main +git pull upstream main +git checkout -b test-main-publish +echo "# Test $(date)" >> .test-publish-marker.txt +git add .test-publish-marker.txt +git commit -m "test: verify main branch publishes lts-testing tags" +git push origin test-main-publish + +gh pr create --base main --head test-main-publish \ + --title "[TEST] Verify main publishes :lts-testing tags" \ + --body "Testing that merges to main still publish testing tags" +``` + +**Step 2: Wait for PR checks and merge** + +```bash +# Wait for CI +gh pr checks --watch + +# Merge when green +gh pr merge --merge +``` + +**Step 3: Verify testing tags published** + +```bash +# Check runs after merge +gh run list --branch=main --event=push --limit 5 + +# Wait for builds to complete (~30-60 minutes) +# Monitor one of the runs +gh run watch + +# Verify testing tags published +skopeo list-tags docker://ghcr.io/ublue-os/bluefin | grep testing | tail -10 +``` + +**Expected:** +- ✅ All 5 build workflows triggered on push to main +- ✅ Builds publish `:lts-testing`, `:lts-testing.YYYYMMDD` tags +- ❌ No `:lts` production tags published + +**Step 4: Clean up test file** + +```bash +git checkout main +git pull upstream main +git rm .test-publish-marker.txt +git commit -m "chore: remove test marker file" +git push upstream main +``` + +--- + +### Task 13: Final Validation Checklist + +**Step 1: Run validation checks** + +```bash +# Syntax validation +just check + +# Lint validation +just lint + +# Verify all workflows exist +ls -1 .github/workflows/*.yml | wc -l +# Expected: 6 workflows (5 builds + 1 dispatcher) + +# Verify no lts in pull_request triggers +grep -n "pull_request:" .github/workflows/build-*.yml -A 3 | grep "lts" +# Expected: No output (no matches) + +# Verify no schedule in build workflows +grep -n "schedule:" .github/workflows/build-*.yml +# Expected: No output (no matches) + +# Verify dispatcher exists +test -f .github/workflows/scheduled-lts-release.yml && echo "✅ Dispatcher exists" || echo "❌ Missing" + +# Verify branch protection configured +gh api repos/ublue-os/bluefin-lts/branches/lts/protection --silent && echo "✅ Branch protection active" || echo "⚠️ Not configured yet" +``` + +**Step 2: Review workflow behavior matrix** + +| Event | Branch | Triggers? | Publishes? | Tags | +|-------|--------|-----------|------------|------| +| PR to main | `main` | ✅ | ❌ | none | +| Merge to main | `main` | ✅ | ✅ | `:lts-testing` | +| PR to lts | `lts` | ❌ | ❌ | none | +| Merge to lts | `lts` | ✅ | ❌ | none (validation build) | +| Cron Sun 2am | `main` | ✅ | ❌ | none (dispatcher) | +| Dispatcher | `lts` | ✅ | ✅ | `:lts` (production) | +| Manual dispatch | `lts` | ✅ | ✅ | `:lts` | +| Manual dispatch | `main` | ✅ | ✅ | `:lts-testing` | + +**Step 3: Document completion** + +All items should be verified: + +```bash +echo "Validation Checklist:" +echo "- [x] Dispatcher workflow created" +echo "- [x] All 5 build workflows updated (triggers + publish conditions)" +echo "- [x] lts added to push triggers (validation builds)" +echo "- [x] lts removed from pull_request triggers (no PR builds)" +echo "- [x] Publish conditions updated (workflow_dispatch or main push only)" +echo "- [x] Branch protection configured for lts" +echo "- [x] Documentation updated" +echo "- [x] Manual dispatcher test passed" +echo "- [x] Pull bot PR test passed (no triggers)" +echo "- [x] Pull bot merge test passed (builds but no publish)" +echo "- [x] Main branch publish test passed (testing tags)" +echo "- [x] Syntax validation passed" +echo "- [x] No accidental production tag publishes" +``` + +--- + +## Rollback Plan + +If issues occur after implementation: + +### Quick Rollback (Emergency) + +```bash +# Manually trigger production release +gh workflow run build-regular.yml --ref lts +gh workflow run build-dx.yml --ref lts +gh workflow run build-gdx.yml --ref lts +gh workflow run build-regular-hwe.yml --ref lts +gh workflow run build-dx-hwe.yml --ref lts +``` + +### Full Revert + +```bash +# Revert all workflow changes +git revert +git push upstream main + +# Re-enable lts in pull_request triggers temporarily +# (Manual edit or revert to previous commit) +``` + +### Partial Rollback + +If only dispatcher is problematic: + +```bash +# Keep workflow trigger fixes +# Remove/disable dispatcher +git rm .github/workflows/scheduled-lts-release.yml +git commit -m "revert: remove dispatcher (issues found)" + +# Use manual workflow_dispatch only for releases +``` + +--- + +## Future Enhancements + +### Potential Improvements (YAGNI for now) + +1. **Slack/Discord notifications** when dispatcher runs +2. **GitHub issue creation** when dispatcher fails +3. **Automatic changelog generation** trigger from dispatcher +4. **Parallel dispatcher** (trigger all 5 workflows in parallel, not sequential) +5. **Dispatcher retry logic** if workflow trigger fails +6. **Branch protection audit workflow** to verify settings +7. **Automated testing** of publish behavior (complicated) + +--- + +## Validation Commands Reference + +```bash +# Check recent workflow runs +gh run list --limit 20 + +# Check runs on specific branch +gh run list --branch=lts --limit 10 +gh run list --branch=main --limit 10 + +# Check runs for specific workflow +gh run list --workflow=build-regular.yml --limit 10 + +# Check scheduled runs +gh run list --event=schedule --limit 10 + +# Check workflow_dispatch runs +gh run list --event=workflow_dispatch --limit 10 + +# View published tags +skopeo list-tags docker://ghcr.io/ublue-os/bluefin | grep -E "lts|testing" + +# Check branch protection +gh api repos/ublue-os/bluefin-lts/branches/lts/protection | jq + +# Verify pull bot config +cat .github/pull.yml + +# Syntax check +just check + +# Lint check +just lint +``` + +--- + +## Verification Results (2026-03-02) + +**Plan verified against current codebase:** + +✅ File paths and line numbers accurate +✅ Current state matches plan assumptions +✅ All 5 workflows have `lts` in `pull_request: branches:` (line 12) +✅ All 5 workflows have `schedule:` cron (lines 16-17) +✅ Dispatcher doesn't exist yet (needs creation) +✅ Branch protection exists but needs updating (2 approvals → 1, force push enabled → disabled) +✅ Tag naming logic verified in `reusable-build-image.yml:161-164` +✅ PRODUCTION_BRANCH constant set to `lts` in `reusable-build-image.yml:76` +✅ Publish conditions don't need changes + +**Plan Status:** ✅ Verified and ready for implementation + +--- + +**Estimated Time:** +- Implementation: 1-2 hours (workflow file edits + branch protection config) +- Testing: 1-2 hours (wait for builds, verify behavior) +- Total: 2-4 hours + +**Risk Level:** Low +- Changes are isolated to workflow triggers +- Rollback is straightforward +- Testing can be done incrementally +- No changes to build logic itself From 0b6baa925f30935a99f4d7fcc56926c52edaacc6 Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Mon, 2 Mar 2026 19:17:28 -0500 Subject: [PATCH 14/19] fix(ci): prevent branch pollution by replacing pull app with manual workflow (#1152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR implements a comprehensive 3-layer defense to prevent branch pollution caused by AI agents accidentally merging `lts` → `main`. ### Problem AI agents see branch divergence between `main` and `lts` and attempt to "sync" by merging in the wrong direction (`lts` → `main`), causing old commits to pollute the git history. ### Solution: 3-Layer Defense **Layer 1: Manual Promotion Workflow** - Replace automatic Pull app with manual GitHub Actions workflow - Created `.github/workflows/promote-to-lts.yml` (manual `workflow_dispatch` only) - Deleted `.github/pull.yml` (automatic pull app config) - Operators manually trigger promotions when ready **Layer 2: Renovate Restriction** - Updated `.github/renovate.json5` to only target `main` branch - Prevents Renovate from creating PRs against `lts` - All dependency updates flow through `main` → testing → promotion **Layer 3: Validation Build Triggers** (Critical Fix) - Added `lts` to push triggers in all 5 build workflows - Fixes missing implementation from commit 8ed6d20 - Enables validation builds when promotion PRs merge to `lts` - Builds trigger but **DO NOT publish** (cron-only publishing preserved) ### Workflow Behavior After This PR | Event | Branch | Triggers? | Publishes? | Tags | |-------|--------|-----------|------------|------| | PR to main | main | ✅ | ❌ | none | | Merge to main | main | ✅ | ✅ | `:lts-testing` | | PR to lts | lts | ❌ | ❌ | none | | **Merge to lts** | **lts** | **✅** | **❌** | **validation only** | | Cron Sun 2am | main | ✅ (dispatcher) | ❌ | none | | Dispatcher trigger | lts | ✅ | ✅ | `:lts` (production) | ### Decoupled Promotion & Release **Promotion** (manual): 1. Operator triggers `promote-to-lts.yml` workflow 2. PR auto-created from `main` → `lts` 3. Operator reviews and merges 4. Validation builds trigger (no publish) **Release** (separate): 1. Sunday cron OR manual trigger 2. `scheduled-lts-release.yml` dispatches builds on `lts` 3. Production images published to ghcr.io with `:lts` tags ### Changes Made ``` 8 files changed, 70 insertions(+), 16 deletions(-) ``` - ✅ Deleted `.github/pull.yml` - ✅ Created `.github/workflows/promote-to-lts.yml` - ✅ Updated `.github/renovate.json5` (added `baseBranches: ["main"]`) - ✅ Modified 5 build workflows (added `lts` to push triggers) ### Testing - ✅ `just check` passed - ✅ `just lint` passed (no new warnings) - 📋 After merge: Test promotion workflow creates PR correctly - 📋 After merge: Test validation builds trigger on lts merge (no publish) ### Post-Merge Actions - [ ] Manually uninstall Pull app from repository settings (user will handle) - [ ] Test promotion workflow via Actions → "Promote Main to LTS" - [ ] Verify validation builds trigger without publishing ### Related Fixes the branch pollution issue and completes the missing implementation from commit 8ed6d20. Plan documented at: `docs/plans/2026-03-02-fix-branch-pollution.md` --- .github/pull.yml | 16 ------- .github/renovate.json5 | 3 ++ .github/workflows/build-dx-hwe.yml | 1 + .github/workflows/build-dx.yml | 1 + .github/workflows/build-gdx.yml | 1 + .github/workflows/build-regular-hwe.yml | 1 + .github/workflows/build-regular.yml | 1 + .github/workflows/promote-to-lts.yml | 62 +++++++++++++++++++++++++ 8 files changed, 70 insertions(+), 16 deletions(-) delete mode 100644 .github/pull.yml create mode 100644 .github/workflows/promote-to-lts.yml diff --git a/.github/pull.yml b/.github/pull.yml deleted file mode 100644 index 720d2690..00000000 --- a/.github/pull.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: "1" -rules: - - base: lts - upstream: main - mergeMethod: hardreset - mergeUnstable: false - reviewers: - - castrojo - - tulilirockz - - hanthor - conflictReviewers: - - castrojo - - tulilirockz - - hanthor -label: "promotion" -conflictLabel: "promotion-conflict" diff --git a/.github/renovate.json5 b/.github/renovate.json5 index a6b38cac..b1cf1f4d 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -4,6 +4,9 @@ // Enable GitHub's native automerge; merging is handled by GitHub and remains subject to branch protection/bypass settings "platformAutomerge": true, + // Only target main branch - lts is production and only updated via promotion + "baseBranchPatterns": ["main"], + "extends": [ "config:best-practices", ], diff --git a/.github/workflows/build-dx-hwe.yml b/.github/workflows/build-dx-hwe.yml index dc0abb87..588160e8 100644 --- a/.github/workflows/build-dx-hwe.yml +++ b/.github/workflows/build-dx-hwe.yml @@ -12,6 +12,7 @@ on: push: branches: - main + - lts schedule: - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: diff --git a/.github/workflows/build-dx.yml b/.github/workflows/build-dx.yml index 4e09312b..3954ceec 100644 --- a/.github/workflows/build-dx.yml +++ b/.github/workflows/build-dx.yml @@ -12,6 +12,7 @@ on: push: branches: - main + - lts schedule: - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: diff --git a/.github/workflows/build-gdx.yml b/.github/workflows/build-gdx.yml index 337c34d7..fad52779 100644 --- a/.github/workflows/build-gdx.yml +++ b/.github/workflows/build-gdx.yml @@ -12,6 +12,7 @@ on: push: branches: - main + - lts schedule: - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: diff --git a/.github/workflows/build-regular-hwe.yml b/.github/workflows/build-regular-hwe.yml index 565c2bd7..d4ea3949 100644 --- a/.github/workflows/build-regular-hwe.yml +++ b/.github/workflows/build-regular-hwe.yml @@ -12,6 +12,7 @@ on: push: branches: - main + - lts schedule: - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: diff --git a/.github/workflows/build-regular.yml b/.github/workflows/build-regular.yml index 7a872185..003ae355 100644 --- a/.github/workflows/build-regular.yml +++ b/.github/workflows/build-regular.yml @@ -12,6 +12,7 @@ on: push: branches: - main + - lts schedule: - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: diff --git a/.github/workflows/promote-to-lts.yml b/.github/workflows/promote-to-lts.yml new file mode 100644 index 00000000..3d68da99 --- /dev/null +++ b/.github/workflows/promote-to-lts.yml @@ -0,0 +1,62 @@ +name: Promote Main to LTS + +on: + workflow_dispatch: + inputs: + pr_title: + description: 'Pull request title' + required: false + default: 'Promote main to lts' + pr_body: + description: 'Pull request body' + required: false + default: | + ## Summary + Promotion of tested changes from `main` to `lts` production branch. + + **IMPORTANT**: This PR should ONLY contain commits from `main` → `lts`. Never merge in the opposite direction. + +permissions: + contents: write + pull-requests: write + +jobs: + create-promotion-pr: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: lts + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Create promotion branch + id: create-branch + run: | + BRANCH_NAME="promote-main-to-lts-$(date +%Y%m%d-%H%M%S)" + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + git checkout -b "$BRANCH_NAME" + + - name: Merge main into promotion branch + run: | + git merge origin/main --no-edit -m "Merge main into lts" + + - name: Push promotion branch + run: | + git push origin ${{ steps.create-branch.outputs.branch_name }} + + - name: Create Pull Request + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr create \ + --base lts \ + --head ${{ steps.create-branch.outputs.branch_name }} \ + --title "${{ inputs.pr_title }}" \ + --body "${{ inputs.pr_body }}" \ + --label "promotion" From aa2af52a771dd3bd6154adea70217654c8acf314 Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:17:49 -0500 Subject: [PATCH 15/19] chore(deps): update cgr.dev/chainguard/wolfi-base:latest docker digest to 786c4d1 (#1149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | cgr.dev/chainguard/wolfi-base | container | digest | `9925d30` → `786c4d1` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- .github/workflows/reusable-build-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-build-image.yml b/.github/workflows/reusable-build-image.yml index 41af2a75..20378bf5 100644 --- a/.github/workflows/reusable-build-image.yml +++ b/.github/workflows/reusable-build-image.yml @@ -326,7 +326,7 @@ jobs: - generate_matrix - build_push container: - image: cgr.dev/chainguard/wolfi-base:latest@sha256:9925d3017788558fa8f27e8bb160b791e56202b60c91fbcc5c867de3175986c8 + image: cgr.dev/chainguard/wolfi-base:latest@sha256:786c4d16fa02447c89409d4c0a0c0d3ff48f6886ab5e6350e95af62d876e2373 options: --privileged --security-opt seccomp=unconfined permissions: contents: read From 39cc90cdaec2535353cb1269b7901e4c156facb9 Mon Sep 17 00:00:00 2001 From: "ubot-7274[bot]" <217212047+ubot-7274[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:17:59 -0500 Subject: [PATCH 16/19] chore(deps): update system_files/usr/share/gnome-shell/extensions/tmp/caffeine digest to 98b3b4f (#1148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | [system_files/usr/share/gnome-shell/extensions/tmp/caffeine](https://redirect.github.com/eonpatapon/gnome-shell-extension-caffeine.git) ([changelog](https://redirect.github.com/eonpatapon/gnome-shell-extension-caffeine.git/compare/07643c383db62dfcbb0485f344d063389644f2f9..98b3b4f60247d61b8d93acdd6055d5b41adbbb24)) | digest | `07643c3` → `98b3b4f` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/549) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). Co-authored-by: ubot-7274[bot] <217212047+ubot-7274[bot]@users.noreply.github.com> --- system_files/usr/share/gnome-shell/extensions/tmp/caffeine | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system_files/usr/share/gnome-shell/extensions/tmp/caffeine b/system_files/usr/share/gnome-shell/extensions/tmp/caffeine index 07643c38..98b3b4f6 160000 --- a/system_files/usr/share/gnome-shell/extensions/tmp/caffeine +++ b/system_files/usr/share/gnome-shell/extensions/tmp/caffeine @@ -1 +1 @@ -Subproject commit 07643c383db62dfcbb0485f344d063389644f2f9 +Subproject commit 98b3b4f60247d61b8d93acdd6055d5b41adbbb24 From 550e8de61f32b08c3146dae3137ecc17ca08a2eb Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Mon, 2 Mar 2026 19:45:50 -0500 Subject: [PATCH 17/19] fix(ci): prevent production LTS tag pollution from main branch merges (#1154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes a critical bug where merges to `main` branch were accidentally pushing container images to the production `:lts` tag instead of the testing `:lts-testing` tag. ## Problem The manifest generation step (line 372) had incorrect conditional logic: - **Build step (line 161)**: Simple condition `if [ "${REF_NAME}" != "${PRODUCTION_BRANCH}" ]` - adds `-testing` for all non-production branches ✅ - **Manifest step (line 372)**: Complex condition that only added `-testing` for PRs/merge groups - omitted pushes to main ❌ This caused: - Build step creates image tagged `lts-testing` ✅ - Manifest step pushes manifest with tag `lts` ❌ - **Result**: Production tag gets polluted with testing builds! ## Solution - Line 372: Changed from complex condition to simple `if [ "${REF_NAME}" != "${PRODUCTION_BRANCH}" ]` to match build step logic - Line 375: Fixed `CENTOS_VERSION_SUFFIX` to append suffix instead of replacing (preserves `-hwe` when present) ## Evidence - Bug introduced in commit `0566080` (PR #1101) which fixed the build step but forgot the manifest step - Registry shows `:lts-testing` tags exist but haven't been updated since Feb 22 (builds were cancelled) - Production `:lts` tags show recent activity through Mar 2 ## Verification - ✅ `just check && just lint` passes - ✅ Test script confirms push to main will now tag as `lts-testing` not `lts` --- .github/workflows/reusable-build-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-build-image.yml b/.github/workflows/reusable-build-image.yml index 20378bf5..654d0ba1 100644 --- a/.github/workflows/reusable-build-image.yml +++ b/.github/workflows/reusable-build-image.yml @@ -369,10 +369,10 @@ jobs: export DEFAULT_TAG="${DEFAULT_TAG}-hwe" export CENTOS_VERSION_SUFFIX="-hwe" fi - if [ "${REF_NAME}" != "${PRODUCTION_BRANCH}" ] && [ "$EVENT_NAME" == "pull_request" ] || [ "${EVENT_NAME}" == "merge_group" ] ; then + if [ "${REF_NAME}" != "${PRODUCTION_BRANCH}" ]; then export TAG_SUFFIX="testing" export DEFAULT_TAG="${DEFAULT_TAG}-${TAG_SUFFIX}" - export CENTOS_VERSION_SUFFIX="-${TAG_SUFFIX}" + export CENTOS_VERSION_SUFFIX="${CENTOS_VERSION_SUFFIX}-${TAG_SUFFIX}" fi echo "DEFAULT_TAG=${DEFAULT_TAG}" >> "${GITHUB_ENV}" echo "CENTOS_VERSION_SUFFIX=${CENTOS_VERSION_SUFFIX}" >> "${GITHUB_ENV}" From 6bb5b773aa69f4d6a1cc97669fa901822c0ad9a8 Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Mon, 2 Mar 2026 20:10:55 -0500 Subject: [PATCH 18/19] Delete docs/plans directory --- docs/plans/2026-02-14-weekly-lts-promotion.md | 1030 ----------------- .../2026-03-02-fix-lts-tag-publishing.md | 854 -------------- 2 files changed, 1884 deletions(-) delete mode 100644 docs/plans/2026-02-14-weekly-lts-promotion.md delete mode 100644 docs/plans/2026-03-02-fix-lts-tag-publishing.md diff --git a/docs/plans/2026-02-14-weekly-lts-promotion.md b/docs/plans/2026-02-14-weekly-lts-promotion.md deleted file mode 100644 index f67f78e6..00000000 --- a/docs/plans/2026-02-14-weekly-lts-promotion.md +++ /dev/null @@ -1,1030 +0,0 @@ -# Weekly LTS Promotion Workflow - Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Automate weekly LTS releases with smart retry mechanism, ensuring changes bake in -testing for 24h before promoting to stable. - -**Architecture:** GitHub Actions workflow on `main` branch with aggressive 6-hour polling Tuesday-Friday. Merges pullapp PR (main→lts) when conditions met, triggering automatic stable image builds. - -**Tech Stack:** -- GitHub Actions (scheduled workflows) -- GitHub CLI (`gh`) for PR operations -- Bash scripting for condition checks -- Branch protection bypass configuration - -**Status:** ✅ Plan reviewed and corrected (2026-02-14, round 2) - -**Fixes Applied (Round 1):** -- Fixed tracking issue week calculation (handles Tuesday edge case correctly) -- Added error handling for commit timestamp parsing -- Added force mode audit trail (logs who forced promotion) -- Fixed all relative URLs to absolute paths (PR/issue comments, workflow links) -- Enhanced verification command to show both apps and users - -**Fixes Applied (Round 2 - Code Review):** -- **C1:** Removed `--auto` from `gh pr merge` (merge immediately, not deferred) -- **C2:** Defaulted FORCE_PROMOTE to `'false'` for scheduled runs -- **C3:** Guarded `statusCheckRollup` against null (PRs with no checks) -- **I1:** Simplified 13 cron entries to single `0 */6 * * TUE-FRI` -- **I3:** Removed dead Monday/Sunday branches from week calculation -- **I5:** Fixed misleading security claim about bypass scope -- **R3:** Added concurrency group to prevent overlapping runs -- **R5:** Added fork guard (`if: github.repository == 'ublue-os/bluefin-lts'`) -- **M1:** Removed unnecessary checkout step (workflow only uses `gh` CLI) -- **M3:** Removed `promotion-override` label (YAGNI, kept in Future Enhancements) -- **M4:** Moved PR comment before merge command (PR may close on merge) - ---- - -## Problem Statement - -**Current State:** -- Changes merge to `main` → auto-publish `lts-testing` images -- Pull app bot creates PR #1100 (main → lts) automatically -- **Manual step:** Maintainer reviews and merges pullapp PR weekly -- Merge to `lts` → auto-publish stable `lts` images - -**Issues:** -1. Manual merge required every week (toil) -2. Timing inconsistent (last promotions: Feb 2, Jan 26, Jan 18 - irregular) -3. No enforcement of testing time in -testing images -4. Cron in workflow files removed (complicates branch management) - -**Goal:** -- Weekly LTS releases on Tuesdays -- Automatic promotion when ready -- Mandatory 24h baking time in -testing -- Smart retry if Tuesday blocked by timing - ---- - -## Solution Design - -### Workflow Schedule (6-Hour Polling, Tuesday-Friday) - -**Schedule:** `cron: '0 */6 * * TUE-FRI'` — runs at 00:00, 06:00, 12:00, 18:00 UTC each day, Tuesday through Friday (up to 16 attempts per week). - -**Rationale:** -- Normal case: Promotes on first Tuesday attempt -- Monday night merges: Caught by early Tuesday check (after 24h baking) -- Continuous updates: Eventually oldest commits age past 24h threshold -- Maximum delay: 5 days (Friday → next Tuesday) -- Most weekly runs are no-ops that exit in seconds after detecting a recent promotion - -### Promotion Conditions (All Must Pass) - -1. **No recent promotion:** Last merge to `lts` was ≥7 days ago -2. **Pullapp PR exists:** Active PR from `main` to `lts` by pull[bot] -3. **Has changes:** PR contains at least 1 commit -4. **No hold:** PR does not have `promotion-hold` label -5. **CI passing:** All status checks succeeded or skipped -6. **24h baking:** Newest commit in PR is ≥24 hours old (strictest check) - -### Behavior - -**On Success:** -- Adds comment explaining automated promotion (before merge) -- Merges pullapp PR immediately with `--merge` strategy -- Closes tracking issue (if exists) -- Workflow skips remaining attempts until next Tuesday - -**On Block:** -- Logs reason to workflow output -- Creates/updates tracking issue (first block only) -- Waits 6 hours for next attempt -- Retries until Friday final attempt - -**On Permanent Block:** -- Tracking issue remains open with status -- Next Tuesday starts new cycle -- Manual intervention available via labels - ---- - -## Prerequisites - -### 1. Branch Protection Configuration (REQUIRED) - -**Current State:** -- `lts` branch requires 2 approving reviews before merge -- No bypass allowances configured -- Default `GITHUB_TOKEN` cannot auto-merge - -**Required Change (Repository Admin):** - -1. Navigate to repository settings - ``` - https://github.com/ublue-os/bluefin-lts/settings/branch_protection_rules - ``` - -2. Edit `lts` branch protection rule - -3. Under "Require pull request reviews before merging": - - Enable: "Allow specified actors to bypass pull request requirements" - - Add actor: `github-actions[bot]` or the GitHub Actions app - - Save changes - -**Security Note:** Adding `github-actions[bot]` to the bypass list allows ANY workflow using `GITHUB_TOKEN` to bypass reviews on `lts`. The promote-lts workflow restricts itself via the pullapp author check (software guard), but the platform-level bypass is broader. Consider this acceptable given the `lts` branch only receives merges from the pull app. - -**Verification:** -```bash -gh api repos/ublue-os/bluefin-lts/branches/lts/protection/required_pull_request_reviews \ - --jq '{apps: [.bypass_pull_request_allowances.apps[]? | .slug], users: [.bypass_pull_request_allowances.users[]? | .login]}' -# Should show github-actions in the apps array -``` - -### 2. Labels Creation - -Create repository labels: -```bash -gh label create "promotion-tracking" \ - --description "Tracks weekly LTS promotion status" \ - --color "0E8A16" - -gh label create "promotion-hold" \ - --description "Blocks automated LTS promotion" \ - --color "D93F0B" -``` - ---- - -## Task 1: Create Promotion Workflow - -**Files:** -- Create: `.github/workflows/promote-lts.yml` - -**Step 1: Create workflow file with schedule** - -Create `.github/workflows/promote-lts.yml`: - -```yaml -name: Promote LTS Release - -on: - schedule: - # Every 6 hours Tuesday-Friday. Most weeks promote on the first - # Tuesday attempt; the remaining runs are no-ops that exit early. - - cron: '0 */6 * * TUE-FRI' - - workflow_dispatch: - inputs: - force: - description: 'Skip 24h age check (emergency use only)' - type: boolean - default: false - required: false - -permissions: - contents: write - pull-requests: write - issues: write - -concurrency: - group: promote-lts - cancel-in-progress: false - -jobs: - promote: - # Only run in the upstream repo, not in forks - if: github.repository == 'ublue-os/bluefin-lts' - runs-on: ubuntu-latest - steps: - - name: Check Promotion Conditions - id: check - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FORCE_PROMOTE: ${{ inputs.force || 'false' }} - run: | - set -euo pipefail - - echo "::group::Check 1: Already promoted this week?" - LAST_MERGE=$(gh api repos/${{ github.repository }}/branches/lts --jq '.commit.commit.committer.date') - LAST_MERGE_TS=$(date -d "$LAST_MERGE" +%s) - NOW_TS=$(date +%s) - HOURS_SINCE=$(( ($NOW_TS - $LAST_MERGE_TS) / 3600 )) - - echo "Last promotion: $LAST_MERGE ($HOURS_SINCE hours ago)" - - if [ $HOURS_SINCE -lt 168 ]; then - echo "result=skip" >> $GITHUB_OUTPUT - echo "reason=Already promoted this week ($HOURS_SINCE hours ago)" >> $GITHUB_OUTPUT - echo "✓ Already promoted recently" - exit 0 - fi - echo "✓ Last promotion was $HOURS_SINCE hours ago (>168h threshold)" - echo "::endgroup::" - - echo "::group::Check 2: Find pullapp PR" - PULLAPP_PR=$(gh pr list --base lts --author "app/pull" --state open --limit 1 --json number --jq '.[0].number // empty') - - if [ -z "$PULLAPP_PR" ]; then - echo "result=skip" >> $GITHUB_OUTPUT - echo "reason=No pullapp PR found (main → lts)" >> $GITHUB_OUTPUT - echo "⚠️ No pullapp PR exists" - exit 0 - fi - - echo "pr_number=$PULLAPP_PR" >> $GITHUB_OUTPUT - echo "✓ Found pullapp PR #$PULLAPP_PR" - echo "::endgroup::" - - echo "::group::Check 3: PR has commits?" - COMMIT_COUNT=$(gh pr view $PULLAPP_PR --json commits --jq '.commits | length') - - if [ $COMMIT_COUNT -eq 0 ]; then - echo "result=skip" >> $GITHUB_OUTPUT - echo "reason=PR #$PULLAPP_PR is empty (no changes)" >> $GITHUB_OUTPUT - echo "⏭️ PR is empty" - exit 0 - fi - - echo "✓ PR has $COMMIT_COUNT commits" - echo "::endgroup::" - - echo "::group::Check 4: promotion-hold label?" - if gh pr view $PULLAPP_PR --json labels --jq '.labels[].name' | grep -q "promotion-hold"; then - echo "result=blocked" >> $GITHUB_OUTPUT - echo "reason=Blocked by 'promotion-hold' label" >> $GITHUB_OUTPUT - echo "🛑 Promotion on hold" - exit 0 - fi - echo "✓ No hold label" - echo "::endgroup::" - - echo "::group::Check 5: CI status" - FAILED_CHECKS=$(gh pr view $PULLAPP_PR --json statusCheckRollup --jq '[(.statusCheckRollup // [])[] | select(.conclusion != "SUCCESS" and .conclusion != "SKIPPED" and .conclusion != null)] | length') - - if [ $FAILED_CHECKS -gt 0 ]; then - echo "result=blocked" >> $GITHUB_OUTPUT - echo "reason=CI checks failing ($FAILED_CHECKS failures)" >> $GITHUB_OUTPUT - echo "⛔ CI failing" - exit 0 - fi - echo "✓ All CI checks passing" - echo "::endgroup::" - - echo "::group::Check 6: Newest commit age (24h requirement)" - NEWEST_COMMIT=$(gh pr view $PULLAPP_PR --json commits --jq '.commits[-1].committedDate') - - if [ -z "$NEWEST_COMMIT" ] || [ "$NEWEST_COMMIT" == "null" ]; then - echo "result=blocked" >> $GITHUB_OUTPUT - echo "reason=Could not determine newest commit timestamp" >> $GITHUB_OUTPUT - echo "⚠️ No commit timestamp found" - exit 0 - fi - - COMMIT_TS=$(date -d "$NEWEST_COMMIT" +%s) - AGE_HOURS=$(( ($NOW_TS - $COMMIT_TS) / 3600 )) - - echo "Newest commit: $NEWEST_COMMIT" - echo "Age: $AGE_HOURS hours" - echo "commit_age=$AGE_HOURS" >> $GITHUB_OUTPUT - - if [ "$FORCE_PROMOTE" != "true" ] && [ $AGE_HOURS -lt 24 ]; then - echo "result=blocked" >> $GITHUB_OUTPUT - echo "reason=Newest commit only ${AGE_HOURS}h old (requires 24h)" >> $GITHUB_OUTPUT - echo "⏳ Too recent" - exit 0 - fi - - if [ "$FORCE_PROMOTE" == "true" ]; then - echo "⚠️ Force mode: Skipping age check" - echo "force_used=true" >> $GITHUB_OUTPUT - else - echo "✓ Commit age ${AGE_HOURS}h (≥24h)" - echo "force_used=false" >> $GITHUB_OUTPUT - fi - echo "::endgroup::" - - echo "result=promote" >> $GITHUB_OUTPUT - echo "reason=All conditions met" >> $GITHUB_OUTPUT - echo "✅ Ready to promote!" - - - name: Promote to LTS - if: steps.check.outputs.result == 'promote' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.check.outputs.pr_number }} - COMMIT_AGE: ${{ steps.check.outputs.commit_age }} - FORCE_USED: ${{ steps.check.outputs.force_used }} - run: | - set -euo pipefail - - echo "🚀 Promoting PR #$PR_NUMBER to lts branch" - - # Add force mode audit comment if applicable - if [ "$FORCE_USED" == "true" ]; then - gh pr comment $PR_NUMBER --body "⚠️ **FORCED PROMOTION** - -Initiated by: @${{ github.actor }} -Age check bypassed via workflow_dispatch force parameter. - -[View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" - fi - - # Add explanatory comment before merge (PR may close on merge, - # preventing post-merge comments) - gh pr comment $PR_NUMBER --body "🎉 **Automated LTS Promotion** - -Promoted to stable \`lts\` tag after passing all conditions: -- ✅ Commits baked in \`lts-testing\` for ${COMMIT_AGE}+ hours -- ✅ All CI checks passing -- ✅ Weekly promotion window (Tuesday-Friday) - -Stable images will publish shortly via push trigger to \`lts\` branch. - ---- -*Automated by [Promote LTS Release workflow](https://github.com/${{ github.repository }}/actions/workflows/promote-lts.yml)* -*Run: [#${{ github.run_number }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*" - - # Merge immediately (not --auto). All conditions already verified above. - # NOTE: Using --merge (merge commit). The pull app uses hardreset to sync - # main→lts, so the next sync will force-push regardless of merge strategy. - gh pr merge $PR_NUMBER --merge - - echo "✅ Promotion complete" - - - name: Create/Update Tracking Issue - if: steps.check.outputs.result == 'blocked' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BLOCK_REASON: ${{ steps.check.outputs.reason }} - PR_NUMBER: ${{ steps.check.outputs.pr_number }} - run: | - set -euo pipefail - - # Calculate current week's Tuesday - # Cron only fires Tue-Fri, so DAY_OF_WEEK is always 2-5 - DAY_OF_WEEK=$(date +%u) # 1=Mon, 2=Tue, ..., 7=Sun - if [ $DAY_OF_WEEK -eq 2 ]; then - # Today is Tuesday, use today - WEEK_START=$(date +%Y-%m-%d) - else - # Wed-Fri - calculate days back to Tuesday - DAYS_BACK=$(( $DAY_OF_WEEK - 2 )) - WEEK_START=$(date -d "$DAYS_BACK days ago" +%Y-%m-%d) - fi - - ISSUE_TITLE="LTS Promotion Tracking: Week of $WEEK_START" - - # Check if tracking issue exists for this week - EXISTING=$(gh issue list --label "promotion-tracking" --state open --search "$WEEK_START" --json number --jq '.[0].number // empty') - - CURRENT_TIME=$(date -u '+%Y-%m-%d %H:%M UTC') - NEXT_CHECK=$(date -u -d '+6 hours' '+%Y-%m-%d %H:%M UTC') - - if [ -z "$EXISTING" ]; then - # Create new tracking issue - echo "Creating tracking issue for week of $WEEK_START" - gh issue create \ - --title "$ISSUE_TITLE" \ - --label "promotion-tracking" \ - --body "## LTS Promotion Status - -**Target:** Weekly promotion (Tuesdays) -**Status:** ⏳ Waiting for conditions - -### Current Blocker -- **$CURRENT_TIME:** $BLOCK_REASON - -### Retry Schedule -Automatic checks every 6 hours (00:00, 06:00, 12:00, 18:00 UTC): -- **Tuesday-Friday:** Up to 16 attempts per week - -### Manual Override -- **Pause promotion:** Add \`promotion-hold\` label to [PR #$PR_NUMBER](https://github.com/${{ github.repository }}/pull/$PR_NUMBER) -- **Force promote:** Run workflow manually with force option -- **See details:** [Latest run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) - ---- -*Auto-managed by [Promote LTS Release workflow](https://github.com/${{ github.repository }}/actions/workflows/promote-lts.yml)*" - else - # Update existing issue - echo "Updating tracking issue #$EXISTING" - gh issue comment $EXISTING --body "**$CURRENT_TIME:** ⏳ Still waiting -- **Blocker:** $BLOCK_REASON -- **Next check:** $NEXT_CHECK -- **Details:** [Run #${{ github.run_number }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" - fi - - - name: Close Tracking Issue - if: steps.check.outputs.result == 'promote' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.check.outputs.pr_number }} - run: | - set -euo pipefail - - # Find and close tracking issue (same week calculation as above) - DAY_OF_WEEK=$(date +%u) - if [ $DAY_OF_WEEK -eq 2 ]; then - WEEK_START=$(date +%Y-%m-%d) - else - DAYS_BACK=$(( $DAY_OF_WEEK - 2 )) - WEEK_START=$(date -d "$DAYS_BACK days ago" +%Y-%m-%d) - fi - - EXISTING=$(gh issue list --label "promotion-tracking" --state open --search "$WEEK_START" --json number --jq '.[0].number // empty') - - if [ -n "$EXISTING" ]; then - echo "Closing tracking issue #$EXISTING" - CURRENT_TIME=$(date -u '+%Y-%m-%d %H:%M UTC') - - gh issue comment $EXISTING --body "**$CURRENT_TIME:** ✅ **Promoted!** - -PR #$PR_NUMBER merged to \`lts\` branch. -Stable images will publish via automatic build triggers. - -[View promotion run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" - - gh issue close $EXISTING - fi - - - name: Summary - if: always() - env: - RESULT: ${{ steps.check.outputs.result }} - REASON: ${{ steps.check.outputs.reason }} - run: | - echo "### Promotion Result: ${RESULT:-unknown}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Reason:** ${REASON:-No reason provided}" >> $GITHUB_STEP_SUMMARY - - if [ "$RESULT" == "promote" ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ LTS promotion successful!" >> $GITHUB_STEP_SUMMARY - elif [ "$RESULT" == "blocked" ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "⏳ Will retry in 6 hours" >> $GITHUB_STEP_SUMMARY - fi -``` - -**Step 2: Validate workflow syntax** - -```bash -# Check YAML syntax -yamllint .github/workflows/promote-lts.yml - -# Or use actionlint if available -actionlint .github/workflows/promote-lts.yml -``` - -Expected: No syntax errors - -**Step 3: Commit the workflow** - -```bash -git add .github/workflows/promote-lts.yml -git commit -m "feat(ci): add weekly LTS promotion workflow - -Automates weekly promotion of main → lts with: -- 6-hour polling Tuesday-Friday via single cron entry -- 24h minimum baking time in lts-testing -- Smart retry mechanism for timing edge cases -- Tracking issues for blocked promotions -- Fork guard and concurrency control - -Requires: GitHub Actions bypass on lts branch protection - -Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" -``` - ---- - -## Task 2: Create Repository Labels - -**Step 1: Create promotion-tracking label** - -```bash -gh label create "promotion-tracking" \ - --description "Tracks weekly LTS promotion status" \ - --color "0E8A16" \ - || echo "Label already exists" -``` - -**Step 2: Create promotion-hold label** - -```bash -gh label create "promotion-hold" \ - --description "Blocks automated LTS promotion" \ - --color "D93F0B" \ - || echo "Label already exists" -``` - -**Step 3: Verify labels created** - -```bash -gh label list | grep promotion -``` - -Expected output: -``` -promotion-hold Blocks automated LTS promotion -promotion-tracking Tracks weekly LTS promotion status -``` - -**Step 4: Commit label creation documentation** - -Update or create `.github/labels.md`: - -```markdown -# Repository Labels - -## Promotion Labels - -- **promotion-tracking**: Automatically applied to weekly promotion tracking issues -- **promotion-hold**: Apply to pullapp PR to pause automated promotion -``` - -```bash -git add .github/labels.md -git commit -m "docs: document promotion workflow labels" -``` - ---- - -## Task 3: Request Branch Protection Bypass - -**Step 1: Create issue for admin action** - -```bash -gh issue create \ - --title "Configure branch protection bypass for automated LTS promotion" \ - --label "infrastructure" \ - --assignee castrojo \ - --body "## Required Configuration - -To enable the automated LTS promotion workflow, we need to configure branch protection bypass. - -### Action Required (Repository Admin) - -1. Navigate to [Branch Protection Settings](https://github.com/ublue-os/bluefin-lts/settings/branches) -2. Edit the \`lts\` branch protection rule -3. Under **\"Require pull request reviews before merging\"**: - - ✅ Enable: \"Allow specified actors to bypass pull request requirements\" - - ➕ Add actor: \`github-actions[bot]\` - - 💾 Save changes - -### Why This Is Needed - -- The \`lts\` branch currently requires 2 approving reviews -- The promotion workflow needs to auto-merge the pullapp PR (main → lts) -- Without bypass, workflow cannot merge despite passing all conditions - -### Security - -- Adding \`github-actions[bot]\` to bypass allows any workflow using GITHUB_TOKEN to bypass reviews on \`lts\` -- The promote-lts workflow restricts itself to pullapp PRs via author check (software guard) -- All other PRs from humans still require reviews -- Acceptable risk: \`lts\` branch only receives merges from the pull app - -### Verification - -After configuration, verify with: -\`\`\`bash -gh api repos/ublue-os/bluefin-lts/branches/lts/protection/required_pull_request_reviews \\ - --jq '.bypass_pull_request_allowances.apps[] | select(.slug == \"github-actions\")' -\`\`\` - -Should return GitHub Actions app details. - -### Related - -- Workflow: \`.github/workflows/promote-lts.yml\` -- Implementation plan: \`docs/plans/2026-02-14-weekly-lts-promotion.md\`" -``` - -**Step 2: Document current configuration** - -```bash -echo "Documenting current branch protection state..." - -gh api repos/ublue-os/bluefin-lts/branches/lts/protection \ - --jq '{ - required_reviews: .required_pull_request_reviews.required_approving_review_count, - bypass_actors: .required_pull_request_reviews.bypass_pull_request_allowances - }' > docs/branch-protection-lts-before.json - -git add docs/branch-protection-lts-before.json -git commit -m "docs: snapshot lts branch protection before bypass config" -``` - ---- - -## Task 4: Testing Strategy - -### Manual Testing (Before First Tuesday) - -**Test 1: Workflow syntax and basic execution** - -```bash -# Trigger workflow manually (dry run) -gh workflow run promote-lts.yml -``` - -Check workflow output for: -- All condition checks execute -- No script errors -- Appropriate skip/block reason - -**Test 2: Simulate conditions with test PR** - -Option A: Use existing pullapp PR -```bash -# Check current PR state -gh pr view 1100 --json number,commits,statusCheckRollup,labels - -# Workflow will skip if: -# - Last promotion was <7 days ago (expected) -# - Will log reason and exit cleanly -``` - -Option B: Create test label scenario -```bash -# Add promotion-hold label -gh pr edit 1100 --add-label "promotion-hold" - -# Run workflow -gh workflow run promote-lts.yml - -# Should detect label and block -# Check run output - -# Remove label -gh pr edit 1100 --remove-label "promotion-hold" -``` - -**Test 3: Force promotion (if bypass configured)** - -```bash -# ONLY IF: Admin has configured bypass AND you want to test merge -gh workflow run promote-lts.yml --field force=true - -# WARNING: This will actually merge the PR! -# Only use if: -# - Bypass is configured -# - PR is ready to promote -# - You understand this publishes stable images -``` - -### Monitoring First Scheduled Run - -**Next Tuesday 10am UTC:** - -1. Watch workflow execution: -```bash -gh run watch -``` - -2. Check for tracking issue creation: -```bash -gh issue list --label "promotion-tracking" -``` - -3. Verify behavior matches expectations: - - If conditions met: PR merges, images build - - If blocked: Issue created, will retry 4pm - -4. Monitor lts branch builds: -```bash -gh run list --branch lts --limit 5 -``` - ---- - -## Task 5: Documentation Updates - -**Step 1: Update CONTRIBUTING.md (if exists)** - -Add section about automated releases: - -```markdown -## Automated LTS Releases - -### Release Schedule - -LTS stable images are automatically promoted **weekly on Tuesdays** (or within that week if delayed). - -### How It Works - -1. Changes merge to `main` → `-testing` images published -2. Changes "bake" in testing for minimum 24 hours -3. Weekly promotion workflow checks conditions Tuesday-Friday -4. When ready, pullapp PR auto-merges → stable `lts` images publish - -### Blocking a Release - -If you need to hold a release: - -```bash -gh pr edit --add-label "promotion-hold" -``` - -Remove the label when ready to resume: - -```bash -gh pr edit --remove-label "promotion-hold" -``` - -### Monitoring - -Track promotion status via: -- Tracking issues labeled `promotion-tracking` -- [Promote LTS Release workflow runs](../actions/workflows/promote-lts.yml) -``` - -**Step 2: Create operational runbook** - -Create `docs/runbooks/lts-promotion.md`: - -```markdown -# LTS Promotion Runbook - -## Normal Operations - -The LTS promotion workflow runs automatically Tuesday-Friday every 6 hours. - -**Schedule:** Every 6 hours (00:00, 06:00, 12:00, 18:00 UTC), Tuesday through Friday. - -## Common Scenarios - -### Scenario: Promotion Delayed by Recent Commits - -**Symptom:** Tracking issue shows "newest commit only Xh old" - -**Expected Behavior:** Workflow will automatically retry every 6 hours until 24h threshold met - -**Action:** None required, monitor tracking issue for updates - -### Scenario: Need to Hold Promotion - -**Use Case:** Critical bug found in -testing images - -**Action:** -```bash -# Find pullapp PR -gh pr list --base lts --author "app/pull" - -# Add hold label -gh pr edit --add-label "promotion-hold" -``` - -**Result:** Workflow will skip until label removed - -### Scenario: Force Promotion (Emergency) - -**Use Case:** Critical fix needs immediate deployment - -**Action:** -```bash -gh workflow run promote-lts.yml --field force=true -``` - -**Warning:** Bypasses 24h age check. Use sparingly. - -### Scenario: Workflow Not Running - -**Check 1:** Verify workflow exists and is enabled -```bash -gh workflow view promote-lts.yml -``` - -**Check 2:** Check for workflow errors -```bash -gh run list --workflow=promote-lts.yml --limit 5 -``` - -**Check 3:** Verify branch protection configured -```bash -gh api repos/ublue-os/bluefin-lts/branches/lts/protection/required_pull_request_reviews \ - --jq '.bypass_pull_request_allowances.apps' -``` - -Should show github-actions app. - -## Metrics - -### Success Rate - -```bash -# Count successful promotions per month -gh run list --workflow=promote-lts.yml --status success --created ">=YYYY-MM-01" --json conclusion --jq 'length' -``` - -### Average Promotion Time - -```bash -# Time from PR creation to merge -# Manual analysis of tracking issues -``` - -## Troubleshooting - -### Workflow Runs But Doesn't Merge - -**Possible Causes:** -1. Branch protection bypass not configured -2. GitHub Actions doesn't have write permissions -3. PR conflicts exist -4. PR was closed/merged manually - -**Check:** -```bash -# View latest run logs -gh run view --log - -# Check PR status -gh pr view --json mergeable,mergeStateStatus -``` - -### Tracking Issue Not Created - -**Expected:** Only created on first block attempt - -**Check:** Look for existing open issue with `promotion-tracking` label - -### Too Many Retries - -**If Friday final attempt still blocked:** -- Promotion deferred to next Tuesday -- Review tracking issue for persistent blockers -- Consider manual intervention if urgent -``` - -**Step 3: Commit documentation** - -```bash -git add docs/runbooks/lts-promotion.md -git add CONTRIBUTING.md # if modified -git commit -m "docs: add LTS promotion runbook and update contributing guide" -``` - ---- - -## Verification Checklist - -After full implementation: - -- [ ] Workflow file `.github/workflows/promote-lts.yml` exists and is valid -- [ ] Labels created: `promotion-tracking`, `promotion-hold` -- [ ] Issue created for admin to configure branch protection bypass -- [ ] Branch protection bypass configured (admin action) -- [ ] Workflow tested manually with `workflow_dispatch` -- [ ] Documentation updated (CONTRIBUTING.md, runbooks) -- [ ] Team notified of new automated promotion process -- [ ] First Tuesday run monitored and successful - ---- - -## Rollback Plan - -If automation causes issues: - -**Step 1: Disable workflow** - -```bash -gh workflow disable promote-lts.yml -``` - -**Step 2: Close open tracking issues** - -```bash -gh issue list --label "promotion-tracking" --state open --json number \ - | jq -r '.[].number' \ - | xargs -I {} gh issue close {} --comment "Promotion workflow disabled, reverting to manual process" -``` - -**Step 3: Remove branch protection bypass** - -Admin action: Remove `github-actions[bot]` from lts branch bypass list - -**Step 4: Return to manual promotion** - -Resume manual merging of pullapp PRs as before - ---- - -## Success Metrics - -**Week 1-2 (Validation Phase):** -- Workflow runs on schedule -- Tracking issues created/closed appropriately -- At least one successful auto-promotion -- No false merges (promoting when conditions not met) - -**Week 3-4 (Optimization Phase):** -- 90%+ promotions happen Tuesday or Wednesday -- <5% require manual intervention -- Team reports reduced toil - -**Month 2+ (Steady State):** -- Weekly promotions occur automatically -- Tracking issues rare (most promote first try) -- Zero missed promotions -- Clear audit trail of promotion history - ---- - -## Future Enhancements - -**Phase 2 Improvements:** - -1. **Slack/Discord notifications** on promotion success/failure -2. **Promotion override label** for emergency bypasses -3. **Metrics dashboard** showing promotion timing trends -4. **Automatic changelog** generation from promoted commits -5. **Dry-run mode** for testing changes to workflow -6. **Smarter retry logic** based on CI completion time - -**Phase 3 Considerations:** - -1. **Multiple promotion tracks** (stable, beta, etc.) -2. **Staged rollout** with canary deployments -3. **Integration with release notes** generation -4. **Automatic issue triage** for build failures - ---- - -## References - -- GitHub Actions: https://docs.github.com/en/actions -- Branch Protection: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches -- Pull App Bot: https://github.com/apps/pull -- Bluefin LTS repo: https://github.com/ublue-os/bluefin-lts - ---- - -**Plan created:** 2026-02-14 -**Estimated implementation time:** 2-3 hours + admin config + 1 week monitoring -**Risk level:** Low-Medium (can disable anytime, manual override available) -**Complexity:** Medium (workflow logic, branch protection, testing) - ---- - -## Implementation Notes - -### Execution Approach - -This plan can be executed in two ways: - -**Option A: Sequential (Recommended for first time)** -- Implement Task 1 → Test → Implement Task 2 → etc. -- Allows validation at each step -- Easier to debug issues - -**Option B: Parallel (Faster, requires coordination)** -- Task 1 & Task 2 (workflow + labels) can be done in parallel -- Task 3 (bypass config) is blocking for actual promotion -- Task 4 (testing) depends on Tasks 1-3 -- Task 5 (docs) can be done anytime - -**Recommended:** Sequential approach, especially for initial implementation. - -### Key Decision Points - -**Before Task 3:** -- Confirm team agrees with 24h baking requirement -- Review schedule (every 6 hours Tue-Fri appropriate?) -- Verify label names match team conventions - -**Before First Tuesday:** -- Ensure at least one maintainer understands the workflow -- Document emergency contact if workflow fails -- Have rollback plan ready - -### Coordination Required - -**With Repository Admins:** -- Task 3 requires admin action (branch protection) -- Can't proceed to production without this -- Estimated time: 5 minutes for admin - -**With Team:** -- Notify team of automation start date -- Share runbook and emergency procedures -- Set expectations for first few weeks (monitoring period) - ---- - -## Appendix: Example Timeline - -**Week 0 (Implementation):** -- Monday: Implement Tasks 1-2, create admin request (Task 3) -- Tuesday: Admin configures bypass, testing (Task 4) -- Wednesday: Documentation (Task 5), team notification -- Thursday-Friday: Buffer for issues - -**Week 1 (First Run):** -- Tuesday 10am: First automatic attempt (likely succeeds) -- Monitor throughout week -- Document any issues - -**Week 2-3 (Validation):** -- Continue monitoring -- Adjust schedule/conditions if needed -- Gather feedback from team - -**Week 4+ (Steady State):** -- Automation running smoothly -- Weekly promotions happen automatically -- Manual intervention only for holds or emergencies diff --git a/docs/plans/2026-03-02-fix-lts-tag-publishing.md b/docs/plans/2026-03-02-fix-lts-tag-publishing.md deleted file mode 100644 index 8827011f..00000000 --- a/docs/plans/2026-03-02-fix-lts-tag-publishing.md +++ /dev/null @@ -1,854 +0,0 @@ -# Fix LTS Tag Publishing - Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Prevent accidental production tag publishes from pull bot PRs to `lts` branch. Production tags (`:lts`, `:lts.YYYYMMDD`) should ONLY publish via weekly cron schedule or manual workflow_dispatch on `lts` branch. - -**Architecture:** Remove `lts` from workflow pull_request triggers. Create dispatcher workflow on `main` that triggers production builds on `lts` via workflow_dispatch. Update GitHub branch protection to enforce `lts` branch discipline. - -**Tech Stack:** -- GitHub Actions (scheduled workflows, workflow_dispatch, reusable workflows) -- GitHub CLI (`gh`) for workflow dispatch -- Branch protection rules - -**Status:** ✅ Design improved and ready for implementation (v2 - added lts validation builds) - ---- - -## Problem Statement - -**Current Broken Behavior (as of 2026-03-02):** - -1. **Pull bot creates PR from `main` → `lts`** (PR #1144, opened ~17:08 UTC) -2. **Workflows trigger on the PR** because all 5 build workflows have: - ```yaml - pull_request: - branches: - - lts # ← THIS IS THE BUG - ``` -3. **Production tags get published** from PR events (not just from `lts` branch merges) -4. **Tags published about an hour ago** when they should only publish via weekly Sunday 2 AM UTC cron - -**Evidence:** -- Run #22586907105: "Build Bluefin LTS" triggered by PR event -- Run #22586905020: "Build Bluefin LTS DX" triggered by PR event -- Run #22586905071: "Build Bluefin LTS GDX" triggered by PR event -- All published production `:lts` tags despite being PR-triggered - -**Root Cause:** -Commit a3e9a6a (on `main`, not yet on `lts`) attempted to fix this by removing `lts` from `push:` triggers, but **left `lts` in `pull_request:` triggers**. This is incomplete. - -**Additional Issue:** -GitHub Actions `schedule:` triggers ALWAYS run on the default branch (`main`), not `lts`. Current cron would build from `main` branch, not production `lts` branch. - ---- - -## Solution Design - -### Core Strategy - -**Workflow Trigger Matrix:** - -| Event | Branch | Should Trigger? | Should Publish? | Tags Published | -|-------|--------|----------------|-----------------|----------------| -| PR opened | `main` | ✅ Yes | ❌ No | none (validation only) | -| PR merged | `main` | ✅ Yes (push) | ✅ Yes | `:lts-testing` | -| Pull bot PR | `lts` | ❌ **NO** | ❌ No | none | -| Pull bot merge | `lts` | ✅ **YES** (validation) | ❌ **NO** | none (build only) | -| Cron (Sun 2am) | `main` | ✅ Yes (dispatcher) | ❌ No | none (dispatcher only) | -| Dispatcher trigger | `lts` | ✅ Yes (workflow_dispatch) | ✅ Yes | `:lts` (production) | -| Manual dispatch | `lts` | ✅ Yes | ✅ Yes | `:lts` (production) | -| Manual dispatch | `main` | ✅ Yes | ✅ Yes | `:lts-testing` | - -### Dispatcher Pattern (Solve Cron on Wrong Branch) - -Since cron runs on `main` (default branch), we need: - -``` -schedule (on main) → dispatcher workflow → workflow_dispatch (on lts) → builds + publish -``` - -**Flow:** -1. Sunday 2 AM UTC: `scheduled-lts-release.yml` runs on `main` -2. Dispatcher uses `gh workflow run` to trigger all 5 builds on `lts` branch -3. Builds run on `lts` with `workflow_dispatch` event -4. Publish condition allows publishes from `lts` branch -5. Production tags (`:lts`) get published - -### Branch Protection Enforcement - -Configure `lts` branch protection to: -- Require PR approval before merging -- Prevent direct pushes (except pull bot + maintainers) -- Disable force pushes -- Ensure only vetted code reaches production - ---- - -## Implementation Tasks - -### Task 1: Create Dispatcher Workflow - -**File:** -- Create: `.github/workflows/scheduled-lts-release.yml` - -**Step 1: Write the dispatcher workflow** - -```yaml -name: Scheduled LTS Release - -on: - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC - workflow_dispatch: # Allow manual triggering - -permissions: - contents: read - actions: write - -jobs: - trigger-lts-builds: - runs-on: ubuntu-latest - steps: - - name: Trigger all LTS builds on lts branch - env: - GH_TOKEN: ${{ github.token }} - run: | - # Trigger all 5 build workflows on lts branch - gh workflow run build-regular.yml --ref lts -R ${{ github.repository }} - gh workflow run build-dx.yml --ref lts -R ${{ github.repository }} - gh workflow run build-gdx.yml --ref lts -R ${{ github.repository }} - gh workflow run build-regular-hwe.yml --ref lts -R ${{ github.repository }} - gh workflow run build-dx-hwe.yml --ref lts -R ${{ github.repository }} - - echo "✅ Triggered all 5 LTS build workflows on lts branch" - echo "View workflow runs at: ${{ github.server_url }}/${{ github.repository }}/actions" -``` - -**Step 2: Update publish condition** - -**Current (line 33):** -```yaml -publish: ${{ github.event_name != 'pull_request' }} -``` - -**Fixed:** -```yaml -publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} -``` - -**Why:** -- Publish on `workflow_dispatch` (cron dispatcher + manual on lts) -- Publish on `push` to `main` (testing tags) -- Do NOT publish on `push` to `lts` (pull bot validation builds) - -**Step 3: Validate syntax** - -Run: `just check` -Expected: No errors - -**Step 4: Commit the fix** - -```bash -git add .github/workflows/build-regular.yml -git commit -m "fix(ci): update build-regular.yml triggers and publish condition - -Changes: -- Remove lts from pull_request trigger (no PR builds) -- Add lts to push trigger (validation builds on merge) -- Update publish condition (only workflow_dispatch or push to main) -- Remove schedule (moved to dispatcher) - -This prevents accidental production tag publishes from pull bot PRs -while maintaining validation builds on pull bot merges. - -Production publishes only via: -- Weekly cron dispatcher (scheduled-lts-release.yml) -- Manual workflow_dispatch on lts branch - -Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" -``` - ---- - -### Task 3: Fix build-dx.yml Triggers - -**File:** -- Modify: `.github/workflows/build-dx.yml` - -**Step 1: Apply same trigger and publish fixes as build-regular.yml** - -Remove `- lts` from `pull_request: branches:` (line 12) -Add `- lts` to `push: branches:` (after line 15) -Remove `schedule:` section (lines 16-17) -Update `publish:` condition (line 34) to: -```yaml -publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} -``` - -**Step 2: Validate syntax** - -Run: `just check` -Expected: No errors - -**Step 3: Commit the fix** - -```bash -git add .github/workflows/build-dx.yml -git commit -m "fix(ci): update build-dx.yml triggers and publish condition - -Same fix as build-regular.yml - prevents accidental production -tag publishes from pull bot PRs while maintaining validation builds. - -Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" -``` - ---- - -### Task 4: Fix build-gdx.yml Triggers - -**File:** -- Modify: `.github/workflows/build-gdx.yml` - -**Step 1: Apply same trigger and publish fixes** - -Remove `- lts` from `pull_request: branches:` -Add `- lts` to `push: branches:` -Remove `schedule:` section -Update `publish:` condition (line 35) to: -```yaml -publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} -``` - -**Step 2: Validate syntax** - -Run: `just check` -Expected: No errors - -**Step 3: Commit the fix** - -```bash -git add .github/workflows/build-gdx.yml -git commit -m "fix(ci): update build-gdx.yml triggers and publish condition - -Same fix as other build workflows. - -Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" -``` - ---- - -### Task 5: Fix build-regular-hwe.yml Triggers - -**File:** -- Modify: `.github/workflows/build-regular-hwe.yml` - -**Step 1: Apply same trigger and publish fixes** - -Remove `- lts` from `pull_request: branches:` -Add `- lts` to `push: branches:` -Remove `schedule:` section -Update `publish:` condition (line 39) to: -```yaml -publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} -``` - -**Step 2: Validate syntax** - -Run: `just check` -Expected: No errors - -**Step 3: Commit the fix** - -```bash -git add .github/workflows/build-regular-hwe.yml -git commit -m "fix(ci): update build-regular-hwe.yml triggers and publish condition - -Same fix as other build workflows. - -Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" -``` - ---- - -### Task 6: Fix build-dx-hwe.yml Triggers - -**File:** -- Modify: `.github/workflows/build-dx-hwe.yml` - -**Step 1: Apply same trigger and publish fixes** - -Remove `- lts` from `pull_request: branches:` -Add `- lts` to `push: branches:` -Remove `schedule:` section -Update `publish:` condition (line 39) to: -```yaml -publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} -``` - -**Step 2: Validate syntax** - -Run: `just check` -Expected: No errors - -**Step 3: Commit the fix** - -```bash -git add .github/workflows/build-dx-hwe.yml -git commit -m "fix(ci): update build-dx-hwe.yml triggers and publish condition - -Completes the fix across all 5 build workflows. - -All workflows now: -- Trigger on PRs to main (validation) -- Trigger on pushes to main (publish :lts-testing) -- Trigger on pushes to lts (validation, no publish) -- Trigger on workflow_dispatch (manual or from dispatcher) -- Do NOT trigger on pull bot PRs to lts - -Assisted-by: Claude 3.5 Sonnet via GitHub Copilot" -``` - ---- - -### Task 7: Verify Publish Conditions (UPDATED in Tasks 2-6) - -**Note:** This task was previously a read-only check. The publish conditions are now UPDATED in Tasks 2-6. - -**Files modified:** -- `.github/workflows/build-regular.yml:33` -- `.github/workflows/build-dx.yml:34` -- `.github/workflows/build-gdx.yml:35` -- `.github/workflows/build-regular-hwe.yml:39` -- `.github/workflows/build-dx-hwe.yml:39` - -**New publish condition (applied in all workflows):** -```yaml -publish: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} -``` - -**Why this is needed:** -- Old condition: `publish: ${{ github.event_name != 'pull_request' }}` - - ❌ Would publish on push to lts (pull bot merges) -- New condition: Only publish on: - - ✅ `workflow_dispatch` (cron dispatcher + manual) - - ✅ `push` to `main` (testing tags) - - ❌ NOT on `push` to `lts` (validation only) - -**Verification:** - -Run: -```bash -grep "publish:" .github/workflows/build-*.yml -``` - -Expected: All show the new condition with `workflow_dispatch` and `main` checks - -```bash -echo "✅ Publish conditions updated in Tasks 2-6" -echo "Tag naming logic unchanged in reusable-build-image.yml" -``` - ---- - -### Task 8: Update Branch Protection for lts - -**Location:** GitHub repository settings (web UI) - -**Note:** Branch protection already exists but needs updating. - -**Step 1: Navigate to branch protection settings** - -1. Go to: `https://github.com/ublue-os/bluefin-lts/settings/branches` -2. Click "Edit" on existing `lts` protection rule -3. Branch name pattern: `lts` - -**Step 2: Update required settings** - -**Current state verified:** -- Required approvals: 2 -- Force pushes: ENABLED (needs to be DISABLED) -- Enforce admins: DISABLED - -**Update to:** -``` -☑ Require a pull request before merging - ☑ Require approvals: 1 (change from 2) - ☑ Dismiss stale pull request approvals when new commits are pushed - ☐ Require review from Code Owners (not needed) - -☑ Require status checks to pass before merging - ☐ Require branches to be up to date before merging (can cause issues with pull bot) - Required checks: (leave empty - workflows don't run on lts PRs anymore) - -☑ Require conversation resolution before merging - -☐ Require signed commits (optional - up to team preference) - -☐ Require linear history (not recommended - pull bot uses hardreset) - -☑ Do not allow bypassing the above settings - (Alternative: Allow only specific people - castrojo, tulilirockz, hanthor) - -☑ Restrict who can push to matching branches - Allowed: pull[bot], castrojo, tulilirockz, hanthor - -☐ Allow force pushes (DISABLE - currently enabled, needs to be disabled) - -☐ Allow deletions (keep disabled) -``` - -**Step 3: Save and verify** - -Click "Save changes" - -Expected: Branch protection updated on `lts` - -**Step 4: Verify configuration** - -Run: -```bash -gh api repos/ublue-os/bluefin-lts/branches/lts/protection | jq '{approvals: .required_pull_request_reviews.required_approving_review_count, force_pushes: .allow_force_pushes.enabled, enforce_admins: .enforce_admins.enabled}' -``` - -Expected output: -```json -{ - "approvals": 1, - "force_pushes": false, - "enforce_admins": true -} -``` - ---- - -### Task 9: Update Documentation - -**File:** -- Modify: `docs/BRANCH_PROTECTION.md` (add lts branch section) - -**Step 1: Add lts branch protection section** - -Add after line 241: - -```markdown - -## LTS Branch Protection (2026-03) - -**Settings applied to `lts` branch:** - -### Required Settings - -1. **Require pull request before merging** - - Require approvals: 1 - - Dismiss stale approvals when new commits are pushed - -2. **Do not allow bypassing settings** - - Enforce for administrators: Yes - -3. **Restrict who can push** - - Allowed: pull[bot], castrojo, tulilirockz, hanthor - -4. **Block force pushes** - - Force pushes: DISABLED - -**Purpose:** -- Prevent accidental direct pushes to `lts` -- Ensure pull bot PRs get reviewed before merge -- Maintain production branch discipline -- Prevent force push accidents - -**Workflow Integration:** -- Pull bot creates PRs from `main` → `lts` (no CI triggers) -- PRs must be approved before merging -- Merging to `lts` does NOT publish images (workflows don't trigger) -- Production publishes happen via: - - Weekly cron dispatcher (Sunday 2 AM UTC) - - Manual workflow_dispatch on `lts` branch - -**Emergency Access:** -- Maintainers can run workflow_dispatch on `lts` for immediate releases -- Maintainers can bypass branch protection if absolutely necessary (if enforce_admins is disabled) -``` - -**Step 2: Commit documentation update** - -```bash -git add docs/BRANCH_PROTECTION.md -git commit -m "docs: document lts branch protection configuration - -Added section explaining lts branch protection settings -and how they integrate with the workflow changes." -``` - ---- - -### Task 10: Test Dispatcher (Manual) - -**Prerequisites:** All previous tasks completed and merged to `main` - -**Step 1: Manually trigger dispatcher** - -```bash -gh workflow run scheduled-lts-release.yml --ref main -``` - -**Step 2: Verify dispatcher runs** - -```bash -# Check dispatcher run -gh run list --workflow=scheduled-lts-release.yml --limit 1 - -# Wait ~30 seconds for workflows to be dispatched - -# Check if builds triggered on lts -gh run list --branch=lts --limit 10 -``` - -**Expected:** -- Dispatcher completes successfully -- 5 build workflows triggered on `lts` branch -- All show event: `workflow_dispatch` -- All show branch: `lts` - -**Step 3: Monitor build progress** - -```bash -# Watch build progress -gh run watch - -# Or view all runs -gh run list --branch=lts --limit 10 --json event,conclusion,headBranch,workflowName -``` - -**Expected (after ~30-60 minutes):** -- All 5 builds complete successfully -- Production tags published: `:lts`, `:lts.20260302`, etc. -- SBOM artifacts generated -- No `:lts-testing` tags published - -**Step 4: Verify tags in registry** - -```bash -# Check published tags (requires skopeo) -skopeo list-tags docker://ghcr.io/ublue-os/bluefin | grep -E "^lts" | tail -10 -``` - -**Expected:** -- `:lts` tag updated with new digest -- `:lts.YYYYMMDD` tag created (today's date) -- `:lts-testing` unchanged (not from this run) - -**Step 5: Document test results** - -```bash -# No commit - just verification -echo "✅ Manual dispatcher test completed successfully" -echo "Production tags published: lts, lts.YYYYMMDD" -echo "Ready for weekly cron schedule" -``` - ---- - -### Task 11: Test Pull Bot PR Flow - -**Prerequisites:** All changes merged to `main` and synced to `lts` - -**Step 1: Wait for pull bot PR or create test PR** - -Wait for pull bot to create a new PR from `main` → `lts`, or manually create one for testing: - -```bash -# Option A: Wait for pull bot (preferred) -# Check for existing PR -gh pr list --base lts - -# Option B: Create test PR (if needed for immediate testing) -git checkout lts -git pull upstream lts -git checkout -b test-lts-pr-flow -git merge upstream/main --no-edit -git push origin test-lts-pr-flow - -gh pr create --base lts --head test-lts-pr-flow \ - --title "[TEST] Verify lts PR doesn't trigger workflows" \ - --body "Testing that PRs to lts no longer trigger build workflows" -``` - -**Step 2: Verify NO workflows triggered** - -```bash -# Check recent runs -gh run list --limit 20 --json event,headBranch,displayTitle,conclusion,createdAt - -# Should NOT see any runs for the test PR -``` - -**Expected:** -- ❌ No "Build Bluefin LTS" runs triggered -- ❌ No "Build Bluefin LTS DX" runs triggered -- ❌ No runs for any build workflows - -**Step 3: Close test PR (if created manually)** - -```bash -gh pr close --delete-branch -``` - -**Step 4: Document test results** - -```bash -# No commit - just verification -echo "✅ Pull bot PR test completed successfully" -echo "PRs to lts do NOT trigger workflows (as intended)" -``` - ---- - -### Task 12: Test Main Branch Publishing - -**Prerequisites:** All changes merged and active - -**Step 1: Create small test PR to main** - -```bash -git checkout main -git pull upstream main -git checkout -b test-main-publish -echo "# Test $(date)" >> .test-publish-marker.txt -git add .test-publish-marker.txt -git commit -m "test: verify main branch publishes lts-testing tags" -git push origin test-main-publish - -gh pr create --base main --head test-main-publish \ - --title "[TEST] Verify main publishes :lts-testing tags" \ - --body "Testing that merges to main still publish testing tags" -``` - -**Step 2: Wait for PR checks and merge** - -```bash -# Wait for CI -gh pr checks --watch - -# Merge when green -gh pr merge --merge -``` - -**Step 3: Verify testing tags published** - -```bash -# Check runs after merge -gh run list --branch=main --event=push --limit 5 - -# Wait for builds to complete (~30-60 minutes) -# Monitor one of the runs -gh run watch - -# Verify testing tags published -skopeo list-tags docker://ghcr.io/ublue-os/bluefin | grep testing | tail -10 -``` - -**Expected:** -- ✅ All 5 build workflows triggered on push to main -- ✅ Builds publish `:lts-testing`, `:lts-testing.YYYYMMDD` tags -- ❌ No `:lts` production tags published - -**Step 4: Clean up test file** - -```bash -git checkout main -git pull upstream main -git rm .test-publish-marker.txt -git commit -m "chore: remove test marker file" -git push upstream main -``` - ---- - -### Task 13: Final Validation Checklist - -**Step 1: Run validation checks** - -```bash -# Syntax validation -just check - -# Lint validation -just lint - -# Verify all workflows exist -ls -1 .github/workflows/*.yml | wc -l -# Expected: 6 workflows (5 builds + 1 dispatcher) - -# Verify no lts in pull_request triggers -grep -n "pull_request:" .github/workflows/build-*.yml -A 3 | grep "lts" -# Expected: No output (no matches) - -# Verify no schedule in build workflows -grep -n "schedule:" .github/workflows/build-*.yml -# Expected: No output (no matches) - -# Verify dispatcher exists -test -f .github/workflows/scheduled-lts-release.yml && echo "✅ Dispatcher exists" || echo "❌ Missing" - -# Verify branch protection configured -gh api repos/ublue-os/bluefin-lts/branches/lts/protection --silent && echo "✅ Branch protection active" || echo "⚠️ Not configured yet" -``` - -**Step 2: Review workflow behavior matrix** - -| Event | Branch | Triggers? | Publishes? | Tags | -|-------|--------|-----------|------------|------| -| PR to main | `main` | ✅ | ❌ | none | -| Merge to main | `main` | ✅ | ✅ | `:lts-testing` | -| PR to lts | `lts` | ❌ | ❌ | none | -| Merge to lts | `lts` | ✅ | ❌ | none (validation build) | -| Cron Sun 2am | `main` | ✅ | ❌ | none (dispatcher) | -| Dispatcher | `lts` | ✅ | ✅ | `:lts` (production) | -| Manual dispatch | `lts` | ✅ | ✅ | `:lts` | -| Manual dispatch | `main` | ✅ | ✅ | `:lts-testing` | - -**Step 3: Document completion** - -All items should be verified: - -```bash -echo "Validation Checklist:" -echo "- [x] Dispatcher workflow created" -echo "- [x] All 5 build workflows updated (triggers + publish conditions)" -echo "- [x] lts added to push triggers (validation builds)" -echo "- [x] lts removed from pull_request triggers (no PR builds)" -echo "- [x] Publish conditions updated (workflow_dispatch or main push only)" -echo "- [x] Branch protection configured for lts" -echo "- [x] Documentation updated" -echo "- [x] Manual dispatcher test passed" -echo "- [x] Pull bot PR test passed (no triggers)" -echo "- [x] Pull bot merge test passed (builds but no publish)" -echo "- [x] Main branch publish test passed (testing tags)" -echo "- [x] Syntax validation passed" -echo "- [x] No accidental production tag publishes" -``` - ---- - -## Rollback Plan - -If issues occur after implementation: - -### Quick Rollback (Emergency) - -```bash -# Manually trigger production release -gh workflow run build-regular.yml --ref lts -gh workflow run build-dx.yml --ref lts -gh workflow run build-gdx.yml --ref lts -gh workflow run build-regular-hwe.yml --ref lts -gh workflow run build-dx-hwe.yml --ref lts -``` - -### Full Revert - -```bash -# Revert all workflow changes -git revert -git push upstream main - -# Re-enable lts in pull_request triggers temporarily -# (Manual edit or revert to previous commit) -``` - -### Partial Rollback - -If only dispatcher is problematic: - -```bash -# Keep workflow trigger fixes -# Remove/disable dispatcher -git rm .github/workflows/scheduled-lts-release.yml -git commit -m "revert: remove dispatcher (issues found)" - -# Use manual workflow_dispatch only for releases -``` - ---- - -## Future Enhancements - -### Potential Improvements (YAGNI for now) - -1. **Slack/Discord notifications** when dispatcher runs -2. **GitHub issue creation** when dispatcher fails -3. **Automatic changelog generation** trigger from dispatcher -4. **Parallel dispatcher** (trigger all 5 workflows in parallel, not sequential) -5. **Dispatcher retry logic** if workflow trigger fails -6. **Branch protection audit workflow** to verify settings -7. **Automated testing** of publish behavior (complicated) - ---- - -## Validation Commands Reference - -```bash -# Check recent workflow runs -gh run list --limit 20 - -# Check runs on specific branch -gh run list --branch=lts --limit 10 -gh run list --branch=main --limit 10 - -# Check runs for specific workflow -gh run list --workflow=build-regular.yml --limit 10 - -# Check scheduled runs -gh run list --event=schedule --limit 10 - -# Check workflow_dispatch runs -gh run list --event=workflow_dispatch --limit 10 - -# View published tags -skopeo list-tags docker://ghcr.io/ublue-os/bluefin | grep -E "lts|testing" - -# Check branch protection -gh api repos/ublue-os/bluefin-lts/branches/lts/protection | jq - -# Verify pull bot config -cat .github/pull.yml - -# Syntax check -just check - -# Lint check -just lint -``` - ---- - -## Verification Results (2026-03-02) - -**Plan verified against current codebase:** - -✅ File paths and line numbers accurate -✅ Current state matches plan assumptions -✅ All 5 workflows have `lts` in `pull_request: branches:` (line 12) -✅ All 5 workflows have `schedule:` cron (lines 16-17) -✅ Dispatcher doesn't exist yet (needs creation) -✅ Branch protection exists but needs updating (2 approvals → 1, force push enabled → disabled) -✅ Tag naming logic verified in `reusable-build-image.yml:161-164` -✅ PRODUCTION_BRANCH constant set to `lts` in `reusable-build-image.yml:76` -✅ Publish conditions don't need changes - -**Plan Status:** ✅ Verified and ready for implementation - ---- - -**Estimated Time:** -- Implementation: 1-2 hours (workflow file edits + branch protection config) -- Testing: 1-2 hours (wait for builds, verify behavior) -- Total: 2-4 hours - -**Risk Level:** Low -- Changes are isolated to workflow triggers -- Rollback is straightforward -- Testing can be done incrementally -- No changes to build logic itself From 6ec7dd5a83b0cc439e1722433a0411490da50fa6 Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Mon, 2 Mar 2026 21:59:18 -0500 Subject: [PATCH 19/19] fix(ci): fix LTS promotion workflow failures (#1157) ## Summary - **Fix tag pollution from main branch merges**: The manifest step had complex conditional logic that omitted pushes to `main`, causing `:lts` production tags to be overwritten by testing builds. Aligns manifest step with build step logic. - **Fix `Push Manifest` and `sign` failing on lts push events**: Both steps used `github.event_name != 'pull_request'` which fired even when `publish=false`, causing `image not known` errors. Now gated on `inputs.publish`. - **Remove duplicate `schedule:` from all 5 build workflows**: The dispatcher (`scheduled-lts-release.yml`) owns the weekly cron. The stale entries were triggering 10 extra no-op builds on `main` every Sunday on top of the 5 dispatcher runs on `lts`. - **Simplify `promote-to-lts.yml`**: Replace the checkout+merge+intermediate-branch approach (which reintroduced merge commit pollution) with a single `gh pr create --base lts --head main` call. Drops `contents: write` permission. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/build-dx-hwe.yml | 3 - .github/workflows/build-dx.yml | 3 - .github/workflows/build-gdx.yml | 3 - .github/workflows/build-regular-hwe.yml | 3 - .github/workflows/build-regular.yml | 3 - .github/workflows/generate-release.yml | 12 +- .github/workflows/promote-to-lts.yml | 31 +--- .github/workflows/reusable-build-image.yml | 21 ++- AGENTS.md | 166 +++++++++++++++++++-- 9 files changed, 177 insertions(+), 68 deletions(-) diff --git a/.github/workflows/build-dx-hwe.yml b/.github/workflows/build-dx-hwe.yml index 588160e8..7437b412 100644 --- a/.github/workflows/build-dx-hwe.yml +++ b/.github/workflows/build-dx-hwe.yml @@ -13,8 +13,6 @@ on: branches: - main - lts - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -35,6 +33,5 @@ jobs: flavor: dx kernel-pin: 6.17.12-200.fc42 rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} hwe: true diff --git a/.github/workflows/build-dx.yml b/.github/workflows/build-dx.yml index 3954ceec..9c9e36e4 100644 --- a/.github/workflows/build-dx.yml +++ b/.github/workflows/build-dx.yml @@ -13,8 +13,6 @@ on: branches: - main - lts - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -30,5 +28,4 @@ jobs: image-name: bluefin-dx flavor: dx rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} diff --git a/.github/workflows/build-gdx.yml b/.github/workflows/build-gdx.yml index fad52779..5937ecc7 100644 --- a/.github/workflows/build-gdx.yml +++ b/.github/workflows/build-gdx.yml @@ -13,8 +13,6 @@ on: branches: - main - lts - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -31,5 +29,4 @@ jobs: flavor: gdx kernel-pin: 6.17.12-200.fc42 rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} diff --git a/.github/workflows/build-regular-hwe.yml b/.github/workflows/build-regular-hwe.yml index d4ea3949..82dabb93 100644 --- a/.github/workflows/build-regular-hwe.yml +++ b/.github/workflows/build-regular-hwe.yml @@ -13,8 +13,6 @@ on: branches: - main - lts - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -34,7 +32,6 @@ jobs: image-name: bluefin kernel-pin: 6.17.12-200.fc42 rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} hwe: true diff --git a/.github/workflows/build-regular.yml b/.github/workflows/build-regular.yml index 003ae355..32750449 100644 --- a/.github/workflows/build-regular.yml +++ b/.github/workflows/build-regular.yml @@ -13,8 +13,6 @@ on: branches: - main - lts - schedule: - - cron: '0 2 * * 0' # Weekly on Sunday at 2 AM UTC merge_group: workflow_dispatch: @@ -29,5 +27,4 @@ jobs: with: image-name: bluefin rechunk: ${{ github.event_name != 'pull_request' }} - sbom: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} publish: ${{ (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} diff --git a/.github/workflows/generate-release.yml b/.github/workflows/generate-release.yml index 7d1d681e..4ee73ccd 100644 --- a/.github/workflows/generate-release.yml +++ b/.github/workflows/generate-release.yml @@ -25,11 +25,15 @@ on: jobs: generate-release: runs-on: ubuntu-latest - # Only run if the workflow was successful and on lts branch + # Only run if the workflow was successful, on lts branch, and triggered by + # workflow_dispatch (meaning scheduled-lts-release.yml fired it — images were published). + # Push-to-lts validation builds complete successfully too but publish nothing, + # so we must not create a release for those. if: | github.event_name == 'workflow_dispatch' || - (github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.head_branch == 'lts') + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'lts' && + github.event.workflow_run.event == 'workflow_dispatch') steps: - name: Checkout repository @@ -63,7 +67,7 @@ jobs: fi - name: Generate changelog - uses: hanthor/changelog-action@master + uses: hanthor/changelog-action@2d212cd35f65cfe33954dd79013887e7bee76580 # master with: stream: ${{ steps.target.outputs.target }} family: bluefin-lts diff --git a/.github/workflows/promote-to-lts.yml b/.github/workflows/promote-to-lts.yml index 3d68da99..da918201 100644 --- a/.github/workflows/promote-to-lts.yml +++ b/.github/workflows/promote-to-lts.yml @@ -17,46 +17,21 @@ on: **IMPORTANT**: This PR should ONLY contain commits from `main` → `lts`. Never merge in the opposite direction. permissions: - contents: write pull-requests: write + issues: write jobs: create-promotion-pr: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: lts - fetch-depth: 0 - - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Create promotion branch - id: create-branch - run: | - BRANCH_NAME="promote-main-to-lts-$(date +%Y%m%d-%H%M%S)" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - git checkout -b "$BRANCH_NAME" - - - name: Merge main into promotion branch - run: | - git merge origin/main --no-edit -m "Merge main into lts" - - - name: Push promotion branch - run: | - git push origin ${{ steps.create-branch.outputs.branch_name }} - - name: Create Pull Request env: GH_TOKEN: ${{ github.token }} run: | gh pr create \ + --repo ${{ github.repository }} \ --base lts \ - --head ${{ steps.create-branch.outputs.branch_name }} \ + --head main \ --title "${{ inputs.pr_title }}" \ --body "${{ inputs.pr_body }}" \ --label "promotion" diff --git a/.github/workflows/reusable-build-image.yml b/.github/workflows/reusable-build-image.yml index 654d0ba1..fbc5afb0 100644 --- a/.github/workflows/reusable-build-image.yml +++ b/.github/workflows/reusable-build-image.yml @@ -32,11 +32,6 @@ on: required: false type: boolean default: true - sbom: - description: "Generate/publish SBOMs for the artifacts" - required: false - type: boolean - default: true cleanup_runner: description: "Use the ublue cleanup action to clean up the runner before running the build" required: false @@ -52,7 +47,7 @@ on: required: false type: boolean # default: ${{ github.event_name != 'pull_request' }} - default: true + default: false tag-suffix: description: "The suffix to append to the image tag" required: false @@ -176,12 +171,14 @@ jobs: - name: Setup Syft id: setup-syft - if: ${{ inputs.sbom && inputs.publish }} + if: ${{ github.ref == 'refs/heads/lts' && inputs.publish }} + continue-on-error: true uses: anchore/sbom-action/download-syft@17ae1740179002c89186b61233e0f892c3118b11 # v0 - name: Generate SBOM id: generate-sbom - if: ${{ inputs.sbom && inputs.publish }} + if: ${{ github.ref == 'refs/heads/lts' && inputs.publish }} + continue-on-error: true env: IMAGE: ${{ env.IMAGE_NAME }} DEFAULT_TAG: ${{ env.DEFAULT_TAG }} @@ -267,7 +264,8 @@ jobs: COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} - name: Add SBOM Attestation - if: ${{ inputs.sbom }} + if: ${{ github.ref == 'refs/heads/lts' && inputs.publish }} + continue-on-error: true env: IMAGE: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }} DIGEST: ${{ steps.push.outputs.remote_image_digest }} @@ -492,7 +490,7 @@ jobs: echo "${{ secrets.GITHUB_TOKEN }}" | podman login -u "${{ github.actor }}" --password-stdin "${REGISTRY}" - name: Push Manifest - if: github.event_name != 'pull_request' + if: ${{ inputs.publish }} id: push_manifest env: MANIFEST: ${{ steps.create-manifest.outputs.MANIFEST }} @@ -512,7 +510,7 @@ jobs: # so we move this to another step in order to run on Ubuntu sign: needs: manifest - if: github.event_name != 'pull_request' + if: ${{ inputs.publish }} runs-on: ubuntu-latest permissions: contents: read @@ -531,6 +529,7 @@ jobs: uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1 - name: Sign Manifest + if: ${{ inputs.publish }} env: DIGEST: ${{ needs.manifest.outputs.digest }} IMAGE: ${{ needs.manifest.outputs.image }} diff --git a/AGENTS.md b/AGENTS.md index 5e413e06..57103ba4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,19 +106,165 @@ Always reference these instructions first and fallback to search or bash command - Architecture-specific: `system_files_overrides/[arch]/` - Combined: `system_files_overrides/[arch]-[variant]/` -## GitHub Actions Integration +## GitHub Actions CI/CD Architecture -### CI Build Process -- **Timeout**: 60 minutes configured in reusable-build-image.yml -- **Platforms**: amd64, arm64 -- **Validation**: Runs `just check` before building -- **Build Command**: `sudo just build [IMAGE] [TAG] [DX] [GDX] [HWE]` +This section is the authoritative reference for all CI/CD behavior. Read it completely before touching any workflow file. Agents repeatedly break the CI system by making changes based on assumptions rather than this documented architecture. + +### Workflow Files and Their Roles + +| File | Role | +|---|---| +| `build-regular.yml` | Caller — builds `bluefin` image | +| `build-dx.yml` | Caller — builds `bluefin-dx` image (developer variant) | +| `build-gdx.yml` | Caller — builds `bluefin-gdx` image (GPU/AI variant) | +| `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` | Creates a PR to merge `main` → `lts` (see below) | +| `generate-release.yml` | Creates a GitHub Release when `build-gdx.yml` completes on `lts` | + +### Two Branches, Two Tag Namespaces + +| Branch | Tags produced | When published | +|---|---|---| +| `main` | `lts-testing`, `lts-hwe-testing`, `lts-testing-YYYYMMDD`, `stream10-testing`, `10-testing`, etc. | Every push/merge to `main` | +| `lts` | `lts`, `lts-hwe`, `lts-YYYYMMDD`, `stream10`, `10`, etc. | Weekly via `scheduled-lts-release.yml` or manual `workflow_dispatch` on `lts` | + +**All tags containing `testing` must be published on every push to `main`.** Production tags must only be published from the `lts` branch. + +### The `main` → `lts` Promotion Flow + +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 opens a PR from `main` targeting `lts` directly (no intermediate branch) +3. A maintainer reviews and merges the PR +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. + +**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 +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. + +**NEVER merge `lts` into `main`.** The flow is always one-way: `main` → `lts`. + +### `publish` Input — How It Is Evaluated + +All 5 caller workflows pass the same `publish:` expression: + +```yaml +publish: ${{ + (github.event_name == 'workflow_dispatch' && (github.ref == 'refs/heads/lts' || github.ref == 'refs/heads/main')) + || + (github.event_name == 'push' && github.ref == 'refs/heads/main') +}} +``` + +Full truth table: + +| Event | Branch | `publish` | Tags published | Notes | +|---|---|---|---|---| +| `push` | `main` | **true** | `-testing` tags | Normal CI after merge | +| `push` | `lts` | **false** | nothing | Intentional — validation only; production ships via dispatch | +| `workflow_dispatch` | `lts` | **true** | production `:lts` tags | Triggered by `scheduled-lts-release.yml` or manually | +| `workflow_dispatch` | `main` | **true** | `-testing` tags | Manual re-run on main | +| `pull_request` | `main` | **false** | nothing | CI check only | +| `merge_group` | `main` | **false** | nothing | CI check only | + +**Push to `lts` runs builds but does not publish — this is intentional.** It validates that promoted code compiles cleanly before the next scheduled release. Do not add publish logic to the `push lts` path. + +**`publish` defaults to `false`** in `reusable-build-image.yml`. Callers must explicitly opt in. A caller that omits `publish:` will build but not push anything. + +### Tag Suffix Logic in `reusable-build-image.yml` + +Tag suffixes are computed in two places: + +**`build_push` job** (build step): +```bash +if [ "${REF_NAME}" != "${PRODUCTION_BRANCH}" ]; then + export TAG_SUFFIX="testing" + export DEFAULT_TAG="${DEFAULT_TAG}-${TAG_SUFFIX}" +fi +echo "DEFAULT_TAG=${DEFAULT_TAG}" >> "${GITHUB_ENV}" +``` + +**`manifest` job** (`Add suffixes` step): +```bash +if [ "${REF_NAME}" != "${PRODUCTION_BRANCH}" ]; then + export TAG_SUFFIX="testing" + export DEFAULT_TAG="${DEFAULT_TAG}-${TAG_SUFFIX}" + export CENTOS_VERSION_SUFFIX="${CENTOS_VERSION_SUFFIX}-${TAG_SUFFIX}" +fi +echo "DEFAULT_TAG=${DEFAULT_TAG}" >> "${GITHUB_ENV}" +echo "CENTOS_VERSION_SUFFIX=${CENTOS_VERSION_SUFFIX}" >> "${GITHUB_ENV}" +``` + +**IMPORTANT**: `TAG_SUFFIX` is set with `export` only — it is **never written to `GITHUB_ENV`**. The `Image Metadata` action uses `${{ env.TAG_SUFFIX }}` in its tags expressions, which will always expand to empty string. This is NOT a bug: `CENTOS_VERSION_SUFFIX` already contains the `-testing` suffix, so all tags are generated correctly. Do not "fix" this by adding `TAG_SUFFIX` to `GITHUB_ENV` — it would produce duplicate suffixes like `stream10-testing-testing`. + +### SBOM Attestation Rules + +SBOMs are generated and attested **only on the `lts` branch** and **only when publishing**. The attestation uses Sigstore/Rekor. Rekor is an external service that has experienced outages (confirmed 2026-02-24: `Post "https://rekor.sigstore.dev/api/v1/log/entries": giving up after 4 attempt(s)`). + +All three SBOM steps in `reusable-build-image.yml` must have **both**: +1. `if: ${{ github.ref == 'refs/heads/lts' && inputs.publish }}` +2. `continue-on-error: true` + +**A failed SBOM must never block image publishing.** We prefer published images without SBOMs over no images at all. Do not remove `continue-on-error: true` from any SBOM step. + +The `sbom:` input has been removed from `reusable-build-image.yml`. SBOM behavior is controlled entirely by the step conditions above — no external toggle is needed or supported. + +### CI Build Process Reference +- **Timeout**: 60 minutes configured in `reusable-build-image.yml` (`build_push` job) +- **Platforms**: amd64, arm64 (matrix-driven) +- **Validation**: `just check` runs before every build +- **Build Command**: `sudo just build [IMAGE] [TAG] [DX] [GDX] [HWE] [KERNEL_PIN]` +- **Rechunk**: Runs on all non-PR builds when `publish=true` +- **fail-fast**: false — both platforms attempt independently +- **`publish` default**: `false` — callers must explicitly opt in + +### Workflow Condition Quick Reference + +When touching any condition in `reusable-build-image.yml`, use this reference: + +| Step / Job | Correct condition | +|---|---| +| SBOM steps (Setup Syft, Generate SBOM, Add SBOM Attestation) | `if: ${{ github.ref == 'refs/heads/lts' && inputs.publish }}` + `continue-on-error: true` | +| Rechunk | `if: ${{ inputs.rechunk && inputs.publish }}` | +| Load Image | `if: ${{ inputs.publish }}` | +| Login to GHCR | `if: ${{ inputs.publish }}` | +| Push to GHCR | `if: ${{ inputs.publish }}` | +| Install Cosign | `if: ${{ inputs.publish }}` | +| Sign Image (build_push job) | `if: ${{ inputs.publish }}` | +| Create Job Outputs | `if: ${{ inputs.publish }}` | +| Upload Output Artifacts | `if: ${{ inputs.publish }}` | +| Push Manifest (manifest job) | `if: ${{ inputs.publish }}` | +| sign job (top-level) | `if: ${{ inputs.publish }}` | +| Sign Manifest (inside sign job) | `if: ${{ inputs.publish }}` | + +**Signing must only happen when an image is actually published to the registry.** Any condition other than `inputs.publish` on signing or manifest push steps is wrong. + +### `schedule:` Triggers — Ownership Rule + +**`scheduled-lts-release.yml` is the sole owner of Sunday 2am 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. + +If you see `schedule:` in any of the 5 build callers, remove it entirely. Do not move or adjust the cron expression — remove it. ### Available Workflows -- `build-regular.yml` - Standard Bluefin LTS build -- `build-dx.yml` - Developer Experience variant -- `build-gdx.yml` - GPU Developer Experience variant -- `build-iso.yml` - ISO installer builds +- `build-regular.yml` — Standard Bluefin LTS (`bluefin` image) +- `build-dx.yml` — Developer Experience (`bluefin-dx` image) +- `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` — Opens a one-way `main` → `lts` promotion PR +- `generate-release.yml` — Creates GitHub Release after successful GDX build on `lts` ## Validation Scenarios