Improve version bump enforcement#659
Conversation
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>
There was a problem hiding this comment.
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-bumpedskill + deterministicversion-bumped.shchecker 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 mvfor 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') |
| 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 |
| # --- 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 | ||
|
|
There was a problem hiding this comment.
💡 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 |
There was a problem hiding this comment.
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$' |
There was a problem hiding this comment.
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 👍 / 👎.
This PR improves the reliability of bumping in
version.gradle.ktswhen a new branch is created and something changes in it.The PR adds the
/version-bumpedskill and related Claude hooks. The code was made agent-neutral as much as possible.Also, the language around using
git mvwas made more strict because Codex tend to "cut the corners" time after time.