Skip to content

build(ci): stop .claude-only changes from triggering full builds - #1846

Merged
lukaszlenart merged 1 commit into
mainfrom
build/skip-ci-for-claude-only-changes
Aug 14, 2026
Merged

build(ci): stop .claude-only changes from triggering full builds#1846
lukaszlenart merged 1 commit into
mainfrom
build/skip-ci-for-claude-only-changes

Conversation

@lukaszlenart

Copy link
Copy Markdown
Member

Editing an agent skill under .claude/ rebuilt the whole project on GitHub Actions and Jenkins. No code changed, so every one of those runs was wasted.

The trap this has to avoid

.asf.yaml makes Build and Test (JDK 17) a required status check on main (and Build and Test (8) on support/struts-6-x-x). GitHub documents that a workflow skipped by path filtering never reports its checks — they stay Pending, and "a pull request that requires those checks to be successful will be blocked from merging."

So a plain paths-ignore on maven.yml's pull_request trigger would make every docs-only PR permanently unmergeable. A job skipped by an if: condition is different: it does report, as skipped, and required checks accept "successful, skipped, or neutral".

What changed

File Approach
codeql.yml, owasp.yml, sonar.yml paths-ignore on both push and pull_request — none of their checks are required, so a run that never happens blocks nothing
maven.yml paths-ignore on push only; on pull_request a small changes job reads the PR's file list and the build job carries if: needs.changes.outputs.code == 'true'
Jenkinsfile pollSCM can't be path-filtered, so the two JDK stages are guarded by a Detect changes stage instead

Jenkins detection fails open: no previous successful commit, an unreachable commit, or any git error reports true and the build runs exactly as before.

One thing worth knowing

The filter tests for a non-empty list of files outside .claude/ rather than using grep -qv. While testing I hit ugrep 7.5.0 (installed locally as grep) returning 1 from -qv on input where -cv counts 1 and -v prints the line — which silently inverts the decision and would have skipped builds for real code changes. GNU grep on the runners wouldn't do that, but testing emptiness behaves the same everywhere.

Exercised against six inputs: .claude/-only (skip), mixed .claude/ + code (build), empty (skip), .claudefoo/ (build — must not be treated as .claude/), code-only (build), nested .claude/.../scripts/*.sh (skip).

Not verified yet

This PR itself touches .github/ and Jenkinsfile, so it exercises the build path, not the skip path. The skip path is first proven by the next .claude/-only PR — #1844 is one, once this merges. If the required check somehow fails to report there, the fallback is to drop the if: from maven.yml and keep the push filter.

Only the 7.x line is changed here; support/struts-6-x-x needs the same edit on its own branch.

🤖 Generated with Claude Code

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 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@lukaszlenart
lukaszlenart merged commit 0ef2017 into main Aug 14, 2026
13 checks passed
@lukaszlenart
lukaszlenart deleted the build/skip-ci-for-claude-only-changes branch August 14, 2026 10:48
lukaszlenart added a commit that referenced this pull request Aug 14, 2026
#1846 gated the maven.yml build job with a job-level `if:` on the
`changes` output, on the reasoning that a job skipped that way still
reports its check as "skipped", which required status checks accept.

That holds for a plain job, but not for a matrix one. A matrix job whose
condition is false is skipped before the matrix expands, so it emits a
single check run named after the raw name template rather than one per
matrix entry. On #1848 the reported name was literally

  Build and Test (JDK ${{ matrix.java }})${{ ... }}

while .asf.yaml requires the context "Build and Test (JDK 17)". That
context never appeared, so it stayed Pending and the pull request could
not be merged - exactly the failure mode #1846 set out to avoid.

Drop the job-level condition and gate the four steps instead. The matrix
expands, all five checks report success under their expanded names, and
no Maven build runs: a .claude-only pull request costs five idle runners
for a few seconds instead of five full builds.

Jenkins is unaffected - stage-level `when` has no matrix to expand.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
lukaszlenart added a commit that referenced this pull request Aug 14, 2026
…ch (#1850)

#1846 guarded the two JDK stages on a diff against
GIT_PREVIOUS_SUCCESSFUL_COMMIT. On a branch build that is the right
baseline. On a pull request build it is not: the pointer is the previous
head of the same PR, so once the PR is rebased - or the target branch is
merged into it - everything the target absorbed in between shows up as a
change of the PR's own.

PR-1848 build #2 is the case. The pull request touches only
.claude/skills/releasing-struts/, but it had been rebased across the
maven.yml fix, and Jenkins computed:

  + base=b633817af047afaa80948404e2e6f1eb78e02b7a
  + git diff --name-only b633817... HEAD
  + outside=.github/workflows/maven.yml
  Changes outside .claude/: true

so both JDK stages ran a full Maven round trip for a documentation-only
change. Since main almost always carries code, this made the filter
useless for any pull request that is ever brought up to date.

Use the merge base with the target branch as the baseline when
CHANGE_TARGET is set. The multibranch checkout already fetches it -

  git fetch ... +refs/heads/main:refs/remotes/origin/main

- so origin/$CHANGE_TARGET resolves in the workspace. Branch builds have
no target and keep the previous-successful-commit baseline.

Fail-open is unchanged and still covers the new path: an unresolvable
merge base (target branch absent) yields an empty base and reports true.

Exercised against the real commits of #1848 either side of its rebase,
and against synthetic heads for: code only, .claude only, mixed, a
.claudefoo/ near miss, a missing target branch, and the three branch
build baselines. All ten behave as intended.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant