Skip to content

Improve version bump enforcement#659

Merged
alexander-yevsyukov merged 12 commits into
masterfrom
improve-version-bump-enforcement
May 20, 2026
Merged

Improve version bump enforcement#659
alexander-yevsyukov merged 12 commits into
masterfrom
improve-version-bump-enforcement

Conversation

@alexander-yevsyukov
Copy link
Copy Markdown
Contributor

@alexander-yevsyukov alexander-yevsyukov commented May 19, 2026

This PR improves the reliability of bumping in version.gradle.kts when a new branch is created and something changes in it.

The PR adds the /version-bumped skill and related Claude hooks. The code was made agent-neutral as much as possible.

Also, the language around using git mv was made more strict because Codex tend to "cut the corners" time after time.

alexander-yevsyukov and others added 8 commits May 19, 2026 20:03
Add a three-layer guard so a feature branch with publishable changes
cannot overwrite Maven Local artifacts that integration tests in
consumer repos depend on:

- `.agents/scripts/version-bumped.sh` — deterministic check that
  classifies the branch diff vs base into publishable vs
  non-publishable and verifies `version.gradle.kts` is strictly greater
  when publishable files changed. N/A when the repo has no root
  `version.gradle.kts`.
- `.claude/scripts/publish-version-gate.sh` — PreToolUse hook that
  blocks `./gradlew … (build|publish|publishToMavenLocal|
  publishAllPublicationsToMavenLocal)` when the check fails. Broad by
  design: `build` itself is publish-risky because some repos chain
  `publishToMavenLocal` into integration-test prerequisites.
- `/version-bumped` skill — composable wrapper that runs the check and
  invokes `/bump-version` to auto-recover when needed. Hooked into
  `/dependency-update`, `/bump-gradle`, `/java-to-kotlin`, and
  `/move-files` as a final step so the user is never surprised by a
  blocked gradle command later.

`/pre-pr` step 2 keeps its stricter "always bump for versioned repos"
PR policy unchanged — the new check serves the day-to-day Maven-Local
overwrite case, which is a strictly looser rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Running `./gradlew buildDependants` (the `ConfigTester` task) on every
`local/` move is too strict for the routine dependency-refresh path —
it forces a full downstream rebuild even when the bump is trivial.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous wording prescribed a Conventional Commits–style subject
that does not match this repo's commit log, and assumed a bulk external
refresh as the default. In practice most runs touch only `local/` or a
small coordinated external set (e.g. Protobuf + gRPC together). Show
three worked examples matching the repo's existing `` Bump X -> `Y` ``
convention instead of a single canonical template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous note implied this script enforced a global "version must
be bumped" rule, which conflicted with the user's expectation: CI's
`checkVersionIncrement` requires a bump for any change in a versioned
repo, regardless of whether the diff is publishable. Reframe the note
to scope this script to the Maven Local overwrite case (its actual
purpose) and point to `/pre-pr` step 2 for the stricter CI-aligned
PR-time policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Premise: any feature branch is a candidate for publishing, even when
the only change is the version bump itself (used to retry a publish
that failed because Maven repositories were overloaded). The previous
classifier excluded docs/agent-config diffs as "non-publishable", but
that gave a false sense of security: CI's `checkVersionIncrement`
requires a bump for any branch, so the relaxation just deferred the
failure to CI.

- `version-bumped.sh`: drop `is_publishable()` and the file-by-file
  walk. Replace with a simple "any committed/worktree/untracked
  difference vs base" check. Header rewritten to clarify this script
  is complementary to `checkVersionIncrement` (local git diff vs
  remote Maven metadata) — not a duplicate.
- `publish-version-gate.sh`: drop "publishable changes" phrasing from
  the BLOCK message.
- `version-bumped/SKILL.md`: rewrite to drop publishable-vs-not
  language; add the retry-publish rationale.
- `dependency-update`, `bump-gradle`, `java-to-kotlin`, `move-files`
  SKILL.md: simplify the final-step language to the universal "every
  branch needs a bump" framing. `move-files` step 6 is no longer
  conditional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the existing convention set by
`.agents/skills/update-copyright/scripts/update_copyright.py` — the
script that implements a skill lives under that skill's directory. The
Claude hook that also calls it just crosses the skill boundary to
invoke its public entry point; that does not change ownership.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
It is a bash script — the extension makes that obvious at a glance and
matches the other hook scripts in `.claude/scripts/`. Updates the
`PostToolUse` hook command in `.claude/settings.json` accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexander-yevsyukov alexander-yevsyukov self-assigned this May 19, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens version-bump enforcement by introducing a new /version-bumped skill and adding tool hooks (Claude + Codex) that gate risky commands (e.g., ./gradlew build/publish*) unless the branch version is strictly greater than the base ref, while also consolidating hook scripts under .agents/.

Changes:

  • Added the version-bumped skill + deterministic version-bumped.sh checker and integrated it into other skills as a final-step guard.
  • Added new agent hooks/gates (pre-pr-gate.sh, publish-version-gate.sh) and moved/standardized hook script locations to .agents/scripts/.
  • Tightened file-move guidance to require git mv for tracked files.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
AGENTS.md Adds stricter guidance to use git mv for tracked file moves to preserve history.
.codex/hooks.json Adds Codex hook wiring for version protection, pre-PR gating, publish gating, and sanitization.
.claude/settings.local.json Expands local Claude allow-list to permit running the new version-bumped script.
.claude/settings.json Redirects Claude hooks to .agents/scripts/* and adds the publish-version gate hook.
.claude/scripts/sanitize-source-code.kt Removes the legacy (misnamed) sanitize hook script.
.agents/skills/version-bumped/SKILL.md Documents the new /version-bumped guard skill behavior and rationale.
.agents/skills/version-bumped/scripts/version-bumped.sh Implements the deterministic base-vs-branch version bump check.
.agents/skills/pre-pr/SKILL.md Updates hook path reference from .claude/scripts to .agents/scripts.
.agents/skills/move-files/SKILL.md Enforces git mv and adds /version-bumped as a final step.
.agents/skills/java-to-kotlin/SKILL.md Adds /version-bumped as a final step after conversion verification.
.agents/skills/dependency-update/SKILL.md Adds /version-bumped before build and updates commit-message guidance.
.agents/skills/bump-gradle/SKILL.md Adds /version-bumped as a required final step.
.agents/scripts/sanitize-source-code.sh Reintroduces sanitization as a .sh script with Codex/Claude input handling.
.agents/scripts/publish-version-gate.sh Adds a pre-tool gate to block risky ./gradlew invocations without a bump.
.agents/scripts/protect-version-file.sh Extends version-file protection to also detect Codex apply_patch changes.
.agents/scripts/pre-pr-gate.sh Adds a pre-tool gate to block gh pr create unless /pre-pr passed for HEAD.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

block_needed=1
break
fi
done < <(printf '%s' "$cmd" | tr ';&|' '\n\n\n')
Comment on lines +81 to +85
committed=$(git diff --name-only "$merge_base"..HEAD 2>/dev/null || true)
worktree=$(git diff --name-only HEAD 2>/dev/null || true)
untracked=$(git ls-files --others --exclude-standard 2>/dev/null || true)

if [ -z "$committed" ] && [ -z "$worktree" ] && [ -z "$untracked" ]; then
Comment on lines +144 to +152
# --- Strict-greater comparison via `sort -V` -----------------------------
if [ "$head_version" = "$base_version" ]; then
cmp="equal"
elif [ "$(printf '%s\n%s\n' "$base_version" "$head_version" | sort -V | tail -n1)" = "$head_version" ]; then
cmp="greater"
else
cmp="lesser"
fi

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5cc1e427aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

risky_segment() {
local seg="$1"
# Must start with a gradlew invocation.
printf '%s' "$seg" | grep -qE '^[[:space:]]*\.?/?(config/)?gradlew([[:space:]]|$)' || return 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match wrapped gradlew commands in publish gate

The gate only recognizes segments that start with gradlew, so publish-risky invocations wrapped with an environment assignment or shell grouping (for example JAVA_HOME=... ./gradlew build or (./gradlew build)) bypass this check entirely. In those cases the hook does not run version-bumped.sh, so a branch that has not bumped version.gradle.kts can still run publish-capable Gradle tasks and overwrite Maven Local artifacts the guard is meant to protect.

Useful? React with 👍 / 👎.

fi

printf '%s\n' "$command" \
| grep -qE '^\*\*\* (Add|Update|Delete) File: (.+/)?version\.gradle\.kts$'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect apply_patch moves into version.gradle.kts

The protected-file detector only matches *** Add/Update/Delete File: headers, so an apply_patch that moves another file to version.gradle.kts via *** Move to: is not treated as touching the protected file. That bypasses the direct-edit block and allows creating/replacing version.gradle.kts without going through /bump-version, which contradicts the hook’s enforcement goal.

Useful? React with 👍 / 👎.

@alexander-yevsyukov alexander-yevsyukov merged commit bae2902 into master May 20, 2026
3 checks passed
@alexander-yevsyukov alexander-yevsyukov deleted the improve-version-bump-enforcement branch May 20, 2026 07:59
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.

3 participants