Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 12 additions & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,22 @@ on:
type: boolean
default: false
description: "[Dev/Testing only] Skip branch validation."
skipdockerbuild:
type: boolean
default: false
description: "Skip triggering the VscUse ATK docker build after CD (e.g. when the caller builds its own image)."

permissions:
actions: write
contents: write

# Production CD runs (dev / release/*) use a unique group so concurrency is a no-op and
# real releases / nightly are never cancelled or queued. Other refs (feature/PR branches
# used by the smoke pipeline) are grouped per-branch so a re-dispatch cancels the stale run.
concurrency:
group: ${{ format('cd-{0}', (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/heads/release/')) && github.run_id || github.ref_name) }}
cancel-in-progress: true

jobs:
cd:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -576,7 +587,7 @@ jobs:

trigger-docker-build:
needs: cd
if: ${{ success() }}
if: ${{ success() && !inputs.skipdockerbuild }}
runs-on: ubuntu-latest
steps:
- name: Trigger Docker Build Workflow
Expand Down
26 changes: 18 additions & 8 deletions .github/workflows/dev-smoke-test-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ on:
type: string
default: ""

# Cancel an in-progress smoke run (and let its children cancel too) when a newer
# run starts for the same branch. Distinct branches (different PRs) never collide.
concurrency:
group: dev-smoke-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true

jobs:
run-cd:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -44,16 +50,18 @@ jobs:
const owner = context.repo.owner;
const repo = context.repo.repo;
const sourceBranch = '${{ github.head_ref || github.ref_name }}';
const cdDispatchBranch = 'neil/cdprivatepipeline';

// Trigger CD workflow on private CD branch, build sourceBranch via external_ref
// Trigger CD workflow directly on the reference (source) branch
await github.rest.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: 'cd.yml',
ref: cdDispatchBranch,
ref: sourceBranch,
inputs: {
external_ref: sourceBranch
preid: 'alpha',
skipmarkdowncheck: 'true',
SkipBranchCheck: 'true',
skipdockerbuild: 'true'
}
});

Expand All @@ -63,7 +71,7 @@ jobs:
owner,
repo,
workflow_id: 'cd.yml',
branch: cdDispatchBranch,
branch: sourceBranch,
per_page: 5
});

Expand Down Expand Up @@ -133,7 +141,8 @@ jobs:
ref: 'dev',
inputs: {
tag: '${{ needs.run-cd.outputs.tag_name }}',
run_id: '${{ needs.run-cd.outputs.run_id }}'
run_id: '${{ needs.run-cd.outputs.run_id }}',
source_ref: '${{ github.head_ref || github.ref_name }}'
}
});

Expand Down Expand Up @@ -224,12 +233,13 @@ jobs:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const sourceBranch = '${{ github.head_ref || github.ref_name }}';

await github.rest.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: 'ui-test-vscuse-template.yml',
ref: 'dev',
ref: sourceBranch,
inputs: {
image_tag: '${{ needs.run-cd.outputs.tag_name }}',
'email-receiver': '${{ inputs.email }}',
Expand All @@ -244,7 +254,7 @@ jobs:
owner,
repo,
workflow_id: 'ui-test-vscuse-template.yml',
branch: 'dev',
branch: sourceBranch,
per_page: 5
});

Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/ui-test-vscuse-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ on:
permissions:
actions: read

# Production runs (dev / release/*) use a unique group so concurrency is a no-op (nightly is
# never cancelled); feature/PR branches dispatched by the smoke pipeline are grouped per-branch
# so a re-dispatch cancels the stale UI-test run.
concurrency:
group: ${{ format('uitest-vscuse-template-{0}', (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/heads/release/')) && github.run_id || github.ref_name) }}
cancel-in-progress: true

jobs:
run:
uses: ./.github/workflows/ui-test-vscuse-common.yml
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/vscuse-atk-docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,20 @@ on:
description: 'Whether to trigger UI test after build'
type: boolean
default: false
source_ref:
description: 'Source branch this build belongs to. Used only to group concurrency so a re-dispatch for the same branch cancels the stale build. Leave empty for release/manual builds.'
required: false
default: ''
env:
REGISTRY: ghcr.io
IMAGE_NAME: officedev/vscuse-atk-vscode

# When dispatched with a source_ref (smoke pipeline) group per-branch so a re-dispatch cancels
# the stale build. Without it (CD / manual / release) the run_id keeps the group unique → no-op.
concurrency:
group: ${{ format('vscuse-atk-docker-{0}', github.event.inputs.source_ref || github.run_id) }}
cancel-in-progress: true

jobs:
build-and-push:
runs-on: ubuntu-latest
Expand Down
9 changes: 6 additions & 3 deletions packages/fx-core/src/question/scaffold/constructNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT license.

import { IQTreeNode, OptionItem, Platform, SingleSelectQuestion } from "@microsoft/teamsfx-api";
import { featureFlagManager } from "../../common/featureFlags";
import { FeatureFlag, FeatureFlags, featureFlagManager } from "../../common/featureFlags";
import { getLocalizedString } from "../../common/localizeUtils";
import {
apiSpecNode,
Expand All @@ -16,7 +16,11 @@ import { GCConnectionIdQuestion, GCNameQuestion } from "../create";
import { QuestionNames } from "../questionNames";

function isFeatureEnabled(flagName: string): boolean {
return featureFlagManager.getBooleanValue({ name: flagName, defaultValue: "false" });
const flag = (Object.values(FeatureFlags) as FeatureFlag[]).find((f) => f.name === flagName) ?? {
name: flagName,
defaultValue: "false",
};
return featureFlagManager.getBooleanValue(flag);
}

export function constructNode(
Expand Down Expand Up @@ -117,7 +121,6 @@ function resolveNodeReference(

// TypeScript-defined complex nodes (lazy import to avoid circular dependency)
case "mcpServerTypeNode": {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { MCPServerTypeNode } = require("./vsc/teamsProjectTypeNode");
node = MCPServerTypeNode();
break;
Expand Down
139 changes: 128 additions & 11 deletions packages/fx-core/tests/question/scaffold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
SingleSelectQuestion,
StringValidation,
} from "@microsoft/teamsfx-api";
import { featureFlagManager, FeatureFlags } from "../../src/common/featureFlags";
import { featureFlagManager, FeatureFlagName, FeatureFlags } from "../../src/common/featureFlags";
import { getLocalizedString } from "../../src/common/localizeUtils";
import { AppDefinition } from "../../src/component/driver/teamsApp/interfaces/appdefinitions/appDefinition";
import { Bot } from "../../src/component/driver/teamsApp/interfaces/appdefinitions/bot";
Expand All @@ -26,7 +26,16 @@ import {
} from "../../src/question/scaffold/commonNodes";
import { constructNode } from "../../src/question/scaffold/constructNode";
import { scaffoldQuestionForVS } from "../../src/question/scaffold/vs/createRootNode";
import { ActionStartOptions } from "../../src/question/scaffold/vsc/CapabilityOptions";
import {
ActionStartOptions,
BotCapabilityOptions,
CustomCopilotRagOptions,
MeArchitectureOptions,
MeCapabilityOptions,
NotificationBotOptions,
TabCapabilityOptions,
TeamsAgentCapabilityOptions,
} from "../../src/question/scaffold/vsc/CapabilityOptions";
import { ProjectTypeOptions } from "../../src/question/scaffold/vsc/ProjectTypeOptions";
import {
createFromTdpNode,
Expand All @@ -48,15 +57,6 @@ import { AppPackageFolderName } from "@microsoft/teamsfx-api";
import fs from "fs-extra";
import path from "path";
import { assert, vi } from "vitest";
import {
BotCapabilityOptions,
CustomCopilotRagOptions,
MeArchitectureOptions,
MeCapabilityOptions,
NotificationBotOptions,
TabCapabilityOptions,
TeamsAgentCapabilityOptions,
} from "../../src/question/scaffold/vsc/CapabilityOptions";
import {
botProjectTypeNode,
CreateNewPluginManifestSentinel,
Expand Down Expand Up @@ -1616,6 +1616,123 @@ describe("constructNode", () => {
assert.equal(options.length, 2);
});

it("should include a true-default feature-flagged option when env var is unset", () => {
delete process.env[FeatureFlagName.OpenPluginImportExport];
const json = JSON.stringify({
data: {
title: "test.title",
name: "test",
type: "singleSelect",
options: [
{ id: "always-visible", label: "Always" },
{ id: "flagged", label: "Flagged", featureFlag: FeatureFlagName.OpenPluginImportExport },
],
},
});

const node = constructNode(json);
const data = node.data as SingleSelectQuestion;
const options = data.staticOptions as OptionItem[];
assert.equal(options.length, 2);
assert.isTrue(options.some((o) => o.id === "flagged"));
});

it("should exclude a false-default feature-flagged option when env var is unset", () => {
delete process.env[FeatureFlagName.AgentSkillsManifest];
const json = JSON.stringify({
data: {
title: "test.title",
name: "test",
type: "singleSelect",
options: [
{ id: "always-visible", label: "Always" },
{ id: "flagged", label: "Flagged", featureFlag: FeatureFlagName.AgentSkillsManifest },
],
},
});

const node = constructNode(json);
const data = node.data as SingleSelectQuestion;
const options = data.staticOptions as OptionItem[];
assert.equal(options.length, 1);
assert.equal(options[0].id, "always-visible");
});

it("should let an explicit env override win for a true-default flag", () => {
process.env[FeatureFlagName.OpenPluginImportExport] = "false";
try {
const json = JSON.stringify({
data: {
title: "test.title",
name: "test",
type: "singleSelect",
options: [
{ id: "always-visible", label: "Always" },
{
id: "flagged",
label: "Flagged",
featureFlag: FeatureFlagName.OpenPluginImportExport,
},
],
},
});

const node = constructNode(json);
const data = node.data as SingleSelectQuestion;
const options = data.staticOptions as OptionItem[];
assert.equal(options.length, 1);
assert.equal(options[0].id, "always-visible");
} finally {
delete process.env[FeatureFlagName.OpenPluginImportExport];
}
});

it("should let an explicit env override win for a false-default flag", () => {
process.env[FeatureFlagName.AgentSkillsManifest] = "true";
try {
const json = JSON.stringify({
data: {
title: "test.title",
name: "test",
type: "singleSelect",
options: [
{ id: "always-visible", label: "Always" },
{ id: "flagged", label: "Flagged", featureFlag: FeatureFlagName.AgentSkillsManifest },
],
},
});

const node = constructNode(json);
const data = node.data as SingleSelectQuestion;
const options = data.staticOptions as OptionItem[];
assert.equal(options.length, 2);
assert.isTrue(options.some((o) => o.id === "flagged"));
} finally {
delete process.env[FeatureFlagName.AgentSkillsManifest];
}
});

it("should exclude an unknown feature flag when env var is unset", () => {
delete process.env["TEAMSFX_UNKNOWN_FLAG"];
const json = JSON.stringify({
data: {
title: "test.title",
name: "test",
type: "singleSelect",
options: [
{ id: "always-visible", label: "Always" },
{ id: "flagged", label: "Flagged", featureFlag: "TEAMSFX_UNKNOWN_FLAG" },
],
},
});

const node = constructNode(json);
const data = node.data as SingleSelectQuestion;
const options = data.staticOptions as OptionItem[];
assert.equal(options.length, 1);
assert.equal(options[0].id, "always-visible");
});

it("should handle group type nodes", () => {
const json = JSON.stringify({
data: { type: "group", name: "test-group" },
Expand Down
Loading