Skip to content

Commit

Permalink
Merge pull request #11566 from OfficeDev/yuqzho/validate-manifest
Browse files Browse the repository at this point in the history
feat: validate manifest
  • Loading branch information
MSFT-yiz authored May 13, 2024
2 parents 98854d3 + b4fbc44 commit fca6a78
Show file tree
Hide file tree
Showing 25 changed files with 1,235 additions and 117 deletions.
6 changes: 4 additions & 2 deletions packages/fx-core/resource/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -759,8 +759,10 @@
"driver.teamsApp.summary.copyIconSuccess": "%s icon(s) were successfully updated under %s.",
"driver.teamsApp.summary.validate": "Teams Toolkit has checked against all validation rules:\n\nSummary:\n%s.\n%s%s\n%s\n\nA complete log of validations can be found in %s",
"driver.teamsApp.summary.validate.checkPath": "You can check and update your Teams app package at %s.\n",
"driver.teamsApp.summary.validateManifest": "Teams Toolkit has checked manifest with its schema:\n\nSummary:\n%s.\n%s%s\n",
"driver.teamsApp.summary.validateManifest.checkPath": "You can check and update your Teams manifest at %s.\n",
"driver.teamsApp.summary.validateManifest": "Teams Toolkit has checked manifest(s) with the corresponding schema:\n\nSummary:\n%s.",
"driver.teamsApp.summary.validateTeamsManifest.checkPath": "You can check and update your Teams manifest at %s.",
"driver.teamsApp.summary.validateDeclarativeCopilotManifest.checkPath": "You can check and update your Declarative Copilot manifest at %s.",
"driver.teamsApp.summary.validatePluginManifest.checkPath": "You can check and update your Copilot Plugin manifest at %s.",
"driver.teamsApp.summary.validate.succeed": "%s passed",
"driver.teamsApp.summary.validate.failed": "%s failed",
"driver.teamsApp.summary.validate.warning": "%s warning",
Expand Down
15 changes: 2 additions & 13 deletions packages/fx-core/src/component/driver/teamsApp/createAppPackage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { manifestUtils } from "./utils/ManifestUtils";
import { expandEnvironmentVariable, getEnvironmentVariables } from "../../utils/common";
import { TelemetryPropertyKey } from "./utils/telemetry";
import { InvalidFileOutsideOfTheDirectotryError } from "../../../error/teamsApp";
import { normalizePath } from "./utils/utils";
import { getResolvedManifest, normalizePath } from "./utils/utils";
import { copilotGptManifestUtils } from "./utils/CopilotGptManifestUtils";

export const actionName = "teamsApp/zipAppPackage";
Expand Down Expand Up @@ -309,18 +309,7 @@ export class CreateAppPackageDriver implements StepDriver {
telemetryKey: TelemetryPropertyKey
): Promise<Result<string, FxError>> {
const content = await fs.readFile(filePath, "utf8");
const vars = getEnvironmentVariables(content);
ctx.addTelemetryProperties({
[telemetryKey]: vars.join(";"),
});
const result = expandEnvironmentVariable(content);
const notExpandedVars = getEnvironmentVariables(result);
if (notExpandedVars.length > 0) {
return err(
new MissingEnvironmentVariablesError("teamsApp", notExpandedVars.join(","), filePath)
);
}
return ok(result);
return getResolvedManifest(content, filePath, telemetryKey, ctx);
}

private validateArgs(args: CreateAppPackageArgs): Result<any, FxError> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
export interface DeclarativeCopilotManifestValidationResult {
id: string;
filePath: string;
validationResult: string[];
actionValidationResult: PluginManifestValidationResult[];
}

export interface PluginManifestValidationResult {
id: string;
filePath: string;
validationResult: string[];
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { FxError, Result, err, ok, DeclarativeCopilotManifestSchema } from "@microsoft/teamsfx-api";
import {
FxError,
Result,
err,
ok,
DeclarativeCopilotManifestSchema,
ManifestUtil,
IDeclarativeCopilot,
Platform,
Colors,
} from "@microsoft/teamsfx-api";
import fs from "fs-extra";
import { FileNotFoundError, JSONSyntaxError, WriteFileError } from "../../../../error/common";
import stripBom from "strip-bom";
import { TelemetryPropertyKey } from "./telemetry";
import { WrapDriverContext } from "../../util/wrapUtil";
import { getResolvedManifest } from "./utils";
import { AppStudioResultFactory } from "../results";
import { AppStudioError } from "../errors";
import { getDefaultString, getLocalizedString } from "../../../../common/localizeUtils";
import { DeclarativeCopilotManifestValidationResult } from "../interfaces/ValidationResult";
import path from "path";
import { pluginManifestUtils } from "./PluginManifestUtils";
import { SummaryConstant } from "../../../configManager/constant";
import { EOL } from "os";

export class CopilotGptManifestUtils {
public async readCopilotGptManifestFile(
Expand All @@ -26,6 +47,34 @@ export class CopilotGptManifestUtils {
}
}

/**
* Get Declarative Copilot Manifest with env value filled.
* @param path path of declaraitve Copilot
* @returns resolved manifest
*/
public async getManifest(
path: string,
context?: WrapDriverContext
): Promise<Result<DeclarativeCopilotManifestSchema, FxError>> {
const manifestRes = await this.readCopilotGptManifestFile(path);
if (manifestRes.isErr()) {
return err(manifestRes.error);
}
// Add environment variable keys to telemetry
const resolvedManifestRes = getResolvedManifest(
JSON.stringify(manifestRes.value),
path,
TelemetryPropertyKey.customizedCopilotGptKeys,
context
);

if (resolvedManifestRes.isErr()) {
return err(resolvedManifestRes.error);
}
const resolvedManifestString = resolvedManifestRes.value;
return ok(JSON.parse(resolvedManifestString));
}

public async writeCopilotGptManifestFile(
manifest: DeclarativeCopilotManifestSchema,
path: string
Expand All @@ -39,6 +88,60 @@ export class CopilotGptManifestUtils {
return ok(undefined);
}

public async validateAgainstSchema(
declaraitveCopilot: IDeclarativeCopilot,
manifestPath: string,
context?: WrapDriverContext
): Promise<Result<DeclarativeCopilotManifestValidationResult, FxError>> {
const manifestRes = await this.getManifest(manifestPath, context);
if (manifestRes.isErr()) {
return err(manifestRes.error);
}

const manifest = manifestRes.value;
try {
const manifestValidationRes = await ManifestUtil.validateManifest(manifestRes.value);
const res: DeclarativeCopilotManifestValidationResult = {
id: declaraitveCopilot.id,
filePath: manifestPath,
validationResult: manifestValidationRes,
actionValidationResult: [],
};

if (manifest.actions?.length) {
// action
for (const action of manifest.actions) {
const actionPath = path.join(path.dirname(manifestPath), action.file);

const actionValidationRes = await pluginManifestUtils.validateAgainstSchema(
action,
actionPath,
context
);
if (actionValidationRes.isErr()) {
return err(actionValidationRes.error);
} else {
res.actionValidationResult.push(actionValidationRes.value);
}
}
}
return ok(res);
} catch (e: any) {
return err(
AppStudioResultFactory.UserError(
AppStudioError.ValidationFailedError.name,
AppStudioError.ValidationFailedError.message([
getLocalizedString(
"error.appstudio.validateFetchSchemaFailed",
manifestRes.value.$schema,
e.message
),
])
)
);
}
}

public async addAction(
copilotGptPath: string,
id: string,
Expand Down Expand Up @@ -67,6 +170,89 @@ export class CopilotGptManifestUtils {
}
}
}

public logValidationErrors(
validationRes: DeclarativeCopilotManifestValidationResult,
platform: Platform,
pluginPath: string
): string | Array<{ content: string; color: Colors }> {
const validationErrors = validationRes.validationResult;
const filePath = validationRes.filePath;
let hasError = validationErrors.length > 0;

for (const actionValidationRes of validationRes.actionValidationResult) {
if (actionValidationRes.validationResult.length > 0) {
hasError = true;
break;
}
}
if (!hasError) {
return "";
}

if (platform !== Platform.CLI) {
const errors = validationErrors
.map((error: string) => {
return `${SummaryConstant.Failed} ${error}`;
})
.join(EOL);
let outputMessage =
getLocalizedString(
"driver.teamsApp.summary.validateDeclarativeCopilotManifest.checkPath",
filePath
) +
EOL +
errors;

for (const actionValidationRes of validationRes.actionValidationResult) {
if (pluginPath && actionValidationRes.filePath !== pluginPath) {
// do not output validation result of the Declarative Copilot if same file has been validated when validating plugin manifest.
const actionValidationMessage = pluginManifestUtils.logValidationErrors(
actionValidationRes,
platform
) as string;
if (actionValidationMessage) {
outputMessage += EOL + actionValidationMessage;
}
}
}

return outputMessage;
} else {
const outputMessage = [];
outputMessage.push({
content:
getDefaultString(
"driver.teamsApp.summary.validateDeclarativeCopilotManifest.checkPath",
filePath
) + "\n",
color: Colors.BRIGHT_WHITE,
});
validationErrors.map((error: string) => {
outputMessage.push({ content: `${SummaryConstant.Failed} `, color: Colors.BRIGHT_RED });
outputMessage.push({
content: `${error}\n`,
color: Colors.BRIGHT_WHITE,
});
});

for (const actionValidationRes of validationRes.actionValidationResult) {
if (pluginPath && actionValidationRes.filePath !== pluginPath) {
const actionValidationMessage = pluginManifestUtils.logValidationErrors(
actionValidationRes,
platform
);
if (actionValidationMessage) {
outputMessage.push(
...(actionValidationMessage as Array<{ content: string; color: Colors }>)
);
}
}
}

return outputMessage;
}
}
}

export const copilotGptManifestUtils = new CopilotGptManifestUtils();
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { WrapDriverContext } from "../../util/wrapUtil";
import { hooks } from "@feathersjs/hooks";
import { ErrorContextMW } from "../../../../core/globalVars";
import { getCapabilities as checkManifestCapabilities } from "../../../../common/projectTypeChecker";
import { getResolvedManifest } from "./utils";

export class ManifestUtils {
async readAppManifest(projectPath: string): Promise<Result<TeamsAppManifest, FxError>> {
Expand Down Expand Up @@ -314,22 +315,17 @@ export class ManifestUtils {
const manifestTemplateString = JSON.stringify(manifest);

// Add environment variable keys to telemetry
const customizedKeys = getEnvironmentVariables(manifestTemplateString);
const telemetryProps: { [key: string]: string } = {};
telemetryProps[TelemetryPropertyKey.customizedKeys] = customizedKeys.join(";");
if (context) {
context.addTelemetryProperties(telemetryProps);
}

const resolvedManifestString = expandEnvironmentVariable(manifestTemplateString);
const resolvedManifestRes = getResolvedManifest(
manifestTemplateString,
manifestTemplatePath,
TelemetryPropertyKey.customizedKeys,
context
);

const tokens = getEnvironmentVariables(resolvedManifestString);
if (tokens.length > 0) {
return err(
new MissingEnvironmentVariablesError("teamsApp", tokens.join(","), manifestTemplatePath)
);
if (resolvedManifestRes.isErr()) {
return err(resolvedManifestRes.error);
}

const resolvedManifestString = resolvedManifestRes.value;
manifest = JSON.parse(resolvedManifestString);

if (generateIdIfNotResolved) {
Expand Down
Loading

0 comments on commit fca6a78

Please sign in to comment.