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