Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: switch to builder API for declarative agent apps #13056

Draft
wants to merge 17 commits into
base: dev
Choose a base branch
from
145 changes: 142 additions & 3 deletions packages/fx-core/src/component/m365/packageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT license.

import { hooks } from "@feathersjs/hooks";
import { LogProvider, SystemError, UserError } from "@microsoft/teamsfx-api";
import { LogProvider, SystemError, TeamsAppManifest, UserError } from "@microsoft/teamsfx-api";
import AdmZip from "adm-zip";
import FormData from "form-data";
import fs from "fs-extra";
Expand All @@ -20,10 +20,17 @@
import { WrappedAxiosClient } from "../../common/wrappedAxiosClient";
import { NotExtendedToM365Error } from "./errors";
import { MosServiceEndpoint } from "./serviceConstant";
import { IsDeclarativeAgentManifest } from "../../common/projectTypeChecker";
import stripBom from "strip-bom";

const M365ErrorSource = "M365";
const M365ErrorComponent = "PackageService";

export enum AppScope {
Personal = "Personal",
Shared = "Shared",
}

// Call m365 service for package CRUD
export class PackageService {
private static sharedInstance: PackageService;
Expand Down Expand Up @@ -139,14 +146,96 @@
}

@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
public async sideLoading(token: string, manifestPath: string): Promise<[string, string]> {
public async sideLoading(
token: string,
packagePath: string,
appScope = AppScope.Personal
): Promise<[string, string, string]> {
const manifest = this.getManifestFromZip(packagePath);
if (!manifest) {
throw new Error("Invalid app package zip. manifest.json is missing");
}
const isDelcarativeAgentApp = IsDeclarativeAgentManifest(manifest);
if (isDelcarativeAgentApp) {
const res = await this.sideLoadingV2(token, packagePath, appScope);
let shareLink = "";
if (appScope == AppScope.Shared) {
shareLink = await this.getShareLink(token, res[0]);
}
return [res[0], res[1], shareLink];
} else {
const res = await this.sideLoadingV1(token, packagePath);
return [res[0], res[1], ""];
}
}
// Side loading using Builder API
@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
public async sideLoadingV2(
token: string,
manifestPath: string,
appScope: AppScope
): Promise<[string, string]> {
try {
this.checkZip(manifestPath);
const data = await fs.readFile(manifestPath);
const content = new FormData();
content.append("package", data);
const serviceUrl = await this.getTitleServiceUrl(token);
this.logger?.verbose("Uploading package ...");
this.logger?.debug("Uploading package with sideLoading V2 ...");
const uploadHeaders = content.getHeaders();
uploadHeaders["Authorization"] = `Bearer ${token}`;
const uploadResponse = await this.axiosInstance.post(
"/builder/v1/users/packages",
content.getBuffer(),
{
baseURL: serviceUrl,
headers: uploadHeaders,
params: {
scope: appScope,
},
}
);

const statusId = uploadResponse.data.statusId;
this.logger?.debug(`Acquiring package with statusId: ${statusId as string} ...`);

do {
const statusResponse = await this.axiosInstance.get(
`/builder/v1/users/packages/status/${statusId as string}`,
{
baseURL: serviceUrl,
headers: { Authorization: `Bearer ${token}` },
}
);
const resCode = statusResponse.status;
this.logger?.debug(`Package status: ${resCode} ...`);
if (resCode === 200) {
const titleId: string = statusResponse.data.titleId;
const appId: string = statusResponse.data.appId;
this.logger?.info(`TitleId: ${titleId}`);
this.logger?.info(`AppId: ${appId}`);
this.logger?.verbose("Sideloading done.");
return [titleId, appId];
} else {
await waitSeconds(2);

Check warning on line 220 in packages/fx-core/src/component/m365/packageService.ts

View check run for this annotation

Codecov / codecov/patch

packages/fx-core/src/component/m365/packageService.ts#L220

Added line #L220 was not covered by tests
}
} while (true);
} catch (error: any) {
if (error.response) {
error = this.traceError(error);
}
throw assembleError(error, M365ErrorSource);
}
}
@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
public async sideLoadingV1(token: string, manifestPath: string): Promise<[string, string]> {
try {
this.checkZip(manifestPath);
const data = await fs.readFile(manifestPath);
const content = new FormData();
content.append("package", data);
const serviceUrl = await this.getTitleServiceUrl(token);
this.logger?.debug("Uploading package with sideLoading V1 ...");
const uploadHeaders = content.getHeaders();
uploadHeaders["Authorization"] = `Bearer ${token}`;
const uploadResponse = await this.axiosInstance.post(
Expand Down Expand Up @@ -211,6 +300,27 @@
}
}
@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
public async getShareLink(token: string, titleId: string): Promise<string> {
const serviceUrl = await this.getTitleServiceUrl(token);
try {
const resp = await this.axiosInstance.get(
`/marketplace/v1/users/titles/${titleId}/sharingInfo`,
{
baseURL: serviceUrl,
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return resp.data.unifiedStoreLink;
} catch (error: any) {
if (error.response) {
error = this.traceError(error);

Check warning on line 318 in packages/fx-core/src/component/m365/packageService.ts

View check run for this annotation

Codecov / codecov/patch

packages/fx-core/src/component/m365/packageService.ts#L318

Added line #L318 was not covered by tests
}
throw assembleError(error, M365ErrorSource);
}
}
@hooks([ErrorContextMW({ source: M365ErrorSource, component: M365ErrorComponent })])
public async getLaunchInfoByManifestId(token: string, manifestId: string): Promise<any> {
try {
const serviceUrl = await this.getTitleServiceUrl(token);
Expand Down Expand Up @@ -293,6 +403,24 @@
});
this.logger?.verbose("Unacquiring done.");
} catch (error: any) {
// try to delete in the builder API
try {
const serviceUrl = await this.getTitleServiceUrl(token);
this.logger?.verbose(`Unacquiring package with TitleId ${titleId} in builder API...`);
await this.axiosInstance.delete(`/builder/v1/users/titles/${titleId}`, {
baseURL: serviceUrl,
headers: {
Authorization: `Bearer ${token}`,
},
});
this.logger?.verbose("Unacquiring using builder api done.");
return;
} catch (subError: any) {
if (subError.response) {
subError = this.traceError(subError);
}
this.logger?.error(subError);
}
if (error.response) {
error = this.traceError(error);
}
Expand Down Expand Up @@ -440,4 +568,15 @@
this.logger?.warning(`Please make sure input path is a valid app package zip. ${path}`);
}
}

private getManifestFromZip(path: string): TeamsAppManifest | undefined {
const zip = new AdmZip(path);
const manifestEntry = zip.getEntry("manifest.json");
if (!manifestEntry) {
return undefined;

Check warning on line 576 in packages/fx-core/src/component/m365/packageService.ts

View check run for this annotation

Codecov / codecov/patch

packages/fx-core/src/component/m365/packageService.ts#L576

Added line #L576 was not covered by tests
}
let manifestContent = manifestEntry.getData().toString("utf8");
manifestContent = stripBom(manifestContent);
return JSON.parse(manifestContent) as TeamsAppManifest;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ describe("teamsApp/extendToM365", async () => {
["appId", "MY_APP_ID"],
]);

sinon.stub(PackageService.prototype, "sideLoading").resolves(["test-title-id", "test-app-id"]);
sinon
.stub(PackageService.prototype, "sideLoading")
.resolves(["test-title-id", "test-app-id", ""]);
sinon.stub(fs, "pathExists").resolves(true);

const result = await acquireDriver.execute(args, mockedDriverContext, outputEnvVarNames);
Expand Down
Loading
Loading