This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch build/skip-ci-for-claude-only-changes in repository https://gitbox.apache.org/repos/asf/struts.git
commit 21bd2f7bbf01533e152396cebc902605c4333e76 Author: Lukasz Lenart <[email protected]> AuthorDate: Fri Aug 14 12:28:37 2026 +0200 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 <[email protected]> --- .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 5f0214d01..6e7cf4d8b 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 754dc98e3..6d1e86112 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 661b09187..7b6d0523a 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 bf33a5520..becd7c789 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 1fce32d7c..105625d5d 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' }
