diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 246a6a8..f5277e1 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -17,7 +17,19 @@ on: # yamllint disable-line rule:truthy - ".claude/**" - ".github/workflows/cleanup-old-images.yml" workflow_call: + inputs: + changed_only: + description: >- + Build only variants whose upstream base digest changed. + type: boolean + default: false workflow_dispatch: + inputs: + changed_only: + description: >- + Build only variants whose upstream base digest changed. + type: boolean + default: false permissions: contents: write packages: write @@ -33,3 +45,4 @@ jobs: image_flavor: ["Bazzite"] with: image_flavor: ${{ matrix.image_flavor }} + changed_only: ${{ inputs.changed_only || false }} diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 92cddf7..7dda048 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -5,6 +5,12 @@ on: # yamllint disable-line rule:truthy inputs: image_flavor: type: string + changed_only: + description: >- + Select only variants whose upstream base digest changed. + Scheduled runs always do this regardless of this input. + type: boolean + default: false concurrency: group: >- ${{ github.workflow }}-${{ github.ref @@ -52,7 +58,8 @@ jobs: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ github.token }} run: | - if [[ "${{ github.event_name }}" == "schedule" ]]; then + if [[ "${{ inputs.changed_only }}" == "true" \ + || "${{ github.event_name }}" == "schedule" ]]; then changed_only=true else changed_only=false @@ -74,11 +81,27 @@ jobs: echo "images=$images" >> "$GITHUB_OUTPUT" echo "manifest_images=$manifest_images" >> "$GITHUB_OUTPUT" echo "publish=$publish" >> "$GITHUB_OUTPUT" + + # An empty matrix is a normal outcome on scheduled runs; say so on + # the run page rather than only in this job's log. + { + echo "## ${{ inputs.image_flavor }} build matrix" + echo + if [[ "$images" == "[]" ]]; then + echo "No variants need rebuilding: every published image" + echo "already records the current upstream base digest." + else + jq -r '.[] | "- `\(.image)` (\(.arch))"' <<<"$images" + fi + } >> "$GITHUB_STEP_SUMMARY" build-image: name: >- Build ${{ inputs.image_flavor }} Images (${{ matrix.combo.image }}/${{ matrix.combo.arch }}) needs: get-images + # GitHub fails the entire run -- with no failing job and no annotation -- + # when a strategy matrix expression evaluates to []. Skip the job instead. + if: ${{ needs.get-images.outputs.images != '[]' }} runs-on: ${{ matrix.combo.runner }} continue-on-error: false permissions: diff --git a/.github/workflows/build-server.yml b/.github/workflows/build-server.yml index c14a2e2..bf52033 100644 --- a/.github/workflows/build-server.yml +++ b/.github/workflows/build-server.yml @@ -17,7 +17,19 @@ on: # yamllint disable-line rule:truthy - ".claude/**" - ".github/workflows/cleanup-old-images.yml" workflow_call: + inputs: + changed_only: + description: >- + Build only variants whose upstream base digest changed. + type: boolean + default: false workflow_dispatch: + inputs: + changed_only: + description: >- + Build only variants whose upstream base digest changed. + type: boolean + default: false permissions: contents: write packages: write @@ -33,3 +45,4 @@ jobs: image_flavor: ["Server"] with: image_flavor: ${{ matrix.image_flavor }} + changed_only: ${{ inputs.changed_only || false }} diff --git a/docs/design/build-scheduling.md b/docs/design/build-scheduling.md index 0d6b327..c730551 100644 --- a/docs/design/build-scheduling.md +++ b/docs/design/build-scheduling.md @@ -65,8 +65,13 @@ never triggers a build either, regardless of what files changed. - The matrix job emits `images` (flat list of `{image, arch, runner, base_digest}`) and `manifest_images` (grouped by image, for the later multi-arch manifest step), plus `publish` (`true` except on `pull_request`). + It also writes the selection to `$GITHUB_STEP_SUMMARY` — the chosen + image/arch pairs, or "No variants need rebuilding" — so a no-op run + explains itself on the run page. -**`build-image` job** runs `just build ` per matrix +**`build-image` job** is guarded by +`if: needs.get-images.outputs.images != '[]'`, then runs +`just build ` per matrix entry, pinning the build to the exact digest the matrix already resolved (not re-resolving the tag, which could have moved). It labels the built image with `org.opencontainers.image.base.digest=` and a "Verify @@ -91,6 +96,21 @@ resolved version tag, and signs both with Cosign. - Pushes to `main` always publish every enabled variant that built, regardless of digest state — publishing is driven by the git event, not by whether a rebuild was "needed." +- An empty matrix is the normal steady state for a scheduled run once every + variant is current, and it is why `build-image` carries + `if: needs.get-images.outputs.images != '[]'`. Without that guard GitHub + fails the *entire run* when a `strategy.matrix` expression evaluates to + `[]` ("Matrix vector 'combo' does not contain any values") — the job is + never created, so nothing shows as failed, there is no annotation, and + `gh run view --log-failed` is empty. Do not remove the guard. + `create-manifest` (`manifest_images != '[]'`) and `check`'s "Exit on + failure" step already tolerate the same empty-matrix case. +- `build-desktop.yml` and `build-server.yml` accept a `changed_only` boolean + on `workflow_dispatch`, which forces the scheduled-run selection path + (including the empty matrix) on demand: + `gh workflow run build-desktop.yml -f changed_only=true`. Scheduled runs + select changed-only regardless of the input; every other trigger defaults + to `false`. - To debug why a variant was or wasn't selected on a scheduled run, check the "Get Images for Build" job's log output from `just generate-ci-matrix` (`stderr` prints `Skipping ; : is unchanged` for skipped