From cc329cb605a7a9f8aac76d68ecb6a46679f47d52 Mon Sep 17 00:00:00 2001 From: Zhiyu You Date: Mon, 29 Jun 2026 11:20:23 +0800 Subject: [PATCH 1/5] 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 ++++--- .github/workflows/test-v4-channel.yml | 73 +++++++++++++++++++ .../src/v4/distribution/templateConfig.ts | 40 +++++++--- .../v4/distribution/templateConfig.test.ts | 17 ++++- templates/scripts/generateV4Zip.js | 20 ++--- 6 files changed, 147 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/test-v4-channel.yml 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/.github/workflows/test-v4-channel.yml b/.github/workflows/test-v4-channel.yml new file mode 100644 index 0000000000..340cc63307 --- /dev/null +++ b/.github/workflows/test-v4-channel.yml @@ -0,0 +1,73 @@ +name: Test v4 template channel +run-name: test-v4-${{ inputs.rawVersion }} + +# Manual: build templates from a branch/sha and publish ONE v4 package to the +# real channel (templates-v4@ + template-v4-tag-list) so dev can test v4. +# Safe because v4 is not enabled in prod yet. No version mint, no npm/vsix/v3. +on: + workflow_dispatch: + inputs: + rawVersion: + description: "Raw templates version to mint from (even-minor preview bumps to 6.11.)." + required: true + default: "6.11.0-beta.2026062908.0" + ref: + description: "Branch/sha to build template content from." + required: true + default: "" + production: + description: "true = online channel config; false = bundled floor." + required: true + default: "true" + +permissions: + contents: write + +jobs: + test-publish: + runs-on: ubuntu-latest + steps: + - id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.APP_GITHUB_APP_ID }} + private-key: ${{ secrets.APP_GITHUB_APP_PRIVATE_KEY }} + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ steps.generate-token.outputs.token }} + ref: ${{ github.event.inputs.ref }} + + - uses: actions/setup-node@v3 + with: + node-version: 22 + - uses: pnpm/action-setup@v4 + + - name: Setup project + run: npm run setup + + - name: Pin templates version + run: | + jq --arg v "${{ github.event.inputs.rawVersion }}" '.version=$v' \ + templates/package.json > templates/tmp.json + mv templates/tmp.json templates/package.json + + - name: Build templates (v4 zip + metadata) + run: cd templates && npm run build + + - name: Sync v4 template config + env: + PRODUCTION: ${{ github.event.inputs.production }} + run: node .github/scripts/v4/sync-v4-template-config.js + + - name: Publish to v4 channel + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + run: | + bash .github/scripts/v4/publish-v4-channel.sh \ + "templates@${{ github.event.inputs.rawVersion }}" \ + "${{ github.workspace }}/templates/build/v4/templates.zip" \ + "${{ runner.temp }}" \ + "${{ github.sha }}" \ + "${{ github.workspace }}/templates/build/metadata.zip" 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 bcb734593170bfb63a88eb0b6d217caffa8ce4c9 Mon Sep 17 00:00:00 2001 From: Zhiyu You Date: Mon, 29 Jun 2026 13:39:22 +0800 Subject: [PATCH 2/5] test: enable TEAMSFX_V4_ENABLED by default to exercise v4 e2e/ui cases --- packages/fx-core/src/common/featureFlags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fx-core/src/common/featureFlags.ts b/packages/fx-core/src/common/featureFlags.ts index 850a0db740..262b3fcd10 100644 --- a/packages/fx-core/src/common/featureFlags.ts +++ b/packages/fx-core/src/common/featureFlags.ts @@ -121,7 +121,7 @@ export class FeatureFlags { }; static readonly V4Enabled = { name: FeatureFlagName.V4Enabled, - defaultValue: "false", + defaultValue: "true", }; static readonly MCPForDADT = { name: FeatureFlagName.MCPForDADT, From 3c2c6fc8ff6c118b8492e6b890fee6d15bff14fd Mon Sep 17 00:00:00 2001 From: Zhiyu You Date: Mon, 29 Jun 2026 14:01:21 +0800 Subject: [PATCH 3/5] test(e2e): cap generated app name at 30 chars, keep fxE2E prefix --- packages/tests/src/e2e/commonUtils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/tests/src/e2e/commonUtils.ts b/packages/tests/src/e2e/commonUtils.ts index d633f3ffd0..41584cb37b 100644 --- a/packages/tests/src/e2e/commonUtils.ts +++ b/packages/tests/src/e2e/commonUtils.ts @@ -63,7 +63,14 @@ export function getAppNamePrefix(): string { } export function getUniqueAppName(): string { - return getAppNamePrefix() + Date.now().toString() + uuidv4().slice(0, 2); + // Cap at the 30-char app-name limit; the `fxE2E` prefix is preserved + // (only the trailing timestamp/uuid is trimmed) so prefix-based cleanup still + // matches. Date.now() growing to 14 digits could push the raw name to 31. + return ( + getAppNamePrefix() + + Date.now().toString() + + uuidv4().slice(0, 2) + ).slice(0, 30); } export function convertToAlphanumericOnly(appName: string): string { From b316dd676772fa2ad895deb86d9dccfeea2a4210 Mon Sep 17 00:00:00 2001 From: Zhiyu You Date: Mon, 29 Jun 2026 14:35:05 +0800 Subject: [PATCH 4/5] fix(scaffold): disable v4 minEngineVersion engine gate --- .../v4/validation/validateTemplatePackage.ts | 38 +++---------------- .../validateTemplatePackage.test.ts | 14 ++----- 2 files changed, 9 insertions(+), 43 deletions(-) diff --git a/packages/fx-core/src/v4/validation/validateTemplatePackage.ts b/packages/fx-core/src/v4/validation/validateTemplatePackage.ts index 7d223b09f2..dce73834fe 100644 --- a/packages/fx-core/src/v4/validation/validateTemplatePackage.ts +++ b/packages/fx-core/src/v4/validation/validateTemplatePackage.ts @@ -11,7 +11,7 @@ const SOURCE = "Scaffold"; /** Package namespace. */ export type PackageKind = "create" | "modify"; -/** Validation mode; only `load` compares this engine against `minEngineVersion`. */ +/** Validation mode; retained for signature parity. The engine gate is disabled. */ export type ValidateMode = "build" | "load"; /** One content file's path plus extracted `{{token}}` names. */ @@ -112,33 +112,11 @@ function v4RouteIds(selectorData: unknown): string[] { return ids; } -/** Compare `major.minor.patch` numerically: <0 / 0 / >0. */ -function compareSemver(a: string, b: string): number { - const pa = parseSemver(a); - const pb = parseSemver(b); - for (let i = 0; i < 3; i++) { - if (pa[i] !== pb[i]) { - return pa[i] < pb[i] ? -1 : 1; - } - } - return 0; -} - -function parseSemver(v: string): number[] { - const parts = v.split("."); - const nums: number[] = []; - for (let i = 0; i < 3; i++) { - const n = Number.parseInt(parts[i] ?? "0", 10); - nums.push(Number.isNaN(n) ? 0 : n); - } - return nums; -} - /** Validate one `/` package before any content is rendered. */ export function validateTemplatePackage( kind: PackageKind, id: string, - mode: ValidateMode, + _mode: ValidateMode, port: TemplatePackagePort ): Result { const pkg = `${kind}/${id}`; @@ -282,14 +260,8 @@ export function validateTemplatePackage( ); } - if (mode === "load" && compareSemver(port.engineVersion(), minEngineVersion) < 0) { - return err( - userError( - VALIDATE_ENGINE_TOO_OLD, - `${pkg}: requires engine ${minEngineVersion}, but this engine is ${port.engineVersion()}; upgrade the engine (no silent fallback)` - ) - ); - } - + // The engine-version reverse gate is disabled: minEngineVersion is still + // mandatory (recorded for telemetry) but never blocks load, so v4 packages + // run regardless of the engine SemVer. return ok({ descriptor, minEngineVersion, contentFiles }); } diff --git a/packages/fx-core/tests/v4/validation/validateTemplatePackage.test.ts b/packages/fx-core/tests/v4/validation/validateTemplatePackage.test.ts index 1b5535277c..1877f915f7 100644 --- a/packages/fx-core/tests/v4/validation/validateTemplatePackage.test.ts +++ b/packages/fx-core/tests/v4/validation/validateTemplatePackage.test.ts @@ -7,7 +7,6 @@ import { ContentFile, TemplatePackagePort, VALIDATE_DANGLING_ROUTE, - VALIDATE_ENGINE_TOO_OLD, VALIDATE_KIND_OVERLAP, VALIDATE_MIN_ENGINE_MISSING, VALIDATE_PLACEHOLDER_DRIFT, @@ -346,7 +345,7 @@ describe("v4/validation/validateTemplatePackage", () => { assert.isTrue(res.isOk()); }); - it("AC-18: load, engine 6.11.0 < minEngineVersion 6.11.3 -> UserError (upgrade engine)", () => { + it("AC-18: engine gate disabled - engine 6.11.0 < minEngineVersion 6.11.3 still ok", () => { const parts = validParts(); parts.engineVersion = "6.11.0"; parts.descriptor = { @@ -358,14 +357,10 @@ describe("v4/validation/validateTemplatePackage", () => { replaceMap: [{ var: "MCPNamespace", const: "ns" }], }; const res = validateTemplatePackage("create", "mcp-server", "load", makePort(parts)); - assert.isTrue(res.isErr()); - const e = res._unsafeUnwrapErr(); - assert.instanceOf(e, UserError); - assert.equal(e.name, VALIDATE_ENGINE_TOO_OLD); - assert.include(e.message, "6.11.3"); + assert.isTrue(res.isOk()); }); - it("AC-19: per-package gate separates siblings in one artifact (mcp-server ok, foo too-old)", () => { + it("AC-19: engine gate disabled - siblings both ok regardless of minEngineVersion", () => { const okParts = validParts(); okParts.engineVersion = "6.11.0"; @@ -389,8 +384,7 @@ describe("v4/validation/validateTemplatePackage", () => { const resOk = validateTemplatePackage("create", "mcp-server", "load", makePort(okParts)); const resFoo = validateTemplatePackage("create", "foo", "load", makePort(foo)); assert.isTrue(resOk.isOk()); - assert.isTrue(resFoo.isErr()); - assert.equal(resFoo._unsafeUnwrapErr().name, VALIDATE_ENGINE_TOO_OLD); + assert.isTrue(resFoo.isOk()); }); it("AC-20: a malformed package fails identically under build and load", () => { From 0eac43bee0362a7a3cd2c8e4646376c4480d5f1a Mon Sep 17 00:00:00 2001 From: Zhiyu You Date: Mon, 29 Jun 2026 16:20:15 +0800 Subject: [PATCH 5/5] ci(e2e): use branch cli for pull request tests --- .github/workflows/e2e-test-same-tenant.yml | 8 ++++---- .github/workflows/e2e-test.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e-test-same-tenant.yml b/.github/workflows/e2e-test-same-tenant.yml index 7ce4060e67..8b21450d67 100644 --- a/.github/workflows/e2e-test-same-tenant.yml +++ b/.github/workflows/e2e-test-same-tenant.yml @@ -178,13 +178,13 @@ jobs: run: | npm install -g @microsoft/m365agentstoolkit-cli@${{ github.event.inputs.cli-version }} - - name: install cli for schedule - if: ${{ github.event_name == 'schedule' || github.event_name == 'pull_request' || (github.event.ref == 'refs/heads/dev' && github.event_name == 'workflow_dispatch' && github.event.inputs.cli-version == '') }} + - name: install published cli + if: ${{ github.event_name == 'schedule' || (github.event.ref == 'refs/heads/dev' && github.event_name == 'workflow_dispatch' && github.event.inputs.cli-version == '') }} run: | npm install -g @microsoft/m365agentstoolkit-cli@alpha - - name: link cli for workflow_dispatch - if: ${{ github.event_name == 'workflow_dispatch' && github.event.ref != 'refs/heads/dev' && github.event.inputs.cli-version == '' }} + - name: link cli for pull_request or workflow_dispatch + if: ${{ github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.event.ref != 'refs/heads/dev' && github.event.inputs.cli-version == '') }} run: | pnpm link --global diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index aa9da101e9..78d3d5679e 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -241,13 +241,13 @@ jobs: run: | npm install -g @microsoft/m365agentstoolkit-cli@${{ github.event.inputs.cli-version }} - - name: install cli for schedule - if: ${{ github.event_name == 'schedule' || github.event_name == 'pull_request' || ( github.event.ref == 'refs/heads/dev' && github.event_name == 'workflow_dispatch' && github.event.inputs.cli-version == '' ) }} + - name: install published cli + if: ${{ github.event_name == 'schedule' || ( github.event.ref == 'refs/heads/dev' && github.event_name == 'workflow_dispatch' && github.event.inputs.cli-version == '' ) }} run: | npm install -g @microsoft/m365agentstoolkit-cli@alpha - - name: link cli for workflow_dispatch - if: ${{ github.event_name == 'workflow_dispatch' && github.event.ref != 'refs/heads/dev' && github.event.inputs.cli-version == '' }} + - name: link cli for pull_request or workflow_dispatch + if: ${{ github.event_name == 'pull_request' || ( github.event_name == 'workflow_dispatch' && github.event.ref != 'refs/heads/dev' && github.event.inputs.cli-version == '' ) }} run: | pnpm link --global