From d9c14047719ea0b8875359d788db4bf9f5b05644 Mon Sep 17 00:00:00 2001 From: Brenno Ferrari Date: Tue, 2 Jun 2026 02:16:14 +0200 Subject: [PATCH 1/3] fix: tag the version-bump commit so released tags carry their own version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release workflow created the git tag at main HEAD *before* generate-changelog.ts bumped shard.yaml / vault-manifest.json and committed the result, so the tagged tree always lagged one version behind its tag: v6.2 tag -> tree says version 6.1.0 v6.1 tag -> tree says version 6.0.0 ShardMind reads the manifest `version:` as the shard's identity (and `shardmind update` keys off it), so `shardmind install github:...` resolved the correct latest tag but reported the stale version, and consecutive stale versions can make update detection misfire. generate-changelog.ts already bumps both manifests correctly — the bug was purely ordering. Fix: - Don't create the tag up front. Keep only a pre-flight "tag must not exist" guard on the manual trigger. - After committing the version bump to main, tag THAT commit and push it (-f/--force so the push:tags trigger's pre-bump tag moves forward). - Add a verification step that fails the release if the tagged tree's shard.yaml / vault-manifest version doesn't match the tag (2-component tags like v6.2 are normalized to 6.2.0 for the check). Scope: release pipeline only. No hook-lifecycle changes (PR #96). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 39 ++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 965f87c2..ce28c894 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,15 +51,18 @@ jobs: echo "VERSION=$VERSION" >> "$GITHUB_ENV" - - name: Create tag (manual trigger only) + - name: Pre-flight — tag must not already exist (manual trigger only) if: ${{ github.event_name == 'workflow_dispatch' }} run: | + # Guard only — the tag is created LATER, after the version bump is + # committed, so the tagged tree carries its own version. Creating it + # here (the old flow) tagged main HEAD before generate-changelog + # bumped shard.yaml / vault-manifest.json, so every tagged tree + # lagged one version behind its tag. if git ls-remote --tags origin "refs/tags/$VERSION" | grep -q .; then echo "::error::Tag $VERSION already exists" exit 1 fi - git tag "$VERSION" - git push origin "$VERSION" - name: Run tests run: node --experimental-strip-types --test '.claude/scripts/tests/'*.test.ts @@ -106,6 +109,36 @@ jobs: git commit -m "release: update CHANGELOG and manifests for $VERSION" git push origin main + - name: Tag the release commit + run: | + # Point the tag at the version-bump commit so the tagged tree carries + # its own version (shard.yaml + vault-manifest.json). ShardMind reads + # the manifest `version:` as the shard's identity and `shardmind + # update` keys off it, so a tag whose tree lags its name mislabels + # installs and can misdetect updates. -f / --force handles the + # push:tags trigger, where a human-pushed tag already points at the + # pre-bump commit; we move it forward to the bump commit. (Tag pushes + # via GITHUB_TOKEN don't re-trigger this workflow.) + git tag -f "$VERSION" + git push origin "refs/tags/$VERSION" --force + + - name: Verify tagged tree version matches the tag + run: | + # Fail the release if the tagged tree's manifest version doesn't match + # the tag — the regression guard for the skew this reordering fixes. + want="${VERSION#v}" + case "$want" in + *.*.*) ;; # already 3-component + *.*) want="${want}.0" ;; # normalize 2-component tag (6.2 -> 6.2.0) + esac + shard=$(git show "$VERSION:.shardmind/shard.yaml" | sed -n 's/^version:[[:space:]]*//p' | head -1) + manifest=$(git show "$VERSION:vault-manifest.json" | sed -n 's/.*"version":[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) + echo "tag=$VERSION expected=$want shard.yaml=$shard vault-manifest=$manifest" + if [ "$shard" != "$want" ] || [ "$manifest" != "$want" ]; then + echo "::error::Tagged tree version skew (shard.yaml=$shard manifest=$manifest, expected $want)" + exit 1 + fi + - name: Build vault zip run: | ZIP_NAME="obsidian-mind-${VERSION}.zip" From fec93a8c2f6eae44d56f6ee0380e8fd100c61e23 Mon Sep 17 00:00:00 2001 From: Brenno Ferrari Date: Tue, 2 Jun 2026 02:20:47 +0200 Subject: [PATCH 2/3] fix: normalize release tags to 3-component semver (vX.Y -> vX.Y.Z) Make the git tag match the 3-component manifest version ShardMind resolves against. The Resolve-version step now normalizes vX / vX.Y to vX.Y.0 (vX.Y.Z passes through), so workflow_dispatch cuts v6.2.0 rather than v6.2. For the push:tags trigger, a human-pushed 2-component tag (v6.2) is recorded as TRIGGER_TAG and retired after the canonical v6.2.0 tag is created at the bump commit, so no stale duplicate lingers on the pre-bump commit. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce28c894..e71c4644 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,19 +33,25 @@ jobs: run: | if [ -n "$INPUT_VERSION" ]; then VERSION="$INPUT_VERSION" - if [[ ! "$VERSION" =~ ^v ]]; then - VERSION="v$VERSION" - fi - # Normalize bare major (v4 → v4.0) or major.minor (v4.0 stays v4.0) - if [[ "$VERSION" =~ ^v[0-9]+$ ]]; then - VERSION="${VERSION}.0" - fi else VERSION="${GITHUB_REF#refs/tags/}" + # Remember the exact tag that triggered us so we can retire it if + # normalization renames it (e.g. a pushed v6.2 → released v6.2.0), + # rather than leaving a stale 2-component duplicate behind. + echo "TRIGGER_TAG=$VERSION" >> "$GITHUB_ENV" + fi + [[ "$VERSION" =~ ^v ]] || VERSION="v$VERSION" + + # Normalize to 3-component semver so the tag matches the manifest + # version ShardMind resolves: v6 → v6.0.0, v6.2 → v6.2.0, v6.2.1 stays. + if [[ "$VERSION" =~ ^v[0-9]+$ ]]; then + VERSION="${VERSION}.0.0" + elif [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+$ ]]; then + VERSION="${VERSION}.0" fi - if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then - echo "::error::Invalid version format: $VERSION (expected v*.* or v*.*.*)" + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid version format: $VERSION (expected vX.Y or vX.Y.Z)" exit 1 fi @@ -121,6 +127,12 @@ jobs: # via GITHUB_TOKEN don't re-trigger this workflow.) git tag -f "$VERSION" git push origin "refs/tags/$VERSION" --force + # If a 2-component tag triggered this run (push:tags of v6.2), retire + # it now that the canonical 3-component tag (v6.2.0) points at the + # bump commit — otherwise the old name lingers on the pre-bump commit. + if [ -n "${TRIGGER_TAG:-}" ] && [ "$TRIGGER_TAG" != "$VERSION" ]; then + git push origin ":refs/tags/$TRIGGER_TAG" || true + fi - name: Verify tagged tree version matches the tag run: | From ecba4a5884a83ebf40082fd8813d3ddebf36aa85 Mon Sep 17 00:00:00 2001 From: Brenno Ferrari Date: Tue, 2 Jun 2026 02:25:14 +0200 Subject: [PATCH 3/3] fix: address Copilot review on release workflow Three fixes from the PR #97 review: - prevTag off-by-one: generate-changelog.ts derives the previous release as the second-newest tag, assuming the current release tag exists. Removing the early tag creation broke that on workflow_dispatch. Restore it as a LOCAL (unpushed) staged tag before the script runs; it's moved onto the bump commit and force-pushed later. - verify step ENOENT-tolerance: always verify vault-manifest.json (present in every vault); verify .shardmind/shard.yaml only when it exists in the tag, so non-ShardMind forks stay releasable like the sibling steps already allow. - error message: list the actual accepted inputs (vX, vX.Y, vX.Y.Z). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 49 +++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e71c4644..2b0c4562 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,24 +51,28 @@ jobs: fi if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::Invalid version format: $VERSION (expected vX.Y or vX.Y.Z)" + echo "::error::Invalid version: $VERSION (accepted inputs: vX, vX.Y, or vX.Y.Z, with or without the leading v)" exit 1 fi echo "VERSION=$VERSION" >> "$GITHUB_ENV" - - name: Pre-flight — tag must not already exist (manual trigger only) + - name: Stage release tag locally (manual trigger only) if: ${{ github.event_name == 'workflow_dispatch' }} run: | - # Guard only — the tag is created LATER, after the version bump is - # committed, so the tagged tree carries its own version. Creating it - # here (the old flow) tagged main HEAD before generate-changelog - # bumped shard.yaml / vault-manifest.json, so every tagged tree - # lagged one version behind its tag. + # Guard against clobbering an existing release. if git ls-remote --tags origin "refs/tags/$VERSION" | grep -q .; then echo "::error::Tag $VERSION already exists" exit 1 fi + # Create the tag LOCALLY (not pushed) before generate-changelog runs. + # The script derives the previous release as the second-newest tag, + # which assumes the current release tag exists; without it prevTag + # would be off by one (wrong changelog range + fingerprint diff). The + # "Tag the release commit" step later moves this onto the bump commit + # and force-pushes it. The push:tags trigger already has its tag + # locally from checkout, so only the dispatch path needs this. + git tag "$VERSION" - name: Run tests run: node --experimental-strip-types --test '.claude/scripts/tests/'*.test.ts @@ -136,21 +140,32 @@ jobs: - name: Verify tagged tree version matches the tag run: | - # Fail the release if the tagged tree's manifest version doesn't match - # the tag — the regression guard for the skew this reordering fixes. + # Regression guard for the skew this reordering fixes: the tagged tree + # must carry its own version. VERSION is 3-component already; the case + # is belt-and-suspenders. want="${VERSION#v}" - case "$want" in - *.*.*) ;; # already 3-component - *.*) want="${want}.0" ;; # normalize 2-component tag (6.2 -> 6.2.0) - esac - shard=$(git show "$VERSION:.shardmind/shard.yaml" | sed -n 's/^version:[[:space:]]*//p' | head -1) + case "$want" in *.*.*) ;; *.*) want="${want}.0" ;; esac + + # vault-manifest.json exists in every vault — always verify it. manifest=$(git show "$VERSION:vault-manifest.json" | sed -n 's/.*"version":[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) - echo "tag=$VERSION expected=$want shard.yaml=$shard vault-manifest=$manifest" - if [ "$shard" != "$want" ] || [ "$manifest" != "$want" ]; then - echo "::error::Tagged tree version skew (shard.yaml=$shard manifest=$manifest, expected $want)" + echo "tag=$VERSION expected=$want vault-manifest=$manifest" + if [ "$manifest" != "$want" ]; then + echo "::error::Tagged vault-manifest version=$manifest, expected $want" exit 1 fi + # shard.yaml is optional — forks without ShardMind omit it, and the + # commit step / generate-changelog.ts are ENOENT-tolerant. Verify it + # only when present so this step stays consistent with that invariant. + if git cat-file -e "$VERSION:.shardmind/shard.yaml" 2>/dev/null; then + shard=$(git show "$VERSION:.shardmind/shard.yaml" | sed -n 's/^version:[[:space:]]*//p' | head -1) + echo "shard.yaml=$shard" + if [ "$shard" != "$want" ]; then + echo "::error::Tagged shard.yaml version=$shard, expected $want" + exit 1 + fi + fi + - name: Build vault zip run: | ZIP_NAME="obsidian-mind-${VERSION}.zip"