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 16fd32295 build(ci): decide "does this need a build" in one reusable
workflow (#1965)
16fd32295 is described below
commit 16fd32295d53f5e98dfa9a8b9e51f40fb9593e99
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Sep 18 07:12:03 2026 +0200
build(ci): decide "does this need a build" in one reusable workflow (#1965)
* build(ci): decide "does this need a build" in one reusable workflow
CLAUDE.md, AGENTS.md, SECURITY.md and THREAT_MODEL.md feed nothing in the
build, packaging or runtime, yet every edit to them ran the full matrix plus
Sonar, CodeQL and OWASP. Adding them beside .claude/ in each workflow's
paths-ignore would have meant five copies of one list that must stay
identical, plus a sixth in the maven.yml `changes` job that keeps the
required "Build and Test" check reporting.
Path filters under `on:` cannot be shared, so the decision moves out of the
triggers into a reusable workflow, changes.yml, that owns the list and
outputs `code`. It lists the changed files from the pull-request files API
or, on push, the compare API, and builds for any other event or whenever
the list cannot be fetched. The four workflows call it and gate on its
output; maven.yml keeps gating at step level for the required check.
Patterns are anchored to the repository root.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* build(ci): grant CodeQL's write permissions at job level only
Sonar S8233 flags workflow-level write permissions. The analyze job already
carries its own permissions block, which replaces the workflow-level one, so
the workflow-level security-events and id-token writes never reached it.
Keep contents: read at workflow level and the security-events write on the
job that uploads.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* build(ci): apply the same skip list to the Jenkins build
The Jenkinsfile keeps its own copy of the filter because Jenkins cannot call
a GitHub reusable workflow. Extend it to the root process docs with the same
anchored pattern, and cross-reference the two copies.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.github/workflows/changes.yml | 93 +++++++++++++++++++++++++++++++++++++++++++
.github/workflows/codeql.yml | 22 +++++-----
.github/workflows/maven.yml | 37 +----------------
.github/workflows/owasp.yml | 11 ++---
.github/workflows/sonar.yml | 12 +++---
Jenkinsfile | 14 ++++---
6 files changed, 123 insertions(+), 66 deletions(-)
diff --git a/.github/workflows/changes.yml b/.github/workflows/changes.yml
new file mode 100644
index 000000000..f6faebd1a
--- /dev/null
+++ b/.github/workflows/changes.yml
@@ -0,0 +1,93 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Decides whether a change needs a build. The one place on GitHub that knows
+# which paths do not: .claude/ and the root process documents. Workflows call
+# this instead of filtering by path in their triggers, so the list is not
+# repeated per workflow and a required check still reports (see maven.yml).
+# The Jenkinsfile carries the same pattern for the ASF Jenkins build; keep
+# the two in step.
+
+name: Detect changes
+
+on:
+ workflow_call:
+ outputs:
+ code:
+ description: "'true' when the change touches anything outside .claude/
and the root process docs"
+ value: ${{ jobs.changes.outputs.code }}
+
+jobs:
+ changes:
+ name: Detect changes outside .claude and the process docs
+ runs-on: ubuntu-latest
+ outputs:
+ code: ${{ steps.filter.outputs.code }}
+ steps:
+ - name: Check which paths the change touches
+ id: filter
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -eu
+ # Anything not matched here needs a build. Every pattern is anchored
+ # to the repository root: a nested file of the same name still
builds.
+
skip='^(\.claude/|CLAUDE\.md$|AGENTS\.md$|SECURITY\.md$|THREAT_MODEL\.md$|$)'
+ # When the file list cannot be fetched, build: the required check
+ # must still report, and building is the safe direction.
+ case "${{ github.event_name }}" in
+ pull_request)
+ files=$(gh api --paginate \
+ "repos/${{ github.repository }}/pulls/${{ github.event.number
}}/files" \
+ --jq '.[].filename') || {
+ echo "Listing the pull request files failed - building."
+ echo "code=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ }
+ ;;
+ push)
+ # A new branch has no "before" to compare against. The compare
API
+ # lists at most 300 files; a push that large is not docs-only.
+ before="${{ github.event.before }}"
+ if [ "$before" = "0000000000000000000000000000000000000000" ];
then
+ echo "New branch - building."
+ echo "code=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ files=$(gh api \
+ "repos/${{ github.repository }}/compare/${before}...${{
github.sha }}" \
+ --jq '.files[].filename') || {
+ echo "Compare failed - building."
+ echo "code=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ }
+ ;;
+ *)
+ echo "Event ${{ github.event_name }} - building."
+ echo "code=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ ;;
+ esac
+ echo "Changed files:"
+ printf '%s\n' "$files"
+ # Tested by emptiness rather than with `grep -qv`, whose exit status
+ # is not reliable across grep implementations.
+ outside=$(printf '%s\n' "$files" | grep -vE "$skip" || true)
+ if [ -n "$outside" ]; then
+ echo "code=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "Only .claude/ or the process docs changed - skipping the
build."
+ echo "code=false" >> "$GITHUB_OUTPUT"
+ fi
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 7d4f41ca2..e56ec1eee 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -21,29 +21,29 @@ 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/**'
+# Write permissions are granted per job below; a job-level block replaces
+# the workflow-level one, so nothing here would reach a job that has its own.
permissions:
- # Needed to upload the results to code-scanning dashboard.
- security-events: write
- actions: read
contents: read
- # Needed to access OIDC token.
- id-token: write
jobs:
+ changes:
+ uses: ./.github/workflows/changes.yml
+ permissions:
+ contents: read
+ pull-requests: read
+
analyze:
name: Analyze
+ needs: changes
+ if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
+ # Needed to upload the results to the code-scanning dashboard.
security-events: write
strategy:
fail-fast: false
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 3e95e88a4..3575dd2ff 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -20,7 +20,7 @@ on:
# 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 always runs instead, and its steps are skipped by
- # condition when only .claude/ changed (see `changes` below).
+ # condition when changes.yml says nothing needs building.
pull_request:
push:
branches:
@@ -28,8 +28,6 @@ on:
- 'develop'
- 'release/*'
- 'support/*'
- paths-ignore:
- - '.claude/**'
workflow_dispatch:
workflow_call:
@@ -41,38 +39,7 @@ env:
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
+ uses: ./.github/workflows/changes.yml
build:
name: Build and Test (JDK ${{ matrix.java }})${{ matrix.profile ==
'-Pjakartaee11' && ' (Jakarta EE 11 + Spring 7)' || matrix.profile }}
diff --git a/.github/workflows/owasp.yml b/.github/workflows/owasp.yml
index 4090151f9..2dde9deb9 100644
--- a/.github/workflows/owasp.yml
+++ b/.github/workflows/owasp.yml
@@ -16,19 +16,13 @@
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
@@ -38,10 +32,13 @@ env:
LANG: en_US.utf8
jobs:
-
+ changes:
+ uses: ./.github/workflows/changes.yml
owasp:
name: OWASP
+ needs: changes
+ if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
env:
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
index dce6d7b25..aa209f81c 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -16,16 +16,10 @@
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
@@ -35,10 +29,14 @@ env:
HAVE_SONARCLOUD_TOKEN: ${{ secrets.SONARCLOUD_TOKEN != '' }}
jobs:
+ changes:
+ uses: ./.github/workflows/changes.yml
+
sonarcloud:
name: Scan
+ needs: changes
runs-on: ubuntu-latest
- if: ${{ !github.event.pull_request.base.repo.fork &&
!github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]'
}}
+ if: ${{ needs.changes.outputs.code == 'true' &&
!github.event.pull_request.base.repo.fork &&
!github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]'
}}
steps:
- uses: actions/checkout@v7
with:
diff --git a/Jenkinsfile b/Jenkinsfile
index 3611fbaae..5709b53f8 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -44,10 +44,12 @@ pipeline {
stage('Detect changes') {
steps {
script {
- // Skip the build when a change only touched .claude/ - agent
- // instructions, not code. Fails open: anything unexpected (no
- // baseline, an unreachable commit, a git error) reports true and
- // the build runs as before.
+ // Skip the build when a change only touched .claude/ or the root
+ // process docs - agent instructions and policy, not code. The
+ // pattern mirrors .github/workflows/changes.yml; keep the two in
+ // step. Fails open: anything unexpected (no baseline, an
+ // unreachable commit, a git error) reports true and the build
+ // runs as before.
//
// On a pull request the baseline is the merge base with the
// target branch, NOT GIT_PREVIOUS_SUCCESSFUL_COMMIT. That
pointer
@@ -70,14 +72,14 @@ pipeline {
echo true
exit 0
fi
- outside=$(git diff --name-only "$base" HEAD | grep -vE
'^(\\.claude/|$)' || true)
+ outside=$(git diff --name-only "$base" HEAD | grep -vE
'^(\\.claude/|CLAUDE\\.md$|AGENTS\\.md$|SECURITY\\.md$|THREAT_MODEL\\.md$|$)'
|| true)
if [ -n "$outside" ]; then
echo true
else
echo false
fi
''').trim()
- echo "Changes outside .claude/: ${env.CODE_CHANGED}"
+ echo "Changes outside .claude/ and the process docs:
${env.CODE_CHANGED}"
}
}
}