diff --git a/scripts/check-connect-release.mjs b/scripts/check-connect-release.mjs index 2fee8d7..4cbdc98 100644 --- a/scripts/check-connect-release.mjs +++ b/scripts/check-connect-release.mjs @@ -10,7 +10,6 @@ const release = JSON.parse(await readFile(new URL("../src/data/connect-release.j const sources = JSON.parse(await readFile(new URL("../site-sources.json", import.meta.url))); assert.equal(release.tag, `v${release.version}`, "release tag must match version"); -assert.equal(release.sdkVersion, release.version, "public SDK and Connect release versions must match"); assert.equal(sources.connect.ref, release.sourceRevision, "published docs must use the released Connect source"); assert.match(release.releaseUrl, new RegExp(`/releases/tag/${release.tag}$`)); assert.match(release.channelManifestUrl, new RegExp(`/releases/download/${release.tag}/mdbase-connect-channel-v1\\.json$`)); @@ -81,10 +80,14 @@ const npmAttestationsResponse = await fetch(`https://registry.npmjs.org/-/npm/v1 signal: AbortSignal.timeout(30000) }); assert.equal(npmAttestationsResponse.status, 200, "npm provenance request failed"); +const sdkTag = `v${release.sdkVersion}`; +const sdkRevision = sdkTag === release.tag ? release.sourceRevision : execFileSync("gh", [ + "api", `repos/mdbase-dev/mdbase-connect/commits/${sdkTag}`, "--jq", ".sha" +], { encoding: "utf8" }).trim(); validateNpmProvenance({ version: release.sdkVersion, - releaseTag: release.tag, - sourceRevision: release.sourceRevision, + releaseTag: sdkTag, + sourceRevision: sdkRevision, metadata: sdk, attestations: await npmAttestationsResponse.json() }); diff --git a/scripts/connect-release-record.mjs b/scripts/connect-release-record.mjs index a54fc9f..1294aeb 100644 --- a/scripts/connect-release-record.mjs +++ b/scripts/connect-release-record.mjs @@ -13,7 +13,7 @@ export function buildConnectReleaseRecord({ tag, sourceRevision, release, channe assert.equal(channel.tag, tag, "channel manifest tag differs from requested tag"); assert.equal(channel.channel, version.includes("-") ? "beta" : "stable", "channel differs from semantic version"); assert.equal(channel.release_url, `https://github.com/${CONNECT_REPOSITORY}/releases/tag/${tag}`, "channel release URL differs from tag"); - assert.equal(npmVersion, version, "published SDK version differs from client release"); + assert.ok(compareSemanticVersions(npmVersion, version) <= 0, "published SDK version is newer than client release"); assert.match(sourceRevision, /^[0-9a-f]{40}$/, "release source must be a full commit SHA"); assert.ok(Number.isFinite(Date.parse(release.published_at)), "GitHub release has no publication time"); @@ -59,7 +59,7 @@ export function buildConnectReleaseRecord({ tag, sourceRevision, release, channe schemaVersion: 1, channel: channel.channel, version, - sdkVersion: version, + sdkVersion: npmVersion, tag, sourceRevision, publishedAt: release.published_at, diff --git a/scripts/connect-release-record.test.mjs b/scripts/connect-release-record.test.mjs index 600923e..b1d3b9c 100644 --- a/scripts/connect-release-record.test.mjs +++ b/scripts/connect-release-record.test.mjs @@ -54,6 +54,7 @@ function build(overrides = {}) { test("derives the website record from verified release evidence", () => { const record = build(); assert.equal(record.version, version); + assert.equal(record.sdkVersion, version); assert.equal(record.channel, "beta"); assert.equal(record.desktop.length, 5); assert.equal(record.cli.length, 4); @@ -72,8 +73,13 @@ test("labels macOS downloads from their notarization marker", () => { assert.equal(record.desktop[1].trust, "Beta preview ยท notarized"); }); +test("carries forward a previously verified older SDK without changing client evidence", () => { + const record = build({ npmVersion: "0.1.0-beta.83" }); + assert.deepEqual(record, { ...build(), sdkVersion: "0.1.0-beta.83" }); +}); + test("rejects inconsistent release evidence and duplicate artifacts", () => { - assert.throws(() => build({ npmVersion: "0.1.0-beta.83" }), /SDK version/); + assert.throws(() => build({ npmVersion: "0.1.0-beta.85" }), /SDK version/); assert.throws(() => build({ channel: { ...channel, tag: "v0.1.0-beta.83" } }), /manifest tag/); const wrongDigest = structuredClone(channel); wrongDigest.targets["linux-x64"].artifacts[0].sha256 = "f".repeat(64); diff --git a/scripts/update-connect-release.mjs b/scripts/update-connect-release.mjs index c263cb3..be0989e 100644 --- a/scripts/update-connect-release.mjs +++ b/scripts/update-connect-release.mjs @@ -34,9 +34,9 @@ try { ], { stdio: ["ignore", "ignore", "inherit"] }); const channel = JSON.parse(await readFile(channelPath, "utf8")); - const npmVersion = await waitForSdk(versionFromTag(tag), tag, sourceRevision); - const next = buildConnectReleaseRecord({ tag, sourceRevision, release, channel, npmVersion }); const current = JSON.parse(await readFile(releasePath, "utf8")); + const npmVersion = await waitForSdk(versionFromTag(tag), tag, sourceRevision) ?? current.sdkVersion; + const next = buildConnectReleaseRecord({ tag, sourceRevision, release, channel, npmVersion }); const disposition = releaseUpdateDisposition(current, next); if (disposition === "stale") { @@ -136,7 +136,10 @@ async function waitForSdk(version, releaseTag, sourceRevision) { if (attempt === attempts) throw error; console.warn(`npm provenance is not ready for ${version}; retrying (${attempt}/${attempts}).`); } - } else if (response.status !== 404 || attempt === attempts) { + } else if (response.status === 404) { + console.warn(`npm has no @mdbase-dev/connect@${version}; retaining the previously verified SDK version.`); + return null; + } else { throw new Error(`npm returned HTTP ${response.status} for @mdbase-dev/connect@${version}`); } await new Promise((resolveDelay) => setTimeout(resolveDelay, delayMs));