Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .githooks/pre-commit.d/01-rebuild-dist.sh
Original file line number Diff line number Diff line change
@@ -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)"
Expand Down
136 changes: 103 additions & 33 deletions .githooks/pre-commit.d/02-auto-bump-version.sh
Original file line number Diff line number Diff line change
@@ -1,44 +1,114 @@
#!/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. 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)"

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. 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
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 '
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

# Empty MANIFESTS (e.g. all hosts removed) => 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"
2 changes: 1 addition & 1 deletion scripts/check-version-bump.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/pack-plugin.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Produce a .plugin bundle for testing. The output is named
# glean-<version>.plugin, consumable by Cowork's local upload.
set -euo pipefail
Expand Down
Loading