This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 0ef20170c build(ci): stop .claude-only changes from triggering full
builds (#1846)
0ef20170c is described below
commit 0ef20170ce1f3be9d5e3e1e5a4b770ef40a5c6d6
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Aug 14 12:48:33 2026 +0200
build(ci): stop .claude-only changes from triggering full builds (#1846)
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 572f99f08..f3cf28201 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'
}