fix(release): recover verified partial registry publications

Validate original exact-source qualification, release tag, retained package provenance and npm integrity before the corrected public-package canary. Publish only the existing release's MCP metadata through a shared idempotent publisher; never recreate tags or npm versions.
This commit is contained in:
morluto
2026-10-14 21:15:34 +08:00
parent 0b4fd11c8c
commit 790657dc0d
13 changed files with 806 additions and 23 deletions
+41
View File
@@ -0,0 +1,41 @@
name: Publish MCP Registry metadata
description: Validate and publish one released server.json, preserving an identical existing version
inputs:
directory:
description: Directory containing the released server.json
default: .
runs:
using: composite
steps:
- name: Install MCP Registry publisher
shell: bash
working-directory: ${{ inputs.directory }}
env:
MCP_PUBLISHER_VERSION: v1.8.0
MCP_PUBLISHER_SHA256_LINUX_AMD64: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
run: |
archive="mcp-publisher_linux_amd64.tar.gz"
curl -fsSLo "${archive}" \
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/${archive}"
echo "${MCP_PUBLISHER_SHA256_LINUX_AMD64} ${archive}" | sha256sum --check --strict
tar xzf "${archive}" mcp-publisher
chmod +x mcp-publisher
- name: Validate MCP Registry metadata
shell: bash
working-directory: ${{ inputs.directory }}
run: ./mcp-publisher validate
- name: Check existing registry version
id: registry
shell: bash
working-directory: ${{ inputs.directory }}
run: node "${GITHUB_ACTION_PATH}/registry-state.mjs"
- name: Authenticate to MCP Registry
if: steps.registry.outputs.exists != 'true'
shell: bash
working-directory: ${{ inputs.directory }}
run: ./mcp-publisher login github-oidc
- name: Publish MCP Registry metadata
if: steps.registry.outputs.exists != 'true'
shell: bash
working-directory: ${{ inputs.directory }}
run: ./mcp-publisher publish
@@ -0,0 +1,4 @@
export function registryPublicationState(
server: Readonly<{ name: string; version: string } & Record<string, unknown>>,
request?: (url: URL, init: RequestInit) => Promise<Response>,
): Promise<"present" | "absent">;
@@ -0,0 +1,89 @@
import assert from "node:assert/strict";
import { appendFile, readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
/** Exact-version routes currently return false 404s; enumerate the filtered listing. */
export async function registryPublicationState(server, request = fetch) {
assert.equal(typeof server?.name, "string");
assert.equal(typeof server?.version, "string");
const cursors = new Set();
let cursor;
const matches = [];
do {
const url = new URL(
"https://registry.modelcontextprotocol.io/v0.1/servers",
);
url.search = new URLSearchParams({
search: server.name,
version: server.version,
include_deleted: "true",
limit: "100",
}).toString();
if (cursor !== undefined) url.searchParams.set("cursor", cursor);
const response = await request(url, {
signal: AbortSignal.timeout(30_000),
});
if (!response.ok)
throw new Error(`Registry lookup failed: HTTP ${response.status}`);
const page = await response.json();
assert(Array.isArray(page?.servers), "Registry response omitted servers");
assert(
page.metadata && typeof page.metadata === "object",
"Registry response omitted metadata",
);
for (const entry of page.servers) {
assert(
typeof entry?.server?.name === "string" &&
typeof entry.server.version === "string",
"Malformed registry server record",
);
if (
entry.server.name === server.name &&
entry.server.version === server.version
)
matches.push(entry);
}
cursor = page.metadata.nextCursor;
if (cursor !== undefined) {
assert(
typeof cursor === "string" && cursor.length > 0 && !cursors.has(cursor),
"Registry returned a non-advancing cursor",
);
cursors.add(cursor);
}
} while (cursor !== undefined);
assert(
matches.length <= 1,
"Registry returned ambiguous exact-version records",
);
if (matches.length === 0) return "absent";
assert.equal(
matches[0]._meta?.["io.modelcontextprotocol.registry/official"]?.status,
"active",
"Existing registry version is not active",
);
assert.deepEqual(
matches[0].server,
server,
"Existing registry version differs from released metadata",
);
return "present";
}
if (
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
const state = await registryPublicationState(
JSON.parse(await readFile("server.json", "utf8")),
);
assert(process.env.GITHUB_OUTPUT, "GITHUB_OUTPUT is required");
await appendFile(
process.env.GITHUB_OUTPUT,
`exists=${state === "present"}\n`,
);
process.stdout.write(
`Registry version is ${state}; ${state === "present" ? "preserving identical published metadata" : "publication may proceed"}.\n`,
);
}
+105
View File
@@ -0,0 +1,105 @@
name: Recover published release
on:
workflow_dispatch:
inputs:
version:
description: Existing npm and GitHub release version
required: true
type: string
release_run_id:
description: Original Release run with successful exact-source qualification and npm publication
required: true
type: string
concurrency:
group: release-recovery-${{ inputs.version }}
cancel-in-progress: false
jobs:
verify:
name: Verify existing published package
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: read
outputs:
source_sha: ${{ steps.release.outputs.source_sha }}
version: ${{ steps.release.outputs.version }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version-file: .nvmrc
package-manager-cache: false
- run: npm ci
- name: Validate original release authority
id: release
env:
GH_TOKEN: ${{ github.token }}
RECOVERY_VERSION: ${{ inputs.version }}
RECOVERY_RUN_ID: ${{ inputs.release_run_id }}
run: node scripts/ci/release-recovery.mjs resolve
- name: Check out released metadata
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
ref: ${{ steps.release.outputs.source_sha }}
path: .cache/release-source
persist-credentials: false
- name: Download original qualified package
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: verified-release-package
run-id: ${{ steps.release.outputs.run_id }}
github-token: ${{ github.token }}
path: .cache/verified-package
- name: Restore original verifier runtime
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rea-test-runtime
run-id: ${{ steps.release.outputs.run_id }}
github-token: ${{ github.token }}
path: .
- name: Match published npm bytes to the qualified package
env:
RECOVERY_VERSION: ${{ steps.release.outputs.version }}
RECOVERY_RUN_ID: ${{ steps.release.outputs.run_id }}
RECOVERY_RUN_ATTEMPT: ${{ steps.release.outputs.run_attempt }}
RECOVERY_SOURCE_SHA: ${{ steps.release.outputs.source_sha }}
RECOVERY_SOURCE_DIRECTORY: .cache/release-source
RECOVERY_ARTIFACT_DIRECTORY: .cache/verified-package
run: node scripts/ci/release-recovery.mjs verify
- name: Verify published CLI, MCP and update
env:
RECOVERY_VERSION: ${{ steps.release.outputs.version }}
run: node scripts/verify-published-package.mjs "$RECOVERY_VERSION"
- name: Validate released registry metadata
working-directory: .cache/release-source
run: npm run mcp-registry:check
publish-mcp:
name: Finish MCP Registry publication
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Check out verified released metadata
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
ref: ${{ needs.verify.outputs.source_sha }}
path: .cache/release-source
persist-credentials: false
- uses: ./.github/actions/publish-mcp
with:
directory: .cache/release-source
+1 -22
View File
@@ -371,8 +371,6 @@ jobs:
published_version="$(npm view --prefer-online "rea-agents@${version}" version --json 2>/dev/null || true)"
if [ "${published_version}" = "\"${version}\"" ]; then
echo "rea-agents@${version} is visible on npm after attempt ${attempt}/${max_attempts}; verifying the published CLI."
npm exec --yes --package="rea-agents@${version}" -- rea --help
npm exec --yes --package="rea-agents@${version}" -- rea --llms
node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"
exit 0
else
@@ -406,23 +404,4 @@ jobs:
- name: Verify package and registry metadata versions
run: npm run mcp-registry:check
- name: Install MCP Registry publisher
env:
MCP_PUBLISHER_VERSION: v1.8.0
MCP_PUBLISHER_SHA256_LINUX_AMD64: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
run: |
archive="mcp-publisher_linux_amd64.tar.gz"
curl -fsSLo "${archive}" \
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/${archive}"
echo "${MCP_PUBLISHER_SHA256_LINUX_AMD64} ${archive}" | sha256sum --check --strict
tar xzf "${archive}" mcp-publisher
chmod +x mcp-publisher
- name: Validate MCP Registry metadata
run: ./mcp-publisher validate
- name: Authenticate to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Publish MCP Registry metadata
run: ./mcp-publisher publish
- uses: ./.github/actions/publish-mcp