fix(release): wire Hermes into version bumps

Register the Hermes YAML manifest alongside the existing JSON manifests. Route manifest reads and writes by extension through jq or Mike Farah yq v4, with field names and values passed as data.

Preflight every present manifest before the mutating bump loop so a deterministic YAML read failure cannot leave earlier JSON manifests partially updated. Cover check, audit, bump, registry wiring, and byte-for-byte no-partial-write behavior with one focused fixture test.
This commit is contained in:
Drew Ritter
2026-08-06 14:47:14 -07:00
committed by Jesse Vincent
parent 707b155a38
commit 5f8f500b1d
3 changed files with 143 additions and 4 deletions
+1
View File
@@ -1,6 +1,7 @@
{ {
"files": [ "files": [
{ "path": "package.json", "field": "version" }, { "path": "package.json", "field": "version" },
{ "path": ".hermes-plugin/plugin.yaml", "field": "version" },
{ "path": ".claude-plugin/plugin.json", "field": "version" }, { "path": ".claude-plugin/plugin.json", "field": "version" },
{ "path": ".cursor-plugin/plugin.json", "field": "version" }, { "path": ".cursor-plugin/plugin.json", "field": "version" },
{ "path": ".codex-plugin/plugin.json", "field": "version" }, { "path": ".codex-plugin/plugin.json", "field": "version" },
+66 -4
View File
@@ -40,12 +40,72 @@ write_json_field() {
jq "$jq_path = \"$value\"" "$file" > "$tmp" && mv "$tmp" "$file" jq "$jq_path = \"$value\"" "$file" > "$tmp" && mv "$tmp" "$file"
} }
require_tool() {
command -v "$1" >/dev/null 2>&1 || {
echo "error: required tool '$1' is not on PATH" >&2
return 1
}
}
read_yaml_field() {
local file="$1" field="$2"
require_tool yq || return 1
FIELD="$field" yq -er '.[strenv(FIELD)] | select(tag == "!!str")' "$file"
}
write_yaml_field() {
local file="$1" field="$2" value="$3"
FIELD="$field" VALUE="$value" \
yq -i '.[strenv(FIELD)] = strenv(VALUE)' "$file"
}
read_manifest_field() {
local file="$1"
case "$file" in
*.json) read_json_field "$@" ;;
*.yaml) read_yaml_field "$@" ;;
*)
echo "error: unsupported manifest format: $file" >&2
return 1
;;
esac
}
write_manifest_field() {
local file="$1"
case "$file" in
*.json) write_json_field "$@" ;;
*.yaml) write_yaml_field "$@" ;;
*)
echo "error: unsupported manifest format: $file" >&2
return 1
;;
esac
}
# Read the list of declared files from config. # Read the list of declared files from config.
# Outputs lines of "path<TAB>field" # Outputs lines of "path<TAB>field"
declared_files() { declared_files() {
jq -r '.files[] | "\(.path)\t\(.field)"' "$CONFIG" jq -r '.files[] | "\(.path)\t\(.field)"' "$CONFIG"
} }
preflight_manifests() {
local path field fullpath
require_tool jq || return 1
while IFS=$'\t' read -r path field; do
fullpath="$REPO_ROOT/$path"
[[ -f "$fullpath" ]] || continue
if ! read_manifest_field "$fullpath" "$field" >/dev/null; then
echo "error: cannot read declared manifest: $path ($field)" >&2
return 1
fi
done < <(declared_files)
}
# Read the audit exclude patterns from config. # Read the audit exclude patterns from config.
audit_excludes() { audit_excludes() {
jq -r '.audit.exclude[]' "$CONFIG" 2>/dev/null jq -r '.audit.exclude[]' "$CONFIG" 2>/dev/null
@@ -68,7 +128,7 @@ cmd_check() {
continue continue
fi fi
local ver local ver
ver=$(read_json_field "$fullpath" "$field") ver=$(read_manifest_field "$fullpath" "$field")
printf " %-45s %s\n" "$path ($field)" "$ver" printf " %-45s %s\n" "$path ($field)" "$ver"
versions+=("$ver") versions+=("$ver")
done < <(declared_files) done < <(declared_files)
@@ -101,7 +161,7 @@ cmd_audit() {
current_version=$( current_version=$(
while IFS=$'\t' read -r path field; do while IFS=$'\t' read -r path field; do
local fullpath="$REPO_ROOT/$path" local fullpath="$REPO_ROOT/$path"
[[ -f "$fullpath" ]] && read_json_field "$fullpath" "$field" [[ -f "$fullpath" ]] && read_manifest_field "$fullpath" "$field"
done < <(declared_files) | sort | uniq -c | sort -rn | head -1 | awk '{print $2}' done < <(declared_files) | sort | uniq -c | sort -rn | head -1 | awk '{print $2}'
) )
@@ -172,6 +232,8 @@ cmd_bump() {
exit 1 exit 1
fi fi
preflight_manifests
echo "Bumping all declared files to $new_version..." echo "Bumping all declared files to $new_version..."
echo "" echo ""
@@ -182,8 +244,8 @@ cmd_bump() {
continue continue
fi fi
local old_ver local old_ver
old_ver=$(read_json_field "$fullpath" "$field") old_ver=$(read_manifest_field "$fullpath" "$field")
write_json_field "$fullpath" "$field" "$new_version" write_manifest_field "$fullpath" "$field" "$new_version"
printf " %-45s %s -> %s\n" "$path ($field)" "$old_ver" "$new_version" printf " %-45s %s -> %s\n" "$path ($field)" "$old_ver" "$new_version"
done < <(declared_files) done < <(declared_files)
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SCRIPT_SOURCE="$REPO_ROOT/scripts/bump-version.sh"
TEST_ROOT="$(mktemp -d)"
cleanup() {
rm -rf "$TEST_ROOT"
}
trap cleanup EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
make_fixture() {
local repo="$1"
local yaml_body="$2"
mkdir -p "$repo/scripts" "$repo/.hermes-plugin"
cp "$SCRIPT_SOURCE" "$repo/scripts/bump-version.sh"
cat >"$repo/.version-bump.json" <<'JSON'
{
"files": [
{ "path": "package.json", "field": "version" },
{ "path": ".hermes-plugin/plugin.yaml", "field": "version" }
],
"audit": { "exclude": [] }
}
JSON
cat >"$repo/package.json" <<'JSON'
{
"name": "fixture",
"version": "1.2.3"
}
JSON
printf '%s\n' "$yaml_body" >"$repo/.hermes-plugin/plugin.yaml"
}
happy_repo="$TEST_ROOT/happy"
make_fixture "$happy_repo" $'name: superpowers\nversion: 1.2.3'
/bin/bash "$happy_repo/scripts/bump-version.sh" --check >"$TEST_ROOT/check.out"
/bin/bash "$happy_repo/scripts/bump-version.sh" --audit >"$TEST_ROOT/audit.out"
/bin/bash "$happy_repo/scripts/bump-version.sh" 2.3.4 >"$TEST_ROOT/bump.out"
[[ "$(jq -r '.version' "$happy_repo/package.json")" == "2.3.4" ]] \
|| fail "JSON manifest was not bumped"
[[ "$(yq -r '.version' "$happy_repo/.hermes-plugin/plugin.yaml")" == "2.3.4" ]] \
|| fail "YAML manifest was not bumped"
jq -e '
any(.files[];
.path == ".hermes-plugin/plugin.yaml" and .field == "version")
' "$REPO_ROOT/.version-bump.json" >/dev/null \
|| fail "Hermes manifest is not registered"
invalid_repo="$TEST_ROOT/invalid"
make_fixture "$invalid_repo" $'name: superpowers\nversion: 123'
cp "$invalid_repo/package.json" "$TEST_ROOT/package.before"
cp "$invalid_repo/.hermes-plugin/plugin.yaml" "$TEST_ROOT/plugin.before"
if /bin/bash "$invalid_repo/scripts/bump-version.sh" 2.3.4 \
>"$TEST_ROOT/invalid.out" 2>&1; then
fail "bump accepted a non-string YAML version"
fi
cmp -s "$TEST_ROOT/package.before" "$invalid_repo/package.json" \
|| fail "JSON manifest changed before YAML validation failed"
cmp -s "$TEST_ROOT/plugin.before" "$invalid_repo/.hermes-plugin/plugin.yaml" \
|| fail "invalid YAML manifest changed"
echo "Version-bump tests passed"