Skip to content

Commit 3e212fd

Browse files
SoapyREDclaude
andauthored
ci(registry): cut GitHub Releases on tag push, idempotent guard for re-runs (#3)
Glama tracks the GitHub Releases API (not git tags, not the MCP Registry). Without a Release per version, Glama's 'Latest release' cache, tool-count cache, and maintenance grade all stay stale on their first-ever-scrape value. Sprint 1 made tag pushes cascade to MCP Registry but didn't cut Releases — so Glama is still on v1.0.0. Two changes: 1. New 'Cut GitHub Release if missing' step at the end of the job. Idempotent — skips when 'gh release view' shows the tag already has a Release. Notes pulled from CHANGELOG.md via awk between '## VERSION' and the next '## ' heading; falls back to a pointer line when no entry exists (v1.0.4 / v1.0.5 in current history). The Release is marked --latest=true if its version matches MCP Registry's current isLatest; otherwise --latest=false (historical backfill). 2. New 'Check if version is already on MCP Registry' guard that surrounds the existing publish/auth/server.json-commit steps with 'if: registry_check.outputs.already_published != true'. Without this, re-running the workflow on an already-published tag (which will happen during the Phase 2 historical backfill when 'gh release create' fires both push:tags and release:published) would error at the mcp-publisher publish step. Walkthrough — next v2.1.2 release: npm version patch # bumps package.json, creates v2.1.2 tag npm publish # canonical event, npm now has 2.1.2 git push --follow-tags # tag push fires publish-registry.yml # → publishes 2.1.2 to MCP Registry # → cuts GitHub Release v2.1.2 with notes # → Glama picks up on its next scrape Audit: docs/audit/distribution-cascade-2026-05-16.md (2026-05-17 amendment) in the main freighttools repo documents the diagnosis and walkthrough. Co-authored-by: SoapyRED <soapyred@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 07b110c commit 3e212fd

1 file changed

Lines changed: 82 additions & 1 deletion

File tree

.github/workflows/publish-registry.yml

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,17 @@ name: Publish to MCP Registry
1010
# package.json on main, so the dispatch needs no input for the
1111
# common "publish whatever main says" case.
1212
#
13+
# Both the MCP Registry publish and the GitHub Release creation steps are
14+
# idempotent — re-running the workflow on a tag whose version is already
15+
# isLatest on the Registry, or for which a Release already exists, is a
16+
# no-op for that surface. This lets backfills (and the dual-trigger case
17+
# where one tag push fires both push:tags and release:published) succeed
18+
# without duplicate-publish errors.
19+
#
1320
# The diagnosis at docs/audit/distribution-cascade-2026-05-16.md in the
14-
# main freighttools repo documents why this trigger set exists.
21+
# main freighttools repo documents why this trigger set exists, and the
22+
# 2026-05-17 amendment in the same doc covers the Release-gap discovery
23+
# that drove this version of the workflow.
1524
on:
1625
push:
1726
tags: ['v*']
@@ -94,13 +103,36 @@ jobs:
94103
- name: Validate server.json
95104
run: ./mcp-publisher validate ./server.json
96105

106+
- name: Check if version is already on MCP Registry
107+
id: registry_check
108+
run: |
109+
VERSION="${{ steps.version.outputs.version }}"
110+
RESULT=$(curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=freightutils")
111+
# Skip publish if this version appears in the Registry history,
112+
# in any status. mcp-publisher errors on duplicate-version
113+
# publish, so this guard makes the workflow safe to re-run on
114+
# backfill tag pushes (v2.0.0, v1.1.0, v1.0.5, v1.0.4) where
115+
# the version was published long ago via a manual path.
116+
ALREADY=$(echo "$RESULT" | jq -r --arg v "$VERSION" '.servers[] | select(.server.version == $v) | .server.version' | head -1)
117+
LATEST=$(echo "$RESULT" | jq -r '.servers[] | select(._meta["io.modelcontextprotocol.registry/official"].isLatest == true) | .server.version')
118+
if [ -n "$ALREADY" ]; then
119+
echo "Registry already has version $VERSION (current isLatest=$LATEST) — skipping publish step."
120+
echo "already_published=true" >> "$GITHUB_OUTPUT"
121+
else
122+
echo "Registry does not have $VERSION (current isLatest=$LATEST) — publish step will run."
123+
echo "already_published=false" >> "$GITHUB_OUTPUT"
124+
fi
125+
97126
- name: Authenticate via GitHub OIDC
127+
if: steps.registry_check.outputs.already_published != 'true'
98128
run: ./mcp-publisher login github-oidc
99129

100130
- name: Publish to MCP Registry
131+
if: steps.registry_check.outputs.already_published != 'true'
101132
run: ./mcp-publisher publish
102133

103134
- name: Commit updated server.json back to main
135+
if: steps.registry_check.outputs.already_published != 'true'
104136
run: |
105137
git config user.name "github-actions[bot]"
106138
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
@@ -123,3 +155,52 @@ jobs:
123155
else
124156
echo "::warning::Registry shows isLatest=$LATEST_VERSION but expected $EXPECTED. May be a replication delay."
125157
fi
158+
159+
- name: Cut GitHub Release if missing
160+
# Glama reads the GitHub Releases API, not git tags or the MCP
161+
# Registry. Without a Release per version, Glama's "Latest
162+
# release" / tool-count cache / maintenance grade all stay
163+
# stale. This step creates the Release if one does not already
164+
# exist for the current tag. Notes are pulled from CHANGELOG.md
165+
# (entry between `## X.Y.Z` and the next `## ` heading); if no
166+
# entry exists, falls back to a pointer line. Idempotent —
167+
# safe to re-run on existing-Release tags.
168+
env:
169+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
170+
run: |
171+
VERSION="${{ steps.version.outputs.version }}"
172+
TAG="v$VERSION"
173+
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
174+
echo "GitHub Release $TAG already exists — skipping."
175+
exit 0
176+
fi
177+
# Extract CHANGELOG entry for this version. The CHANGELOG.md
178+
# heading format is `## X.Y.Z — YYYY-MM-DD` (em-dash, em-dash
179+
# then date may be present). Match `## VERSION` at start of
180+
# line, capture lines until the next `## ` heading.
181+
NOTES_FILE=$(mktemp)
182+
awk -v v="$VERSION" '
183+
$0 ~ "^## "v"([^0-9]|$)" { capturing=1; next }
184+
/^## [0-9]/ && capturing { exit }
185+
capturing { print }
186+
' CHANGELOG.md > "$NOTES_FILE"
187+
# Trim leading/trailing blank lines.
188+
sed -i -e '/./,$!d' -e ':a' -e '/^\n*$/{$d;N;ba' -e '}' "$NOTES_FILE"
189+
if [ ! -s "$NOTES_FILE" ]; then
190+
echo "No CHANGELOG.md entry found for $VERSION — using pointer note." >&2
191+
echo "See [CHANGELOG.md](./CHANGELOG.md) for details and the [npm package page](https://www.npmjs.com/package/freightutils-mcp/v/$VERSION) for the release artefact." > "$NOTES_FILE"
192+
fi
193+
# If this tag is the current MCP Registry isLatest, mark as
194+
# the GitHub latest too. Otherwise create as a historical
195+
# Release (--latest=false).
196+
LATEST_FLAG="--latest=false"
197+
REGISTRY_LATEST=$(curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=freightutils" | jq -r '.servers[] | select(._meta["io.modelcontextprotocol.registry/official"].isLatest == true) | .server.version')
198+
if [ "$REGISTRY_LATEST" = "$VERSION" ]; then
199+
LATEST_FLAG="--latest=true"
200+
fi
201+
gh release create "$TAG" \
202+
--repo "$GITHUB_REPOSITORY" \
203+
--title "$TAG" \
204+
--notes-file "$NOTES_FILE" \
205+
$LATEST_FLAG
206+
echo "Created GitHub Release $TAG ($LATEST_FLAG)"

0 commit comments

Comments
 (0)