From 33c45c6daeabb490de2301f3cd1bf7f3e6f7a8d5 Mon Sep 17 00:00:00 2001 From: Zhiyu You Date: Mon, 29 Jun 2026 11:20:23 +0800 Subject: [PATCH 1/2] fix(scaffold): v4 even-minor preview publishes 6.11. --- .github/scripts/v4/publish-v4-channel.sh | 27 +++---------- .github/scripts/v4/sync-v4-template-config.js | 25 ++++++------ .../src/v4/distribution/templateConfig.ts | 40 ++++++++++++++----- .../v4/distribution/templateConfig.test.ts | 17 +++++++- templates/scripts/generateV4Zip.js | 20 +++++----- 5 files changed, 74 insertions(+), 55 deletions(-) diff --git a/.github/scripts/v4/publish-v4-channel.sh b/.github/scripts/v4/publish-v4-channel.sh index 95ef4e22ca..d8f5582705 100644 --- a/.github/scripts/v4/publish-v4-channel.sh +++ b/.github/scripts/v4/publish-v4-channel.sh @@ -32,29 +32,14 @@ SHA="${4:?Need the commit sha to anchor the release.}" METADATA_ZIP="${5:-}" RAW_VERSION="${TEMPLATE_TAG#templates@}" -RAW_MINOR="$(echo "$RAW_VERSION" | cut -d. -f2)" # Clean-version invariant. The v4 channel only ever holds clean versions that a -# `~major.minor` range can resolve, keyed on the minted version's odd/even minor: -# - ODD minor = prerelease line (6.11.x): published under a clean, date-stamped -# version (6.11.) computed by computeV4PublishVersion and recorded as -# templates-config.json v4.localVersion by the preceding "sync v4 template -# config" step. We reuse that single source of truth here. -# - EVEN minor = stable line (6.10.x): already clean, published as-is. -# An EVEN-minor version that still carries a prerelease suffix means a preview -# lane was minted on a stable branch (the exact 6.10.3-beta. misfire) — -# refuse it, since stripping the suffix would collide with the real stable 6.10.3. -case "$RAW_VERSION" in - *-*) - if [ $((RAW_MINOR % 2)) -eq 0 ]; then - echo "Refusing v4 channel publish: '$RAW_VERSION' is a prerelease-suffixed even-minor (stable) version — a preview lane minted on a stable branch." >&2 - exit 1 - fi - ;; -esac - -# Reuse the clean publish version the sync step already computed (single source -# of truth), falling back to the raw version for an even-minor stable release. +# `~major.minor` range can resolve. The clean publish version was computed by +# computeV4PublishVersion and recorded as templates-config.json v4.localVersion +# by the preceding "sync v4 template config" step; we reuse that single source +# of truth. A preview (-beta. suffix) maps to the odd-minor line +# (6.11.) — bumped from an even-minor stable base when needed, mirroring +# the VSIX vsc-version.sh mints; a stable version is already clean. CONFIG_FILE="packages/fx-core/src/common/templates-config.json" VERSION="$(node -p "(require('./$CONFIG_FILE').v4 || {}).localVersion || '$RAW_VERSION'")" diff --git a/.github/scripts/v4/sync-v4-template-config.js b/.github/scripts/v4/sync-v4-template-config.js index 7872dc6c29..fd1cc84f5a 100644 --- a/.github/scripts/v4/sync-v4-template-config.js +++ b/.github/scripts/v4/sync-v4-template-config.js @@ -48,21 +48,21 @@ function computeBundled(goproduct) { return !goproduct; } -// Clean, suffix-free version published to the v4 channel: odd minor (prerelease) -// stamps the build date into the patch (6.11., read from the -beta. -// preid); even minor (stable) uses major.minor.patch as-is. +// Clean, suffix-free version published to the v4 channel: mirrors the VSIX +// version vsc-version.sh mints. A preview (-beta. suffix) targets the +// odd-minor line, date-stamped patch (6.11.) — an even-minor stable base +// bumps to the next odd minor, like the VSIX; stable (no date) is as-is. function computeV4PublishVersion(version) { const parsed = semver.parse(version); if (parsed === null) { throw new Error(`Cannot compute v4 publish version: "${version}" is not valid SemVer.`); } - if (parsed.minor % 2 === 1) { - const dateStamp = parsed.prerelease.find( - (segment) => typeof segment === "number" && segment >= 1000000000 - ); - if (dateStamp !== undefined) { - return `${parsed.major}.${parsed.minor}.${dateStamp}`; - } + const dateStamp = parsed.prerelease.find( + (segment) => typeof segment === "number" && segment >= 1000000000 + ); + if (dateStamp !== undefined) { + const minor = parsed.minor % 2 === 0 ? parsed.minor + 1 : parsed.minor; + return `${parsed.major}.${minor}.${dateStamp}`; } return `${parsed.major}.${parsed.minor}.${parsed.patch}`; } @@ -79,10 +79,11 @@ function computeRange(version, previousRange) { } function computeV4TemplateConfig(input) { + const localVersion = computeV4PublishVersion(input.version); return { - range: computeRange(input.version, input.previousRange), + range: computeRange(localVersion, input.previousRange), bundled: computeBundled(input.goproduct), - localVersion: computeV4PublishVersion(input.version), + localVersion, }; } diff --git a/packages/fx-core/src/v4/distribution/templateConfig.ts b/packages/fx-core/src/v4/distribution/templateConfig.ts index 2883495be2..87a7966877 100644 --- a/packages/fx-core/src/v4/distribution/templateConfig.ts +++ b/packages/fx-core/src/v4/distribution/templateConfig.ts @@ -28,19 +28,35 @@ export function computeBundled(goproduct: boolean): boolean { return !goproduct; } -/** Compute the clean, suffix-free version published to the v4 channel. */ +/** + * The clean, suffix-free version published to the v4 channel for a minted + * version. It mirrors the VSIX version `vsc-version.sh` mints so the template + * channel and the shipped extension always share one version: + * + * - PREVIEW (any `-beta.`-suffixed version): the odd-minor prerelease + * line, date-stamped patch (`6.11.`). An even-minor base (lerna + * `prerelease` keeps the stable `6.10.x` minor) is bumped to the next odd + * minor, exactly as `vsc-version.sh` bumps the VSIX from `6.10.x` to + * `6.11.`; an already-odd base keeps its minor. The date is read from + * the `-beta.` preid, so every preview build gets a unique version + * that satisfies `~major.minor`. + * - STABLE (no date-stamped suffix): `major.minor.patch` as-is; the stable + * lane already mints a clean version. + * + * This is the v4-channel counterpart of the clean `templates@..` + * pattern the v3 channel uses for prereleases. + */ export function computeV4PublishVersion(version: string): string { const parsed = semver.parse(version); if (parsed === null) { throw new Error(`Cannot compute v4 publish version: "${version}" is not valid SemVer.`); } - if (parsed.minor % 2 === 1) { - const dateStamp = parsed.prerelease.find( - (segment): segment is number => typeof segment === "number" && segment >= 1_000_000_000 - ); - if (dateStamp !== undefined) { - return `${parsed.major}.${parsed.minor}.${dateStamp}`; - } + const dateStamp = parsed.prerelease.find( + (segment): segment is number => typeof segment === "number" && segment >= 1_000_000_000 + ); + if (dateStamp !== undefined) { + const minor = parsed.minor % 2 === 0 ? parsed.minor + 1 : parsed.minor; + return `${parsed.major}.${minor}.${dateStamp}`; } return `${parsed.major}.${parsed.minor}.${parsed.patch}`; } @@ -59,9 +75,13 @@ export function computeRange(version: string, previousRange: string): string { /** Compute the full v4 distribution config block for `templates-config.json`. */ export function computeV4TemplateConfig(input: V4TemplateConfigInput): V4TemplateConfig { + const localVersion = computeV4PublishVersion(input.version); return { - range: computeRange(input.version, input.previousRange), + // Range follows the published version, not the raw minted minor: a preview + // minted on a stable `6.10.x` base publishes as `6.11.`, so the range + // must widen to `~6.11` to resolve it. + range: computeRange(localVersion, input.previousRange), bundled: computeBundled(input.goproduct), - localVersion: computeV4PublishVersion(input.version), + localVersion, }; } diff --git a/packages/fx-core/tests/v4/distribution/templateConfig.test.ts b/packages/fx-core/tests/v4/distribution/templateConfig.test.ts index 1c7487f81e..72a67a734c 100644 --- a/packages/fx-core/tests/v4/distribution/templateConfig.test.ts +++ b/packages/fx-core/tests/v4/distribution/templateConfig.test.ts @@ -32,8 +32,8 @@ describe("templateConfig (v4 build-time)", () => { assert.strictEqual(computeV4PublishVersion("6.10.3"), "6.10.3"); }); - it("even-minor suffixed (preview minted on stable branch) → stripped (publish-time guard refuses it separately)", () => { - assert.strictEqual(computeV4PublishVersion("6.10.3-beta.2026061609.0"), "6.10.3"); + it("even-minor preview (lerna keeps the stable minor) → bumps to the next odd minor, date-stamped, like the VSIX", () => { + assert.strictEqual(computeV4PublishVersion("6.10.4-beta.2026062608.0"), "6.11.2026062608"); }); it("throws on a non-SemVer version (no silent fallback)", () => { @@ -104,6 +104,19 @@ describe("templateConfig (v4 build-time)", () => { }); }); + it("even-minor preview shipping (goproduct=true) → bumps to ~6.11 and 6.11., matching the VSIX", () => { + const config = computeV4TemplateConfig({ + version: "6.10.4-beta.2026062608.0", + goproduct: true, + previousRange: "~6.10", + }); + assert.deepEqual(config, { + range: "~6.11", + bundled: false, + localVersion: "6.11.2026062608", + }); + }); + it("patch within the current range keeps the range stable (reproducibility)", () => { const config = computeV4TemplateConfig({ version: "6.10.2", diff --git a/templates/scripts/generateV4Zip.js b/templates/scripts/generateV4Zip.js index bc6d2757ba..65bda68bf3 100644 --- a/templates/scripts/generateV4Zip.js +++ b/templates/scripts/generateV4Zip.js @@ -39,21 +39,21 @@ const semver = require("semver"); // Mirror of packages/fx-core/src/v4/distribution/templateConfig.ts // `computeV4PublishVersion` (canonical, unit-tested). Kept inline so the -// templates build needs no fx-core build output. Odd minor (prerelease) stamps -// the build date into the patch (6.11., read from the -beta. preid); -// even minor (stable) uses major.minor.patch as-is. Keep in sync with canonical. +// templates build needs no fx-core build output. A preview (-beta. +// suffix) targets the odd-minor line, date-stamped patch (6.11.) — an +// even-minor stable base bumps to the next odd minor, like the VSIX; stable +// (no date) is as-is. Keep in sync with canonical. function computeV4PublishVersion(rawVersion) { const parsed = semver.parse(rawVersion); if (parsed === null) { throw new Error(`Cannot compute v4 publish version: "${rawVersion}" is not valid SemVer.`); } - if (parsed.minor % 2 === 1) { - const dateStamp = parsed.prerelease.find( - (segment) => typeof segment === "number" && segment >= 1000000000 - ); - if (dateStamp !== undefined) { - return `${parsed.major}.${parsed.minor}.${dateStamp}`; - } + const dateStamp = parsed.prerelease.find( + (segment) => typeof segment === "number" && segment >= 1000000000 + ); + if (dateStamp !== undefined) { + const minor = parsed.minor % 2 === 0 ? parsed.minor + 1 : parsed.minor; + return `${parsed.major}.${minor}.${dateStamp}`; } return `${parsed.major}.${parsed.minor}.${parsed.patch}`; } From 3d315c97ceccf05be1ff32c70ae8c54ce7263e17 Mon Sep 17 00:00:00 2001 From: Zhiyu You Date: Tue, 30 Jun 2026 10:10:58 +0800 Subject: [PATCH 2/2] fix(scaffold): harden v4 preview version publishing --- .github/scripts/v4/publish-v4-channel.sh | 4 ++++ .github/scripts/v4/sync-v4-template-config.js | 7 ++++--- packages/fx-core/src/v4/distribution/templateConfig.ts | 9 ++++++--- .../fx-core/tests/v4/distribution/templateConfig.test.ts | 4 ++++ templates/scripts/generateV4Zip.js | 7 ++++--- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/scripts/v4/publish-v4-channel.sh b/.github/scripts/v4/publish-v4-channel.sh index d8f5582705..a5a6efe23d 100644 --- a/.github/scripts/v4/publish-v4-channel.sh +++ b/.github/scripts/v4/publish-v4-channel.sh @@ -42,6 +42,10 @@ RAW_VERSION="${TEMPLATE_TAG#templates@}" # the VSIX vsc-version.sh mints; a stable version is already clean. CONFIG_FILE="packages/fx-core/src/common/templates-config.json" VERSION="$(node -p "(require('./$CONFIG_FILE').v4 || {}).localVersion || '$RAW_VERSION'")" +if [[ "$VERSION" == *-* || "$VERSION" == *+* ]]; then + echo "v4 publish version must be clean, got '$VERSION'." >&2 + exit 1 +fi TAG="templates-v4@$VERSION" NDJSON="$TMP/template-v4-tags.ndjson" diff --git a/.github/scripts/v4/sync-v4-template-config.js b/.github/scripts/v4/sync-v4-template-config.js index fd1cc84f5a..10588ae972 100644 --- a/.github/scripts/v4/sync-v4-template-config.js +++ b/.github/scripts/v4/sync-v4-template-config.js @@ -57,9 +57,10 @@ function computeV4PublishVersion(version) { if (parsed === null) { throw new Error(`Cannot compute v4 publish version: "${version}" is not valid SemVer.`); } - const dateStamp = parsed.prerelease.find( - (segment) => typeof segment === "number" && segment >= 1000000000 - ); + const dateStamp = + parsed.prerelease[0] === "beta" + ? parsed.prerelease.find((segment) => typeof segment === "number" && segment >= 1000000000) + : undefined; if (dateStamp !== undefined) { const minor = parsed.minor % 2 === 0 ? parsed.minor + 1 : parsed.minor; return `${parsed.major}.${minor}.${dateStamp}`; diff --git a/packages/fx-core/src/v4/distribution/templateConfig.ts b/packages/fx-core/src/v4/distribution/templateConfig.ts index 87a7966877..0a7c426718 100644 --- a/packages/fx-core/src/v4/distribution/templateConfig.ts +++ b/packages/fx-core/src/v4/distribution/templateConfig.ts @@ -51,9 +51,12 @@ export function computeV4PublishVersion(version: string): string { if (parsed === null) { throw new Error(`Cannot compute v4 publish version: "${version}" is not valid SemVer.`); } - const dateStamp = parsed.prerelease.find( - (segment): segment is number => typeof segment === "number" && segment >= 1_000_000_000 - ); + const dateStamp = + parsed.prerelease[0] === "beta" + ? parsed.prerelease.find( + (segment): segment is number => typeof segment === "number" && segment >= 1_000_000_000 + ) + : undefined; if (dateStamp !== undefined) { const minor = parsed.minor % 2 === 0 ? parsed.minor + 1 : parsed.minor; return `${parsed.major}.${minor}.${dateStamp}`; diff --git a/packages/fx-core/tests/v4/distribution/templateConfig.test.ts b/packages/fx-core/tests/v4/distribution/templateConfig.test.ts index 72a67a734c..1e51eb5928 100644 --- a/packages/fx-core/tests/v4/distribution/templateConfig.test.ts +++ b/packages/fx-core/tests/v4/distribution/templateConfig.test.ts @@ -36,6 +36,10 @@ describe("templateConfig (v4 build-time)", () => { assert.strictEqual(computeV4PublishVersion("6.10.4-beta.2026062608.0"), "6.11.2026062608"); }); + it("non-beta prerelease with a large numeric segment → major.minor.patch", () => { + assert.strictEqual(computeV4PublishVersion("6.10.4-rc.2026062608.0"), "6.10.4"); + }); + it("throws on a non-SemVer version (no silent fallback)", () => { expect(() => computeV4PublishVersion("not-semver")).toThrow(/not valid SemVer/); }); diff --git a/templates/scripts/generateV4Zip.js b/templates/scripts/generateV4Zip.js index 65bda68bf3..14accfebbb 100644 --- a/templates/scripts/generateV4Zip.js +++ b/templates/scripts/generateV4Zip.js @@ -48,9 +48,10 @@ function computeV4PublishVersion(rawVersion) { if (parsed === null) { throw new Error(`Cannot compute v4 publish version: "${rawVersion}" is not valid SemVer.`); } - const dateStamp = parsed.prerelease.find( - (segment) => typeof segment === "number" && segment >= 1000000000 - ); + const dateStamp = + parsed.prerelease[0] === "beta" + ? parsed.prerelease.find((segment) => typeof segment === "number" && segment >= 1000000000) + : undefined; if (dateStamp !== undefined) { const minor = parsed.minor % 2 === 0 ? parsed.minor + 1 : parsed.minor; return `${parsed.major}.${minor}.${dateStamp}`;