From a3ee37011aae0bddfedd7eec62eb65c11bb0bfe1 Mon Sep 17 00:00:00 2001 From: efiten Date: Fri, 4 Sep 2026 09:44:18 +0200 Subject: [PATCH] ci: scope docs-only skipping to jobs, so required checks still report (#1955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same change as #1954, opened from a branch on this repository instead of from a fork. #1954 never received a workflow run: zero runs and zero check suites for its head commit, and closing and reopening it changed nothing. A manual `workflow_dispatch` on master ran immediately, so Actions itself is working; the `pull_request` event from the fork is what produces nothing. See the note at the end. Replaces the trigger-level `paths-ignore` from #1949 and #1950. That approach was wrong, and it is currently blocking #1953 from merging. ## What was wrong GitHub documents the distinction I had backwards: > a workflow skipped by path filtering keeps its checks **pending** and blocks the merge, while a **job** skipped by an `if:` conditional reports **Success** and does not. So the filtering has to live on the jobs, not on the trigger. I compounded it by claiming, in both the commit and the description of #1949, that master had no required checks: *"verified: the branch protection endpoint returns 404"*. **That verification was invalid.** A 404 there means the token cannot read protection details, not that none exist. The branch reports `protected=true`, and #1953 was refused with `the base branch policy prohibits the merge`. ## What this does A `🔎 Change scope` job computes whether anything outside `docs/`, `*.md` and `LICENSE` changed. `go-test`, `e2e-test`, `build-and-publish` and `release-artifacts` are gated on its output. A documentation-only pull request skips those jobs, they report Success, and the PR can merge. **Only pull requests are scoped.** A push or a dispatch always runs the full pipeline. That second part is deliberate and it fixes a separate failure. `release-fast-path.yml` re-tags `:edge` to `:vX.Y.Z` only when the `:edge` revision label matches the tagged commit. A master commit with no image breaks tagging, which is what happened to the v3.10.0 tag: the tagged commit was documentation-only, the fast path could not re-tag, it fell back to a dispatch, and the dispatch published nothing (#1951). Master pushes now always produce an image. The cost is running the pipeline on documentation commits to master; pull requests are where the queue pressure was. Two conservative defaults in the scope check: a non-`pull_request` event and an empty diff both count as code, so an unexpected shape runs everything rather than silently skipping. ## Note for whoever has repository settings access Fork pull requests stopped getting workflow runs between 22:36 and 05:40. #1949, #1950 and #1951 all came from the same fork and each got a run; #1954 got none, with no check suite created at all, which is different from a skipped run. `repos/.../actions/permissions` returns 403 for a non-admin token, so this could not be confirmed from the API. If the "Fork pull request workflows from outside collaborators" setting was tightened, that would explain it, and it would affect every outside contributor, not just this branch. --------- Co-authored-by: Claude Opus 5 (1M context) --- .github/workflows/deploy.yml | 95 +++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4b0f09d3..6b68ff79 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,40 +1,22 @@ name: CI/CD Pipeline -# Documentation-only changes skip this pipeline entirely. Nothing under -# docs/ or any *.md file is read at build or test time: no Go or JS source -# opens one, and /api/docs serves Swagger generated from openapi.go, not -# from docs/. Running a 16-minute Playwright suite to prove that a -# markdown file does not break the browser only pushed the runs that do -# matter further down a queue that has been hours deep. +# Documentation-only changes still trigger this workflow, on purpose. Skipping +# it at the trigger level leaves every required check in a pending state that +# can never arrive, and the pull request can then never merge. GitHub documents +# exactly this: a workflow skipped by path filtering keeps its checks pending +# and blocks the merge, while a JOB skipped by an if: conditional reports +# Success and does not. So the filtering lives on the jobs below, not here. # -# paths-ignore skips only when EVERY changed file matches, so a PR that -# touches both code and docs still runs the full pipeline. -# -# The pattern is '**.md', not '**/*.md'. GitHub's own example for "any file -# with this extension" is '**.js' with no slash; '**/*.md' reads as requiring -# a directory component, which would leave root-level files such as -# CHANGELOG.md and README.md uncovered. Since paths-ignore only skips when -# EVERY changed file matches, one uncovered root file is enough to run the -# whole pipeline. -# -# Caveat if required status checks are ever enabled on master: a skipped -# workflow never reports, so a docs-only PR would wait forever on a check -# that cannot arrive. At that point this needs to become a change-detection -# job with conditional heavy jobs, not a trigger filter. There are no -# required checks on master today. +# The scope check only applies to pull requests. A push to master always runs +# the full pipeline, which keeps :edge built for every master commit. That +# matters: release-fast-path.yml re-tags :edge to :vX.Y.Z only when the :edge +# revision label matches the tagged commit, so a master commit without an image +# breaks tagging. on: push: branches: [master] - paths-ignore: - - '**.md' - - 'docs/**' - - 'LICENSE' pull_request: branches: [master] - paths-ignore: - - '**.md' - - 'docs/**' - - 'LICENSE' workflow_dispatch: permissions: @@ -56,12 +38,57 @@ env: # PRs stop after build-and-publish (no GHCR push). Master continues to deploy + badges. jobs: + # ─────────────────────────────────────────────────────────────── + # 0. Change scope — decides whether the expensive jobs below run. + # ─────────────────────────────────────────────────────────────── + changes: + name: "🔎 Change scope" + runs-on: ubuntu-latest + outputs: + code: ${{ steps.scope.outputs.code }} + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Decide whether anything but documentation changed + id: scope + run: | + set -euo pipefail + # Only pull requests are scoped. Pushes and dispatches always count as + # code, so master keeps producing an :edge image and tag builds are + # never skipped. + if [ "${{ github.event_name }}" != "pull_request" ]; then + echo "code=true" >> "$GITHUB_OUTPUT" + echo "event=${{ github.event_name }} -> full pipeline" + exit 0 + fi + CHANGED=$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}") + # Empty diff means something is off; run everything rather than guess. + if [ -z "$CHANGED" ]; then + echo "code=true" >> "$GITHUB_OUTPUT" + echo "empty diff -> full pipeline" + exit 0 + fi + echo "Changed files:" + echo "$CHANGED" | sed 's/^/ /' + if echo "$CHANGED" | grep -qvE '(^docs/|[.]md$|^LICENSE$)'; then + echo "code=true" >> "$GITHUB_OUTPUT" + echo "-> non-documentation files changed, full pipeline" + else + echo "code=false" >> "$GITHUB_OUTPUT" + echo "-> documentation only, heavy jobs skip (and report Success)" + fi + # ─────────────────────────────────────────────────────────────── # 1. Go Build & Test # ─────────────────────────────────────────────────────────────── go-test: name: "✅ Go Build & Test" runs-on: ubuntu-latest + needs: [changes] + if: needs.changes.outputs.code == 'true' steps: - name: Checkout code uses: actions/checkout@v5 @@ -290,11 +317,12 @@ jobs: # ─────────────────────────────────────────────────────────────── e2e-test: name: "🎭 Playwright E2E Tests" - needs: [go-test] + needs: [go-test, changes] runs-on: ubuntu-latest defaults: run: shell: bash + if: needs.changes.outputs.code == 'true' steps: - name: Checkout code uses: actions/checkout@v5 @@ -635,8 +663,9 @@ jobs: # comparison failed, and the fallback ran for the first time. build-and-publish: name: "🏗️ Build & Publish Docker Image" - needs: [e2e-test] + needs: [e2e-test, changes] runs-on: ubuntu-latest + if: needs.changes.outputs.code == 'true' steps: - name: Checkout code uses: actions/checkout@v5 @@ -714,11 +743,11 @@ jobs: # ─────────────────────────────────────────────────────────────── release-artifacts: name: "📦 Release Artifacts" - if: startsWith(github.ref, 'refs/tags/v') - needs: [go-test] + needs: [go-test, changes] runs-on: ubuntu-latest permissions: contents: write + if: startsWith(github.ref, 'refs/tags/v') && needs.changes.outputs.code == 'true' steps: - name: Checkout code uses: actions/checkout@v5