Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 6 additions & 21 deletions .github/scripts/v4/publish-v4-channel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.<date>) 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.<date> 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.<date> suffix) maps to the odd-minor line
# (6.11.<date>) — 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'")"

Comment thread
Alive-Fish marked this conversation as resolved.
Expand Down
25 changes: 13 additions & 12 deletions .github/scripts/v4/sync-v4-template-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<date>, read from the -beta.<date>
// 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.<date> suffix) targets the
// odd-minor line, date-stamped patch (6.11.<date>) — 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
);
Comment thread
Alive-Fish marked this conversation as resolved.
Outdated
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}`;
}
Expand All @@ -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,
};
}

Expand Down
40 changes: 30 additions & 10 deletions packages/fx-core/src/v4/distribution/templateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<date>`-suffixed version): the odd-minor prerelease
* line, date-stamped patch (`6.11.<YYYYMMDDHH>`). 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.<date>`; an already-odd base keeps its minor. The date is read from
* the `-beta.<date>` 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@<major>.<minor>.<date>`
* 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
);
Comment thread
Alive-Fish marked this conversation as resolved.
Outdated
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}`;
}
Expand All @@ -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.<date>`, 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,
};
}
17 changes: 15 additions & 2 deletions packages/fx-core/tests/v4/distribution/templateConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)", () => {
Expand Down Expand Up @@ -104,6 +104,19 @@ describe("templateConfig (v4 build-time)", () => {
});
});

it("even-minor preview shipping (goproduct=true) → bumps to ~6.11 and 6.11.<date>, 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",
Expand Down
20 changes: 10 additions & 10 deletions templates/scripts/generateV4Zip.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<date>, read from the -beta.<date> 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.<date>
// suffix) targets the odd-minor line, date-stamped patch (6.11.<date>) — 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
);
Comment thread
Alive-Fish marked this conversation as resolved.
Outdated
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}`;
}
Expand Down
Loading