From 21bd2f7bbf01533e152396cebc902605c4333e76 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Fri, 14 Aug 2026 12:28:37 +0200 Subject: [PATCH] build(ci): stop .claude-only changes from triggering full builds Editing an agent skill rebuilt the whole project on GitHub Actions and Jenkins. No code changes, so every one of those runs was wasted. GitHub Actions, non-required workflows (codeql, owasp, sonar): plain paths-ignore on both push and pull_request. Nothing they report is required in .asf.yaml, so a run that never happens blocks nothing. GitHub Actions, maven.yml: paths-ignore on push only. It is deliberately NOT applied to pull_request, because "Build and Test (JDK 17)" is a required check and GitHub documents that a workflow skipped by path filtering never reports - the check stays Pending and the pull request can never be merged. Instead a small `changes` job inspects the PR's file list and the build job is skipped by condition. A job skipped that way does report, as "skipped", and required checks accept "successful, skipped, or neutral". Jenkins polls SCM, so the trigger cannot be filtered; the two JDK stages are guarded instead. Detection fails open - no previous successful commit, an unreachable commit, or any git error reports true and the build runs exactly as before. The filter tests for a non-empty list of files outside .claude/ rather than using `grep -qv`: the local ugrep 7.5.0 returns 1 from `-qv` on input where `-cv` counts 1 and `-v` prints the line, which silently inverts the decision. Testing emptiness behaves the same everywhere. Exercised against six inputs, including the mixed .claude/ + code case that must still build, and .claudefoo/ which must not be treated as .claude/. Only the 7.x line is changed here; support/struts-6-x-x needs the same edit on its own branch. Co-authored-by: Claude Opus 5 --- .github/workflows/codeql.yml | 6 +++++ .github/workflows/maven.yml | 43 ++++++++++++++++++++++++++++++++++++ .github/workflows/owasp.yml | 6 +++++ .github/workflows/sonar.yml | 6 +++++ Jenkinsfile | 31 ++++++++++++++++++++++++++ 5 files changed, 92 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5f0214d013..6e7cf4d8b5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,13 @@ on: - 'main' - 'release/*' - 'support/*' + paths-ignore: + - '.claude/**' + # Safe to filter by path here: no check from this workflow is required in + # .asf.yaml, so a run that never happens blocks nothing. pull_request: + paths-ignore: + - '.claude/**' permissions: # Needed to upload the results to code-scanning dashboard. diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 754dc98e3b..6d1e861123 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -16,6 +16,11 @@ name: Java Maven on: + # Deliberately NOT filtered by path. "Build and Test (JDK 17)" is a required + # status check in .asf.yaml, and a workflow skipped by path filtering never + # reports its checks - they stay Pending and the pull request can never be + # merged. The build job is skipped by condition instead (see `changes` below), + # which does report, as "skipped", and satisfies the requirement. pull_request: push: branches: @@ -23,6 +28,8 @@ on: - 'develop' - 'release/*' - 'support/*' + paths-ignore: + - '.claude/**' workflow_dispatch: workflow_call: @@ -33,8 +40,44 @@ env: LANG: en_US.utf8 jobs: + changes: + name: Detect changes outside .claude + runs-on: ubuntu-latest + outputs: + code: ${{ steps.filter.outputs.code }} + steps: + - name: Check which paths the pull request touches + id: filter + env: + GH_TOKEN: ${{ github.token }} + run: | + set -eu + if [ "${{ github.event_name }}" != "pull_request" ]; then + echo "Not a pull request - building." + echo "code=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + files=$(gh api --paginate \ + "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" \ + --jq '.[].filename') + echo "Changed files:" + printf '%s\n' "$files" + # Anything outside .claude/ means a real build is needed; an empty + # diff, or one confined to .claude/, does not. Tested by emptiness + # rather than with `grep -qv`, whose exit status is not reliable + # across grep implementations. + outside=$(printf '%s\n' "$files" | grep -vE '^(\.claude/|$)' || true) + if [ -n "$outside" ]; then + echo "code=true" >> "$GITHUB_OUTPUT" + else + echo "Only .claude/ changed - skipping the build." + echo "code=false" >> "$GITHUB_OUTPUT" + fi + build: name: Build and Test (JDK ${{ matrix.java }})${{ matrix.profile == '-Pjakartaee11' && ' (Jakarta EE 11 + Spring 7)' || matrix.profile }} + needs: changes + if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/owasp.yml b/.github/workflows/owasp.yml index 661b09187a..7b6d0523ae 100644 --- a/.github/workflows/owasp.yml +++ b/.github/workflows/owasp.yml @@ -16,13 +16,19 @@ name: OWASP checkup on: + # Safe to filter by path here: no check from this workflow is required in + # .asf.yaml, so a run that never happens blocks nothing. pull_request: + paths-ignore: + - '.claude/**' push: branches: - 'main' - 'develop' - 'release/*' - 'support/*' + paths-ignore: + - '.claude/**' workflow_dispatch: #Allow manual triggers permissions: read-all diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index bf33a5520b..becd7c7897 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -16,10 +16,16 @@ name: SonarCloud on: + # Safe to filter by path here: no check from this workflow is required in + # .asf.yaml, so a run that never happens blocks nothing. pull_request: + paths-ignore: + - '.claude/**' push: branches: - 'main' + paths-ignore: + - '.claude/**' permissions: read-all diff --git a/Jenkinsfile b/Jenkinsfile index 1fce32d7cb..105625d5dd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -41,9 +41,37 @@ pipeline { cleanWs deleteDirs: true, patterns: [[pattern: '**/target/**', type: 'INCLUDE']] } } + stage('Detect changes') { + steps { + script { + // Skip the build when a push only touched .claude/ - agent + // instructions, not code. Fails open: anything unexpected (no + // previous successful build, an unreachable commit, a git error) + // reports true and the build runs as before. + env.CODE_CHANGED = sh(returnStdout: true, script: ''' + set -u + base="${GIT_PREVIOUS_SUCCESSFUL_COMMIT:-}" + if [ -z "$base" ] || ! git cat-file -e "${base}^{commit}" 2>/dev/null; then + echo true + exit 0 + fi + outside=$(git diff --name-only "$base" HEAD | grep -vE '^(\\.claude/|$)' || true) + if [ -n "$outside" ]; then + echo true + else + echo false + fi + ''').trim() + echo "Changes outside .claude/: ${env.CODE_CHANGED}" + } + } + } } } stage('JDK 21') { + when { + expression { env.CODE_CHANGED != 'false' } + } agent { label 'ubuntu' } @@ -74,6 +102,9 @@ pipeline { } } stage('JDK 17') { + when { + expression { env.CODE_CHANGED != 'false' } + } agent { label 'ubuntu' }