From fa12b3abb40fb70c12bec349f37e58cedfb8f24c Mon Sep 17 00:00:00 2001 From: Anirudh Cheriyachanaseri Bijay Date: Mon, 29 Jun 2026 16:10:32 +0530 Subject: [PATCH 1/4] Fix version bump pre-commit script to account for Cursor and Codex as well --- .../pre-commit.d/02-auto-bump-version.sh | 130 +++++++++++++----- 1 file changed, 97 insertions(+), 33 deletions(-) diff --git a/.githooks/pre-commit.d/02-auto-bump-version.sh b/.githooks/pre-commit.d/02-auto-bump-version.sh index 4de515b..425688c 100755 --- a/.githooks/pre-commit.d/02-auto-bump-version.sh +++ b/.githooks/pre-commit.d/02-auto-bump-version.sh @@ -1,44 +1,108 @@ -#!/bin/bash -# Auto-bump patch version if plugin files are staged and version wasn't bumped. -# Uses the same path regex as scripts/check-version-bump.sh (CI check). +#!/usr/bin/env bash +# Auto-bump patch version across all plugin manifests in lockstep when plugin files +# are staged. Uses the same path regex as scripts/check-version-bump.sh (the CI gate). +# +# The new version, v_new, is determined as follows: +# +# v_main = max {version of each manifest on main} +# v_current = max {version of each manifest in the working tree} +# v_new = max {v_current, v_main + 0.0.1} +# +# where max is defined with respect to semver comparison and v_main + 0.0.1 is the patch +# increment of v_main. Versions are plain x.y.z (no pre-release or build metadata). A +# manifest whose version is not a plain x.y.z string is dropped from the max computation +# but still rewritten to v_new; a manifest that is not valid JSON aborts the commit with +# a message. set -e cd "$(git rev-parse --show-toplevel)" -PLUGIN_JSON="plugins/glean/.claude-plugin/plugin.json" -PLUGIN_PATHS="^(src/|plugins/glean/(dist/|skills/|start\.sh|\.mcp\.json|package\.json|\.claude-plugin/plugin\.json))" +BASE_REF="origin/main" + +# Per-host plugin manifests — kept in lockstep on a single version. +MANIFESTS=( + "plugins/glean/.claude-plugin/plugin.json" + "plugins/glean/.codex-plugin/plugin.json" + "plugins/glean/.cursor-plugin/plugin.json" +) + +PLUGIN_PATHS='^(src/|plugins/glean/(dist/|skills/|start\.sh|\.mcp\.json|\.mcp\.codex\.json|package\.json|\.(claude|codex|cursor)-plugin/plugin\.json))' if ! git diff --cached --name-only | grep -qE "$PLUGIN_PATHS"; then exit 0 fi -BASE_REF="origin/main" -BASE_VERSION=$(git show "$BASE_REF":"$PLUGIN_JSON" 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8')).version" 2>/dev/null) || exit 0 -CURRENT_VERSION=$(node -p "require('./$PLUGIN_JSON').version") - -# Skip if already ahead of base. -if node -e " - const a = '$BASE_VERSION'.split('.').map(Number); - const b = '$CURRENT_VERSION'.split('.').map(Number); - for (let i = 0; i < 3; i++) { - if (b[i] > a[i]) process.exit(0); - if (b[i] < a[i]) process.exit(1); - } - process.exit(1); -" 2>/dev/null; then - exit 0 -fi +# Versions on the base branch. Skip a manifest that is absent, unparseable, or whose +# version is not a plain x.y.z string — only valid semver versions form the floor. +base_versions=() +for m in "${MANIFESTS[@]}"; do + blob=$(git show "$BASE_REF:$m" 2>/dev/null) || continue + v=$(printf '%s' "$blob" | node -e ' + const fs = require("fs"); + let pkg; + try { pkg = JSON.parse(fs.readFileSync(0, "utf-8")); } catch (e) { process.exit(0); } + const v = pkg.version; + if (typeof v === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(v)) process.stdout.write(v); + ' 2>/dev/null) + [ -n "$v" ] && base_versions+=("$v") +done + +# Versions in the working tree. A missing manifest is skipped (e.g. a platform being +# removed). Invalid JSON aborts the commit with a clear message. A valid-JSON manifest +# whose version is not a plain x.y.z string stays in the write set (so it gets rewritten +# to the target) but is dropped from the max so it cannot poison the result. +current_versions=() +existing_manifests=() +for m in "${MANIFESTS[@]}"; do + [ -f "$m" ] || continue + if ! v=$(node -e ' + const fs = require("fs"); + let pkg; + try { pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf-8")); } catch (e) { process.exit(3); } + const v = pkg.version; + if (typeof v === "string" && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(v)) process.stdout.write(v); + ' "$m"); then + echo "pre-commit: $m is not valid JSON; fix it before committing." >&2 + exit 1 + fi + existing_manifests+=("$m") + [ -n "$v" ] && current_versions+=("$v") +done + +# No manifests on disk => nothing to bump. +[ ${#existing_manifests[@]} -eq 0 ] && exit 0 -NEW_VERSION=$(node -e " - const [major, minor, patch] = '$BASE_VERSION'.split('.').map(Number); - console.log(major + '.' + minor + '.' + (patch + 1)); -") +# Non-semver versions were filtered out above, +# so only valid x.y.z values reach this stage. If neither base nor the working tree +# yielded a valid version, there is nothing to compute — abort with a message. +TARGET=$(node -e ' + const args = process.argv.slice(1); + const sep = args.indexOf("--"); + const base = args.slice(0, sep); + const cur = args.slice(sep + 1); + const parse = v => v.split(".").map(Number); + const cmp = (a, b) => { for (let i = 0; i < 3; i++) { if (a[i] > b[i]) return 1; if (a[i] < b[i]) return -1; } return 0; }; + const maxOf = vs => vs.map(parse).reduce((m, v) => cmp(v, m) > 0 ? v : m); + const candidates = []; + if (cur.length) candidates.push(maxOf(cur)); + if (base.length) { const b = maxOf(base); candidates.push([b[0], b[1], b[2] + 1]); } + if (!candidates.length) process.exit(7); + const target = candidates.reduce((m, v) => cmp(v, m) > 0 ? v : m); + console.log(target.join(".")); +' "${base_versions[@]}" -- "${current_versions[@]}") || { + echo "pre-commit: no valid x.y.z version found on $BASE_REF or in the working tree; cannot determine a version to bump to." >&2 + exit 1 +} -node -e " - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('$PLUGIN_JSON', 'utf-8')); - pkg.version = '$NEW_VERSION'; - fs.writeFileSync('$PLUGIN_JSON', JSON.stringify(pkg, null, 2) + '\n'); -" +# Write target into every manifest (only the version field changes) and stage it. +for m in "${existing_manifests[@]}"; do + node -e ' + const fs = require("fs"); + const [file, version] = process.argv.slice(1); + const pkg = JSON.parse(fs.readFileSync(file, "utf-8")); + pkg.version = version; + fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n"); + ' "$m" "$TARGET" + git add "$m" +done -git add "$PLUGIN_JSON" -echo "Auto-bumped plugin version: $BASE_VERSION → $NEW_VERSION" +echo "Auto-bumped all plugin manifests to version $TARGET" From 6f31af2c9b112d138aedfeb7c9409a4192490954 Mon Sep 17 00:00:00 2001 From: Anirudh Cheriyachanaseri Bijay Date: Mon, 29 Jun 2026 16:12:08 +0530 Subject: [PATCH 2/4] Use environment Bash in shebang --- .githooks/pre-commit.d/01-rebuild-dist.sh | 2 +- scripts/check-version-bump.sh | 2 +- scripts/pack-plugin.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.githooks/pre-commit.d/01-rebuild-dist.sh b/.githooks/pre-commit.d/01-rebuild-dist.sh index 152ef8e..f9b0b8a 100755 --- a/.githooks/pre-commit.d/01-rebuild-dist.sh +++ b/.githooks/pre-commit.d/01-rebuild-dist.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Rebuild dist/index.js and stage it on every commit that touches source files. set -e cd "$(git rev-parse --show-toplevel)" diff --git a/scripts/check-version-bump.sh b/scripts/check-version-bump.sh index 34efd79..65e6a35 100755 --- a/scripts/check-version-bump.sh +++ b/scripts/check-version-bump.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # CI check: verify the plugin version was incremented when plugin files change. # # The plugin ships one manifest per host (Claude, Codex, Cursor). They must diff --git a/scripts/pack-plugin.sh b/scripts/pack-plugin.sh index 0bcf290..98cf82a 100755 --- a/scripts/pack-plugin.sh +++ b/scripts/pack-plugin.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Produce a .plugin bundle for testing. The output is named # glean-.plugin, consumable by Cowork's local upload. set -euo pipefail From 7f0072e30fa4b16b9d5560925359176e33ac2fbd Mon Sep 17 00:00:00 2001 From: Anirudh Cheriyachanaseri Bijay Date: Tue, 30 Jun 2026 14:59:38 +0530 Subject: [PATCH 3/4] Abort with error on missing manifests in either staged changes or `main` Pros: - Forces update of the hook when a manifest is deleted. Cons: - Forces multiple PRs when a manifest is created. The first PR will create the manifest, the second will update the hook. The CI check is unaffected. --- .../pre-commit.d/02-auto-bump-version.sh | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/.githooks/pre-commit.d/02-auto-bump-version.sh b/.githooks/pre-commit.d/02-auto-bump-version.sh index 425688c..46d5b10 100755 --- a/.githooks/pre-commit.d/02-auto-bump-version.sh +++ b/.githooks/pre-commit.d/02-auto-bump-version.sh @@ -12,7 +12,9 @@ # increment of v_main. Versions are plain x.y.z (no pre-release or build metadata). A # manifest whose version is not a plain x.y.z string is dropped from the max computation # but still rewritten to v_new; a manifest that is not valid JSON aborts the commit with -# a message. +# a message. A manifest listed below but missing from the working tree also aborts the +# commit, unless its deletion is staged (i.e. it is being removed) — this keeps MANIFESTS +# in lockstep with the files on disk and matches the CI gate (scripts/check-version-bump.sh). set -e cd "$(git rev-parse --show-toplevel)" @@ -46,14 +48,26 @@ for m in "${MANIFESTS[@]}"; do [ -n "$v" ] && base_versions+=("$v") done -# Versions in the working tree. A missing manifest is skipped (e.g. a platform being -# removed). Invalid JSON aborts the commit with a clear message. A valid-JSON manifest -# whose version is not a plain x.y.z string stays in the write set (so it gets rewritten -# to the target) but is dropped from the max so it cannot poison the result. +# Manifests staged for deletion in this commit. A missing manifest is tolerated only if +# its removal is staged here; any other absence is drift (an accidental delete or a stale +# MANIFESTS entry) and aborts the commit, matching the CI gate. +staged_deletes=$(git diff --cached --diff-filter=D --name-only) + +# Versions in the working tree. A missing-but-not-being-deleted manifest aborts. Invalid +# JSON aborts. A valid-JSON manifest whose version is not a plain x.y.z string stays in the +# write set (so it gets rewritten to the target) but is dropped from the max so it cannot +# poison the result. current_versions=() existing_manifests=() for m in "${MANIFESTS[@]}"; do - [ -f "$m" ] || continue + if [ ! -f "$m" ]; then + if printf '%s\n' "$staged_deletes" | grep -qxF "$m"; then + continue + fi + echo "pre-commit: $m is listed in MANIFESTS but is missing and its removal is not staged." >&2 + echo "pre-commit: restore the file, or drop it from MANIFESTS (here and in scripts/check-version-bump.sh)." >&2 + exit 1 + fi if ! v=$(node -e ' const fs = require("fs"); let pkg; From 88673c5ed5fd8d18229b738aa8b4da55bad3d693 Mon Sep 17 00:00:00 2001 From: Anirudh Cheriyachanaseri Bijay Date: Tue, 30 Jun 2026 15:36:45 +0530 Subject: [PATCH 4/4] Force update of the hook in the same commit as or in a prior commit to that which deletes a manifest --- .../pre-commit.d/02-auto-bump-version.sh | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/.githooks/pre-commit.d/02-auto-bump-version.sh b/.githooks/pre-commit.d/02-auto-bump-version.sh index 46d5b10..79b8a47 100755 --- a/.githooks/pre-commit.d/02-auto-bump-version.sh +++ b/.githooks/pre-commit.d/02-auto-bump-version.sh @@ -12,9 +12,9 @@ # increment of v_main. Versions are plain x.y.z (no pre-release or build metadata). A # manifest whose version is not a plain x.y.z string is dropped from the max computation # but still rewritten to v_new; a manifest that is not valid JSON aborts the commit with -# a message. A manifest listed below but missing from the working tree also aborts the -# commit, unless its deletion is staged (i.e. it is being removed) — this keeps MANIFESTS -# in lockstep with the files on disk and matches the CI gate (scripts/check-version-bump.sh). +# a message. Every manifest listed below must exist in the working tree — a missing one +# aborts the commit (to remove a host, drop it from MANIFESTS in the same commit). This +# keeps MANIFESTS in lockstep with the files on disk. set -e cd "$(git rev-parse --show-toplevel)" @@ -48,24 +48,16 @@ for m in "${MANIFESTS[@]}"; do [ -n "$v" ] && base_versions+=("$v") done -# Manifests staged for deletion in this commit. A missing manifest is tolerated only if -# its removal is staged here; any other absence is drift (an accidental delete or a stale -# MANIFESTS entry) and aborts the commit, matching the CI gate. -staged_deletes=$(git diff --cached --diff-filter=D --name-only) - -# Versions in the working tree. A missing-but-not-being-deleted manifest aborts. Invalid -# JSON aborts. A valid-JSON manifest whose version is not a plain x.y.z string stays in the -# write set (so it gets rewritten to the target) but is dropped from the max so it cannot -# poison the result. +# Versions in the working tree. Every manifest in MANIFESTS must exist — a missing one +# aborts (to remove a host, drop it from MANIFESTS in the same commit). Invalid JSON aborts. +# A valid-JSON manifest whose version is not a plain x.y.z string stays in the write set (so +# it gets rewritten to the target) but is dropped from the max so it cannot poison the result. current_versions=() existing_manifests=() for m in "${MANIFESTS[@]}"; do if [ ! -f "$m" ]; then - if printf '%s\n' "$staged_deletes" | grep -qxF "$m"; then - continue - fi - echo "pre-commit: $m is listed in MANIFESTS but is missing and its removal is not staged." >&2 - echo "pre-commit: restore the file, or drop it from MANIFESTS (here and in scripts/check-version-bump.sh)." >&2 + echo "pre-commit: $m is listed in MANIFESTS but is missing." >&2 + echo "pre-commit: to remove this host, drop it from MANIFESTS in this commit (and from scripts/check-version-bump.sh)." >&2 exit 1 fi if ! v=$(node -e ' @@ -82,7 +74,7 @@ for m in "${MANIFESTS[@]}"; do [ -n "$v" ] && current_versions+=("$v") done -# No manifests on disk => nothing to bump. +# Empty MANIFESTS (e.g. all hosts removed) => nothing to bump. [ ${#existing_manifests[@]} -eq 0 ] && exit 0 # Non-semver versions were filtered out above,