diff --git a/appservice/package-lock.json b/appservice/package-lock.json index a7bc90758e..5116e6dc52 100644 --- a/appservice/package-lock.json +++ b/appservice/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/vscode-azext-azureappservice", - "version": "2.2.7", + "version": "2.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@microsoft/vscode-azext-azureappservice", - "version": "2.2.7", + "version": "2.3.0", "license": "MIT", "dependencies": { "@azure/abort-controller": "^1.0.4", diff --git a/appservice/package.json b/appservice/package.json index 823b9b8f29..aec3417686 100644 --- a/appservice/package.json +++ b/appservice/package.json @@ -1,7 +1,7 @@ { "name": "@microsoft/vscode-azext-azureappservice", "author": "Microsoft Corporation", - "version": "2.2.7", + "version": "2.3.0", "description": "Common tools for developing Azure App Service extensions for VS Code", "tags": [ "azure", diff --git a/appservice/src/deploy/IDeployContext.ts b/appservice/src/deploy/IDeployContext.ts index 87c29ec2aa..9ae7fbbd50 100644 --- a/appservice/src/deploy/IDeployContext.ts +++ b/appservice/src/deploy/IDeployContext.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { AppServicePlan } from '@azure/arm-appservice'; +import { ExecuteActivityContext, IActionContext } from '@microsoft/vscode-azext-utils'; import { WorkspaceFolder } from 'vscode'; +import { ParsedSite, SiteClient } from '../SiteClient'; export enum AppSource { setting = 'setting', @@ -29,4 +31,16 @@ export interface IDeployContext extends IActionContext { flexConsumptionRemoteBuild?: boolean; stopAppBeforeDeploy?: boolean; syncTriggersPostDeploy?: boolean; + /** + * id retrieved from scm-deployment-id header to track deployment + */ + locationUrl?: string; +} + +// only used by the tools package facilitate creating the wizard execute steps +export interface InnerDeployContext extends IDeployContext, ExecuteActivityContext { + site: ParsedSite; + client: SiteClient; + fsPath: string; + aspPromise: Promise } diff --git a/appservice/src/deploy/delayFirstWebAppDeploy.ts b/appservice/src/deploy/delayFirstWebAppDeploy.ts deleted file mode 100644 index 95711e28fa..0000000000 --- a/appservice/src/deploy/delayFirstWebAppDeploy.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type { AppServicePlan } from '@azure/arm-appservice'; -import { IActionContext } from '@microsoft/vscode-azext-utils'; -import { ParsedSite } from '../SiteClient'; - -export async function delayFirstWebAppDeploy(context: IActionContext, site: ParsedSite, aspPromise: Promise): Promise { - // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor - await new Promise(async (resolve: () => void): Promise => { - setTimeout(resolve, 10000); - try { - // this delay is only valid for the first deployment to a Linux web app on a basic asp, so resolve for anything else - if (site.isFunctionApp) { - resolve(); - } - - const asp: AppServicePlan | undefined = await aspPromise; - if (!asp || !asp.sku || !asp.sku.tier || asp.sku.tier.toLowerCase() !== 'basic') { - resolve(); - } - if (!site.isLinux) { - resolve(); - } - - const kuduClient = await site.createClient(context); - const deployments: number = (await kuduClient.getDeployResults(context)).length; - if (deployments > 1) { - resolve(); - } - } catch (error) { - // ignore the error, an error here isn't a deployment failure - resolve(); - } - }); -} diff --git a/appservice/src/deploy/deploy.ts b/appservice/src/deploy/deploy.ts index a38613bb90..6bf654af34 100644 --- a/appservice/src/deploy/deploy.ts +++ b/appservice/src/deploy/deploy.ts @@ -3,121 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { AppServicePlan, SiteConfigResource } from '@azure/arm-appservice'; -import * as fse from 'fs-extra'; -import * as path from 'path'; -import { l10n, ProgressLocation, window } from 'vscode'; -import { ext } from '../extensionVariables'; -import { ScmType } from '../ScmType'; +import { AppServicePlan } from '@azure/arm-appservice'; +import { AzureWizard, ExecuteActivityContext } from '@microsoft/vscode-azext-utils'; +import { l10n } from 'vscode'; import { ParsedSite } from '../SiteClient'; -import { randomUtils } from '../utils/randomUtils'; -import { deployToStorageAccount } from './deployToStorageAccount'; -import { deployWar } from './deployWar'; -import { deployZip } from './deployZip'; -import { IDeployContext } from './IDeployContext'; -import { localGitDeploy } from './localGitDeploy'; -import { startPostDeployTask } from './runDeployTask'; -import { syncTriggersPostDeploy } from './syncTriggersPostDeploy'; +import { IDeployContext, InnerDeployContext } from './IDeployContext'; +import { createDeployExecuteSteps } from './wizard/createDeployWizard'; /** * NOTE: This leverages a command with id `ext.prefix + '.showOutputChannel'` that should be registered by each extension */ -export async function deploy(site: ParsedSite, fsPath: string, context: IDeployContext): Promise { +export async function deploy(site: ParsedSite, fsPath: string, context: IDeployContext & ExecuteActivityContext): Promise { const client = await site.createClient(context); - const config: SiteConfigResource = await client.getSiteConfig(); - // We use the AppServicePlan in a few places, but we don't want to delay deployment, so start the promise now and save as a const const aspPromise: Promise = client.getAppServicePlan(); - try { - context.telemetry.properties.sourceHash = randomUtils.getPseudononymousStringHash(fsPath); - context.telemetry.properties.destHash = randomUtils.getPseudononymousStringHash(site.fullName); - context.telemetry.properties.scmType = String(config.scmType); - context.telemetry.properties.isSlot = site.isSlot ? 'true' : 'false'; - context.telemetry.properties.alwaysOn = config.alwaysOn ? 'true' : 'false'; - context.telemetry.properties.linuxFxVersion = getLinuxFxVersionForTelemetry(config); - context.telemetry.properties.nodeVersion = String(config.nodeVersion); - context.telemetry.properties.pythonVersion = String(config.pythonVersion); - context.telemetry.properties.hasCors = config.cors ? 'true' : 'false'; - context.telemetry.properties.hasIpSecurityRestrictions = config.ipSecurityRestrictions && config.ipSecurityRestrictions.length > 0 ? 'true' : 'false'; - context.telemetry.properties.javaVersion = String(config.javaVersion); - context.telemetry.properties.siteKind = site.kind; - client.getState().then( - (state: string) => { - context.telemetry.properties.state = state; - }, - () => { - // ignore - }); - aspPromise.then( - (plan: AppServicePlan | undefined) => { - if (plan) { - context.telemetry.properties.planStatus = String(plan.status); - context.telemetry.properties.planKind = String(plan.kind); - if (plan.sku) { - context.telemetry.properties.planSize = String(plan.sku.size); - context.telemetry.properties.planTier = String(plan.sku.tier); - } - } - }, - () => { - // ignore - }); - } catch (error) { - // Ignore - } - const title: string = l10n.t('Deploying to "{0}"... Check [output window](command:{1}) for status.', site.fullName, ext.prefix + '.showOutputChannel'); - await window.withProgress({ location: ProgressLocation.Notification, title }, async () => { - if (context.stopAppBeforeDeploy) { - ext.outputChannel.appendLog(l10n.t('Stopping app...'), { resourceName: site.fullName }); - await client.stop(); - } - - ext.outputChannel.appendLog(l10n.t('Starting deployment...'), { resourceName: site.fullName }); - try { - if (!context.deployMethod && config.scmType === ScmType.GitHub) { - throw new Error(l10n.t('"{0}" is connected to a GitHub repository. Push to GitHub repository to deploy.', site.fullName)); - } else if (!context.deployMethod && config.scmType === ScmType.LocalGit) { - await localGitDeploy(site, { fsPath: fsPath }, context); - } else { - if (!(await fse.pathExists(fsPath))) { - throw new Error(l10n.t('Failed to deploy path that does not exist: {0}', fsPath)); - } - - const javaRuntime = site.isLinux ? config.linuxFxVersion : config.javaContainer; - if (javaRuntime && /^(tomcat|wildfly|jboss)/i.test(javaRuntime)) { - await deployWar(context, site, fsPath); - } else if (javaRuntime && /^java/i.test(javaRuntime) && !site.isFunctionApp) { - const pathFileMap = new Map([ - [path.basename(fsPath), 'app.jar'] - ]); - await deployZip(context, site, fsPath, aspPromise, pathFileMap); - } else if (context.deployMethod === 'storage') { - await deployToStorageAccount(context, fsPath, site); - } else { - await deployZip(context, site, fsPath, aspPromise); - } - } - } finally { - if (context.stopAppBeforeDeploy) { - ext.outputChannel.appendLog(l10n.t('Starting app...'), { resourceName: site.fullName }); - await client.start(); - } - } - - await startPostDeployTask(context, fsPath, config.scmType, site.fullName); - - if (context.syncTriggersPostDeploy) { - // Don't sync triggers if app is stopped /~https://github.com/microsoft/vscode-azurefunctions/issues/1608 - const state: string | undefined = await client.getState(); - if (state?.toLowerCase() === 'running') { - await syncTriggersPostDeploy(context, site); - } - } - }); -} - -function getLinuxFxVersionForTelemetry(config: SiteConfigResource): string { - const linuxFxVersion = config.linuxFxVersion || ''; - // Docker values point to the user's specific image, which we don't want to track - return /^docker/i.test(linuxFxVersion) ? 'docker' : linuxFxVersion; + const innerContext: InnerDeployContext = Object.assign(context, { site, fsPath, client, aspPromise }); + const title: string = l10n.t('Deploying to app "{0}"', site.fullName); + const executeSteps = await createDeployExecuteSteps(innerContext); + const wizard: AzureWizard = new AzureWizard(innerContext, { executeSteps, title }); + innerContext.activityTitle = title; + await wizard.execute(); } diff --git a/appservice/src/deploy/deployWar.ts b/appservice/src/deploy/deployWar.ts deleted file mode 100644 index d2a22feda4..0000000000 --- a/appservice/src/deploy/deployWar.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IActionContext } from '@microsoft/vscode-azext-utils'; -import * as fs from 'fs'; -import * as vscode from 'vscode'; -import { publisherName } from '../constants'; -import { ParsedSite } from '../SiteClient'; -import { getFileExtension } from '../utils/pathUtils'; -import { waitForDeploymentToComplete } from './waitForDeploymentToComplete'; - -export async function deployWar(context: IActionContext, site: ParsedSite, fsPath: string): Promise { - if (getFileExtension(fsPath) !== 'war') { - throw new Error(vscode.l10n.t('Path specified is not a war file')); - } - - const kuduClient = await site.createClient(context); - await kuduClient.warPushDeploy(context, () => fs.createReadStream(fsPath), { - isAsync: true, - author: publisherName, - deployer: publisherName, - trackDeploymentId: true - }); - - await waitForDeploymentToComplete(context, site); -} diff --git a/appservice/src/deploy/deployZip.ts b/appservice/src/deploy/deployZip.ts deleted file mode 100644 index 942b3472ee..0000000000 --- a/appservice/src/deploy/deployZip.ts +++ /dev/null @@ -1,51 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type { AppServicePlan } from '@azure/arm-appservice'; -import { RequestBodyType } from '@azure/core-rest-pipeline'; -import { ParsedSite } from '../SiteClient'; -import { publisherName } from '../constants'; -import { IDeployContext } from './IDeployContext'; -import { delayFirstWebAppDeploy } from './delayFirstWebAppDeploy'; -import { runWithZipStream } from './runWithZipStream'; -import { waitForDeploymentToComplete } from './waitForDeploymentToComplete'; - -export async function deployZip(context: IDeployContext, site: ParsedSite, fsPath: string, aspPromise: Promise, pathFileMap?: Map): Promise { - const kuduClient = await site.createClient(context); - const callback = context.deployMethod === 'flexconsumption' ? - async zipStream => { - return await kuduClient.flexDeploy(context, () => zipStream as RequestBodyType, { - remoteBuild: context.flexConsumptionRemoteBuild, - Deployer: publisherName - }); - } : - async zipStream => { - return await kuduClient.zipPushDeploy(context, () => zipStream as RequestBodyType, { - author: publisherName, - deployer: publisherName, - isAsync: true, - trackDeploymentId: true - }); - }; - - const response = await runWithZipStream(context, { - fsPath, site, pathFileMap, callback - }); - let locationUrl: string | undefined; - try { - if (response) { - context.telemetry.properties.deploymentId = response.headers.get('scm-deployment-id'); - locationUrl = response.headers.get('location'); - } - } catch (e) { - // swallow errors, we don't want a failure here to block deployment - } - - await waitForDeploymentToComplete(context, site, { locationUrl }); - - // /~https://github.com/Microsoft/vscode-azureappservice/issues/644 - // This delay is a temporary stopgap that should be resolved with the new pipelines - await delayFirstWebAppDeploy(context, site, aspPromise); -} diff --git a/appservice/src/deploy/runDeployTask.ts b/appservice/src/deploy/runDeployTask.ts index cde7c7b87f..096091090d 100644 --- a/appservice/src/deploy/runDeployTask.ts +++ b/appservice/src/deploy/runDeployTask.ts @@ -5,8 +5,8 @@ import { IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; -import { ext } from '../extensionVariables'; import { ScmType } from '../ScmType'; +import { ext } from '../extensionVariables'; import { taskUtils } from '../utils/taskUtils'; import { IDeployContext } from './IDeployContext'; @@ -44,33 +44,13 @@ export async function tryRunPreDeployTask(context: IDeployContext, deployFsPath: return preDeployTaskResult; } -/** - * Starts the post deploy task, but doesn't wait for the result (not worth it) - */ -export async function startPostDeployTask(context: IDeployContext, deployFsPath: string, scmType: string | undefined, resourceName: string): Promise { - const settingKey: string = 'postDeployTask'; - const taskName: string | undefined = vscode.workspace.getConfiguration(ext.prefix, vscode.Uri.file(deployFsPath)).get(settingKey); - context.telemetry.properties.hasPostDeployTask = String(!!taskName); - - if (taskName && shouldExecuteTask(context, scmType, settingKey, taskName)) { - const task: vscode.Task | undefined = await taskUtils.findTask(deployFsPath, taskName); - context.telemetry.properties.foundPostDeployTask = String(!!task); - if (task) { - await taskUtils.executeIfNotActive(task); - ext.outputChannel.appendLog(vscode.l10n.t('Started {0} "{1}".', settingKey, taskName), { resourceName }); - } else { - ext.outputChannel.appendLog(vscode.l10n.t('WARNING: Failed to find {0} "{1}".', settingKey, taskName), { resourceName }); - } - } -} - export interface IPreDeployTaskResult { taskName: string | undefined; exitCode: number | undefined; failedToFindTask: boolean; } -function shouldExecuteTask(context: IDeployContext, scmType: string | undefined, settingKey: string, taskName: string): boolean { +export function shouldExecuteTask(context: IDeployContext, scmType: string | undefined, settingKey: string, taskName: string): boolean { // We don't run deploy tasks for non-zipdeploy since that stuff should be handled by kudu const shouldExecute: boolean = context.deployMethod === 'storage' || context.deployMethod === 'zip' || (scmType !== ScmType.LocalGit && scmType !== ScmType.GitHub); if (!shouldExecute) { diff --git a/appservice/src/deploy/runWithZipStream.ts b/appservice/src/deploy/runWithZipStream.ts index 6470f9d1e8..a01d01b507 100644 --- a/appservice/src/deploy/runWithZipStream.ts +++ b/appservice/src/deploy/runWithZipStream.ts @@ -19,12 +19,15 @@ export async function runWithZipStream(context: IActionContext, options: { fsPath: string, site: ParsedSite, pathFileMap?: Map + progress?: vscode.Progress<{ message?: string; increment?: number }> callback: (zipStream: Readable) => Promise }): Promise { function onFileSize(size: number): void { context.telemetry.measurements.zipFileSize = size; - ext.outputChannel.appendLog(vscode.l10n.t('Zip package size: {0}', prettybytes(size)), { resourceName: site.fullName }); + const zipFileSize = vscode.l10n.t('Zip package size: {0}', prettybytes(size)); + ext.outputChannel.appendLog(zipFileSize, { resourceName: site.fullName }); + options.progress?.report({ message: zipFileSize }); } let zipStream: Readable; @@ -40,7 +43,10 @@ export async function runWithZipStream(context: IActionContext, options: { onFileSize(stats.size); }); } else { - ext.outputChannel.appendLog(vscode.l10n.t('Creating zip package...'), { resourceName: site.fullName }); + const creatingZip = vscode.l10n.t('Creating zip package...') + ext.outputChannel.appendLog(creatingZip, { resourceName: site.fullName }); + options.progress?.report({ message: creatingZip }); + const zipFile: yazl.ZipFile = new yazl.ZipFile(); let filesToZip: string[] = []; let sizeOfZipFile: number = 0; @@ -66,10 +72,12 @@ export async function runWithZipStream(context: IActionContext, options: { if (site.isFunctionApp) { filesToZip = await getFilesFromGitignore(fsPath, '.funcignore'); } else { - filesToZip = await getFilesFromGlob(fsPath, site.fullName); + filesToZip = await getFilesFromGlob(fsPath, site.fullName, options.progress); } + ext.outputChannel.appendLog(vscode.l10n.t('Adding {0} files to zip package...', filesToZip.length), { resourceName: site.fullName }); for (const file of filesToZip) { + ext.outputChannel.appendLog(path.join(fsPath, file), { resourceName: site.fullName }); zipFile.addFile(path.join(fsPath, file), getPathFromMap(file, pathFileMap)); } } else { @@ -90,7 +98,7 @@ function getPathFromMap(realPath: string, pathfileMap?: Map): st /** * Adds files using glob filtering */ -export async function getFilesFromGlob(folderPath: string, resourceName: string): Promise { +export async function getFilesFromGlob(folderPath: string, resourceName: string, progress?: vscode.Progress<{ message?: string; increment?: number }>): Promise { // App Service is the only extension with the zipIgnorePattern setting, so if ext.prefix is undefined, use 'appService' const zipDeployConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(ext.prefix ?? 'appService', vscode.Uri.file(folderPath)); const globPattern: string = zipDeployConfig.get('zipGlobPattern') || '**/*'; @@ -100,14 +108,17 @@ export async function getFilesFromGlob(folderPath: string, resourceName: string) // first find all files without any ignorePatterns let files: vscode.Uri[] = await vscode.workspace.findFiles(new vscode.RelativePattern(folderPath, globPattern)); + const ignoringFiles = vscode.l10n.t(`Ignoring files from \"{0}.{1}\"`, ext.prefix, zipIgnorePatternStr); if (ignorePatternList) { try { // not all ouptut channels _have_ to support appendLog, so catch the error - ext.outputChannel.appendLog(vscode.l10n.t(`Ignoring files from \"{0}.{1}\"`, ext.prefix, zipIgnorePatternStr), { resourceName }); + ext.outputChannel.appendLog(ignoringFiles, { resourceName }); } catch (error) { - ext.outputChannel.appendLine(vscode.l10n.t(`Ignoring files from \"{0}.{1}\"`, ext.prefix, zipIgnorePatternStr)); + ext.outputChannel.appendLine(ignoringFiles); } + progress?.report({ message: ignoringFiles }); + // if there is anything to ignore, accumulate a list of ignored files and take the union of the lists for (const pattern of ignorePatternList) { const filesIgnored = (await vscode.workspace.findFiles(globPattern, pattern)).map(uri => uri.fsPath); diff --git a/appservice/src/deploy/waitForDeploymentToComplete.ts b/appservice/src/deploy/waitForDeploymentToComplete.ts index 3d3363c0b2..50fa91e230 100644 --- a/appservice/src/deploy/waitForDeploymentToComplete.ts +++ b/appservice/src/deploy/waitForDeploymentToComplete.ts @@ -5,22 +5,23 @@ import { sendRequestWithTimeout } from '@microsoft/vscode-azext-azureutils'; import { IActionContext, IParsedError, nonNullProp, nonNullValue, parseError } from '@microsoft/vscode-azext-utils'; -import { CancellationToken, l10n, window } from 'vscode'; +import { CancellationToken, Progress, l10n, window } from 'vscode'; import type * as KuduModels from '../KuduModels'; import { ParsedSite, SiteClient } from '../SiteClient'; import { ext } from '../extensionVariables'; import { delay } from '../utils/delay'; import { ignore404Error, retryKuduCall } from '../utils/kuduUtils'; -import { IDeployContext } from './IDeployContext'; +import { InnerDeployContext } from './IDeployContext'; type DeploymentOptions = { expectedId?: string, token?: CancellationToken, pollingInterval?: number, locationUrl?: string + progress?: Progress<{ message?: string; increment?: number }>; } -export async function waitForDeploymentToComplete(context: IActionContext & Partial, site: ParsedSite, options: DeploymentOptions = {}): Promise { +export async function waitForDeploymentToComplete(context: IActionContext & Partial, site: ParsedSite, options: DeploymentOptions = {}): Promise { let fullLog: string = ''; let lastLogTime: Date = new Date(0); @@ -77,6 +78,7 @@ export async function waitForDeploymentToComplete(context: IActionContext & Part if (newEntry.message && newEntry.logTime && newEntry.logTime > lastLogTime) { fullLog = fullLog.concat(newEntry.message); + options.progress?.report({ message: newEntry.message }); ext.outputChannel.appendLog(newEntry.message, { date: newEntry.logTime, resourceName: site.fullName }); lastLogTimeForThisPoll = newEntry.logTime; if (/error/i.test(newEntry.message)) { diff --git a/appservice/src/deploy/wizard/DeployExecuteStepBase.ts b/appservice/src/deploy/wizard/DeployExecuteStepBase.ts new file mode 100644 index 0000000000..421e277a3a --- /dev/null +++ b/appservice/src/deploy/wizard/DeployExecuteStepBase.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AppServicePlan, SiteConfigResource } from "@azure/arm-appservice"; +import { ext } from "@microsoft/vscode-azext-github"; +import { AzureWizardExecuteStep, randomUtils } from "@microsoft/vscode-azext-utils"; +import { Progress, ProgressLocation, l10n, window } from "vscode"; +import { InnerDeployContext } from "../IDeployContext"; + +export abstract class DeployExecuteStepBase extends AzureWizardExecuteStep { + public priority: number = 200; + protected progress: Progress<{ message?: string; increment?: number }> | undefined; + public constructor() { + super(); + } + + public async execute(context: InnerDeployContext, progress: Progress<{ message?: string; increment?: number }>): Promise { + const client = context.client; + this.progress = progress; + const config: SiteConfigResource = await client.getSiteConfig(); + // We use the AppServicePlan in a few places, but we don't want to delay deployment, so start the promise now and save as a const + try { + await setDeploymentTelemetry(context, config, context.aspPromise); + } catch (error) { + // Ignore + } + + const title: string = l10n.t('Deploying to "{0}"... Check [output window](command:{1}) for status.', context.site.fullName, ext.prefix + '.showOutputChannel'); + await window.withProgress({ location: ProgressLocation.Notification, title }, async () => { + const startingDeployment = l10n.t('Starting deployment...') + + ext.outputChannel.appendLog(startingDeployment, { resourceName: context.site.fullName }); + progress.report({ message: startingDeployment }); + await this.deployCore(context, config); + }); + } + + public shouldExecute(_context: InnerDeployContext): boolean { + return true; + } + + public abstract deployCore(context: InnerDeployContext, config: SiteConfigResource): Promise; +} + +function getLinuxFxVersionForTelemetry(config: SiteConfigResource): string { + const linuxFxVersion = config.linuxFxVersion || ''; + // Docker values point to the user's specific image, which we don't want to track + return /^docker/i.test(linuxFxVersion) ? 'docker' : linuxFxVersion; +} + +async function setDeploymentTelemetry(context: InnerDeployContext, config: SiteConfigResource, aspPromise: Promise): Promise { + context.telemetry.properties.sourceHash = await randomUtils.getPseudononymousStringHash(context.workspaceFolder.uri.fsPath); + context.telemetry.properties.destHash = await randomUtils.getPseudononymousStringHash(context.site.fullName); + context.telemetry.properties.scmType = String(config.scmType); + context.telemetry.properties.isSlot = context.site.isSlot ? 'true' : 'false'; + context.telemetry.properties.alwaysOn = config.alwaysOn ? 'true' : 'false'; + context.telemetry.properties.linuxFxVersion = getLinuxFxVersionForTelemetry(config); + context.telemetry.properties.nodeVersion = String(config.nodeVersion); + context.telemetry.properties.pythonVersion = String(config.pythonVersion); + context.telemetry.properties.hasCors = config.cors ? 'true' : 'false'; + context.telemetry.properties.hasIpSecurityRestrictions = config.ipSecurityRestrictions && config.ipSecurityRestrictions.length > 0 ? 'true' : 'false'; + context.telemetry.properties.javaVersion = String(config.javaVersion); + context.telemetry.properties.siteKind = context.site.kind; + context.client.getState().then( + (state: string) => { + context.telemetry.properties.state = state; + }, + () => { + // ignore + }); + aspPromise.then( + (plan: AppServicePlan | undefined) => { + if (plan) { + context.telemetry.properties.planStatus = String(plan.status); + context.telemetry.properties.planKind = String(plan.kind); + if (plan.sku) { + context.telemetry.properties.planSize = String(plan.sku.size); + context.telemetry.properties.planTier = String(plan.sku.tier); + } + } + }, + () => { + // ignore + }); +} diff --git a/appservice/src/deploy/wizard/DeployGitHubExecuteStep.ts b/appservice/src/deploy/wizard/DeployGitHubExecuteStep.ts new file mode 100644 index 0000000000..c27dacf84a --- /dev/null +++ b/appservice/src/deploy/wizard/DeployGitHubExecuteStep.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { l10n } from "vscode"; +import { InnerDeployContext } from "../IDeployContext"; +import { DeployExecuteStepBase } from "./DeployExecuteStepBase"; + +export class DeployGitHubExecuteStep extends DeployExecuteStepBase { + public async deployCore(context: InnerDeployContext): Promise { + throw new Error(l10n.t('"{0}" is connected to a GitHub repository. Push to GitHub repository to deploy.', context.site.fullName)); + } +} diff --git a/appservice/src/deploy/wizard/DeployLocalGitExecuteStep.ts b/appservice/src/deploy/wizard/DeployLocalGitExecuteStep.ts new file mode 100644 index 0000000000..987a7024c5 --- /dev/null +++ b/appservice/src/deploy/wizard/DeployLocalGitExecuteStep.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InnerDeployContext } from "../IDeployContext"; +import { localGitDeploy } from "../localGitDeploy"; +import { DeployExecuteStepBase } from "./DeployExecuteStepBase"; + +export class DeployLocalGitExecuteStep extends DeployExecuteStepBase { + public async deployCore(context: InnerDeployContext): Promise { + await localGitDeploy(context.site, { fsPath: context.workspaceFolder.uri.fsPath }, context); + } +} diff --git a/appservice/src/deploy/wizard/PostDeploySyncTriggersExecuteStep.ts b/appservice/src/deploy/wizard/PostDeploySyncTriggersExecuteStep.ts new file mode 100644 index 0000000000..bdbacd9e77 --- /dev/null +++ b/appservice/src/deploy/wizard/PostDeploySyncTriggersExecuteStep.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; +import { InnerDeployContext } from "../IDeployContext"; +import { syncTriggersPostDeploy } from "../syncTriggersPostDeploy"; + +export class PostDeploySyncTriggersExecuteStep extends AzureWizardExecuteStep { + public priority: number = 310; + public async execute(context: InnerDeployContext): Promise { + // Don't sync triggers if app is stopped /~https://github.com/microsoft/vscode-azurefunctions/issues/1608 + const state: string | undefined = await context.client.getState(); + if (state?.toLowerCase() === 'running') { + await syncTriggersPostDeploy(context, context.site); + } + } + + public shouldExecute(_context: InnerDeployContext): boolean { + return true; + } +} diff --git a/appservice/src/deploy/wizard/PostDeployTaskExecuteStep.ts b/appservice/src/deploy/wizard/PostDeployTaskExecuteStep.ts new file mode 100644 index 0000000000..a994afb398 --- /dev/null +++ b/appservice/src/deploy/wizard/PostDeployTaskExecuteStep.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SiteConfig } from "@azure/arm-appservice"; +import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; +import { Progress, Task, Uri, l10n, workspace } from "vscode"; +import { ext } from "../../extensionVariables"; +import { taskUtils } from "../../utils/taskUtils"; +import { InnerDeployContext } from "../IDeployContext"; +import { shouldExecuteTask } from "../runDeployTask"; + +export class PostDeployTaskExecuteStep extends AzureWizardExecuteStep { + public priority: number = 300; + public constructor(readonly config: SiteConfig) { + super(); + } + + public async execute(context: InnerDeployContext, progress: Progress<{ message?: string; increment?: number }>): Promise { + const settingKey: string = 'postDeployTask'; + const taskName: string | undefined = workspace.getConfiguration(ext.prefix, Uri.file(context.fsPath)).get(settingKey); + context.telemetry.properties.hasPostDeployTask = String(!!taskName); + + if (taskName && shouldExecuteTask(context, this.config.scmType, settingKey, taskName)) { + const task: Task | undefined = await taskUtils.findTask(context.fsPath, taskName); + context.telemetry.properties.foundPostDeployTask = String(!!task); + if (task) { + await taskUtils.executeIfNotActive(task); + const startedTask = l10n.t('Started {0} "{1}".', settingKey, taskName); + progress.report({ message: startedTask }); + ext.outputChannel.appendLog(startedTask, { resourceName: context.site.fullName }); + } else { + const failedToFindTask = l10n.t('Failed to find {0} "{1}".', settingKey, taskName); + progress.report({ message: failedToFindTask }); + ext.outputChannel.appendLog(failedToFindTask, { resourceName: context.site.fullName }); + } + } + } + + public shouldExecute(_context: InnerDeployContext): boolean { + return true; + } +} diff --git a/appservice/src/deploy/wizard/StartAppAfterDeployExecuteStep.ts b/appservice/src/deploy/wizard/StartAppAfterDeployExecuteStep.ts new file mode 100644 index 0000000000..8b7400d0e1 --- /dev/null +++ b/appservice/src/deploy/wizard/StartAppAfterDeployExecuteStep.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ext } from "@microsoft/vscode-azext-github"; +import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; +import { Progress, l10n } from "vscode"; +import { InnerDeployContext } from "../IDeployContext"; + +export class StartAppAfterDeployExecuteStep extends AzureWizardExecuteStep { + public priority: number = 900; + public async execute(context: InnerDeployContext, progress: Progress<{ message?: string; increment?: number }>): Promise { + const site = context.site; + + const client = await site.createClient(context); + const startingApp = l10n.t('Starting app...'); + progress.report({ message: startingApp }); + ext.outputChannel.appendLog(startingApp, { resourceName: site.fullName }); + await client.start(); + } + + public shouldExecute(_context: InnerDeployContext): boolean { + return true; + } +} diff --git a/appservice/src/deploy/wizard/StopAppBeforeDeployExecuteStep.ts b/appservice/src/deploy/wizard/StopAppBeforeDeployExecuteStep.ts new file mode 100644 index 0000000000..e7148cb4ba --- /dev/null +++ b/appservice/src/deploy/wizard/StopAppBeforeDeployExecuteStep.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ext } from "@microsoft/vscode-azext-github"; +import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; +import { Progress, l10n } from "vscode"; +import { InnerDeployContext } from "../IDeployContext"; + +export class StopAppBeforeDeployExecuteStep extends AzureWizardExecuteStep { + public priority: number = 100; + public async execute(context: InnerDeployContext, progress: Progress<{ message?: string; increment?: number }>): Promise { + const site = context.site; + const client = await site.createClient(context); + + const stoppingApp = l10n.t('Stopping app...'); + progress.report({ message: stoppingApp }); + ext.outputChannel.appendLog(stoppingApp, { resourceName: site.fullName }); + await client.stop(); + } + + public shouldExecute(_context: InnerDeployContext): boolean { + return true; + } +} diff --git a/appservice/src/deploy/wizard/createDeployWizard.ts b/appservice/src/deploy/wizard/createDeployWizard.ts new file mode 100644 index 0000000000..14df0666ee --- /dev/null +++ b/appservice/src/deploy/wizard/createDeployWizard.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SiteConfigResource } from "@azure/arm-appservice"; +import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; +import { ScmType } from "../../ScmType"; +import { InnerDeployContext } from "../IDeployContext"; +import { DeployGitHubExecuteStep } from "./DeployGitHubExecuteStep"; +import { DeployLocalGitExecuteStep } from "./DeployLocalGitExecuteStep"; +import { PostDeploySyncTriggersExecuteStep } from "./PostDeploySyncTriggersExecuteStep"; +import { PostDeployTaskExecuteStep } from "./PostDeployTaskExecuteStep"; +import { StartAppAfterDeployExecuteStep } from "./StartAppAfterDeployExecuteStep"; +import { StopAppBeforeDeployExecuteStep } from "./StopAppBeforeDeployExecuteStep"; +import { DelayFirstWebAppDeployStep } from "./deployZip/DelayFirstWebAppDeployStep"; +import { DeployFlexExecuteStep } from "./deployZip/DeployFlexExecuteStep"; +import { DeployStorageAccountExecuteStep } from "./deployZip/DeployStorageAccountExecuteStep"; +import { DeployWarExecuteStep } from "./deployZip/DeployWarExecuteStep"; +import { DeployZipPushExecuteStep } from "./deployZip/DeployZipPushExecuteStep"; +import { WaitForDeploymentToCompleteStep } from "./deployZip/WaitForDeploymentToCompleteStep"; +import path = require("path"); + +export async function createDeployExecuteSteps(context: InnerDeployContext): Promise[]> { + const executeSteps: AzureWizardExecuteStep[] = []; + const config: SiteConfigResource = await context.client.getSiteConfig(); + + if (context.stopAppBeforeDeploy) { + executeSteps.push(new StopAppBeforeDeployExecuteStep(), new StartAppAfterDeployExecuteStep()); + } + + if (!context.deployMethod && config.scmType === ScmType.GitHub) { + executeSteps.push(new DeployGitHubExecuteStep()); + } else { + // deployments that are handled by kudu + if (!context.deployMethod && config.scmType === ScmType.LocalGit) { + executeSteps.push(new DeployLocalGitExecuteStep()); + } else { + // all zip-based deployments + const javaRuntime = context.site.isLinux ? config.linuxFxVersion : config.javaContainer; + if (javaRuntime && /^(tomcat|wildfly|jboss)/i.test(javaRuntime)) { + executeSteps.push(new DeployWarExecuteStep()); + } else if (javaRuntime && /^java/i.test(javaRuntime) && !context.site.isFunctionApp) { + const pathFileMap = new Map([ + [path.basename(context.workspaceFolder.uri.fsPath), 'app.jar'] + ]); + executeSteps.push(new DeployZipPushExecuteStep(pathFileMap)); + } else if (context.deployMethod === 'storage') { + executeSteps.push(new DeployStorageAccountExecuteStep()); + } else if (context.deployMethod === 'flexconsumption') { + executeSteps.push(new DeployFlexExecuteStep()); + } else { + executeSteps.push(new DeployZipPushExecuteStep()); + } + executeSteps.push(new DelayFirstWebAppDeployStep()); + } + + executeSteps.push(new WaitForDeploymentToCompleteStep()); + } + + executeSteps.push(new PostDeployTaskExecuteStep(config)) + if (context.syncTriggersPostDeploy) { + executeSteps.push(new PostDeploySyncTriggersExecuteStep()); + } + + return executeSteps; +} diff --git a/appservice/src/deploy/wizard/deployZip/DelayFirstWebAppDeployStep.ts b/appservice/src/deploy/wizard/deployZip/DelayFirstWebAppDeployStep.ts new file mode 100644 index 0000000000..5163f22c30 --- /dev/null +++ b/appservice/src/deploy/wizard/deployZip/DelayFirstWebAppDeployStep.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { AppServicePlan } from '@azure/arm-appservice'; +import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; +import { InnerDeployContext } from '../../IDeployContext'; + +export class DelayFirstWebAppDeployStep extends AzureWizardExecuteStep { + public priority: number = 300; + public constructor() { + super(); + } + + public async execute(context: InnerDeployContext): Promise { + // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor + await new Promise(async (resolve: () => void): Promise => { + setTimeout(resolve, 10000); + try { + // this delay is only valid for the first deployment to a Linux web app on a basic asp, so resolve for anything else + if (context.site.isFunctionApp) { + resolve(); + } + + const asp: AppServicePlan | undefined = await context.aspPromise; + if (!asp || !asp.sku || !asp.sku.tier || asp.sku.tier.toLowerCase() !== 'basic') { + resolve(); + } + if (!context.site.isLinux) { + resolve(); + } + + const deployments: number = (await context.client.getDeployResults(context)).length; + if (deployments > 1) { + resolve(); + } + } catch (error) { + // ignore the error, an error here isn't a deployment failure + resolve(); + } + }); + } + + public shouldExecute(_context: InnerDeployContext): boolean { + return true; + } +} diff --git a/appservice/src/deploy/wizard/deployZip/DeployFlexExecuteStep.ts b/appservice/src/deploy/wizard/deployZip/DeployFlexExecuteStep.ts new file mode 100644 index 0000000000..4a3be17d92 --- /dev/null +++ b/appservice/src/deploy/wizard/deployZip/DeployFlexExecuteStep.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RequestBodyType } from "@azure/core-rest-pipeline"; +import { AzExtPipelineResponse } from "@microsoft/vscode-azext-azureutils"; +import { publisherName } from "../../../constants"; +import { InnerDeployContext } from "../../IDeployContext"; +import { runWithZipStream } from "../../runWithZipStream"; +import { DeployZipBaseExecuteStep } from "./DeployZipBaseExecuteStep"; + +export class DeployFlexExecuteStep extends DeployZipBaseExecuteStep { + public async deployZip(context: InnerDeployContext): Promise { + const kuduClient = await context.site.createClient(context); + const callback = async zipStream => { + return await kuduClient.flexDeploy(context, () => zipStream as RequestBodyType, { + remoteBuild: context.flexConsumptionRemoteBuild, + Deployer: publisherName + }); + }; + + return await runWithZipStream(context, { + fsPath: context.workspaceFolder.uri.fsPath, + site: context.site, + pathFileMap: this.pathFileMap, + callback, + progress: this.progress + }); + } +} diff --git a/appservice/src/deploy/wizard/deployZip/DeployStorageAccountExecuteStep.ts b/appservice/src/deploy/wizard/deployZip/DeployStorageAccountExecuteStep.ts new file mode 100644 index 0000000000..9560cb2544 --- /dev/null +++ b/appservice/src/deploy/wizard/deployZip/DeployStorageAccountExecuteStep.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InnerDeployContext } from "../../IDeployContext"; +import { deployToStorageAccount } from "../../deployToStorageAccount"; +import { DeployZipBaseExecuteStep } from "./DeployZipBaseExecuteStep"; + +export class DeployStorageAccountExecuteStep extends DeployZipBaseExecuteStep { + public async deployZip(context: InnerDeployContext): Promise { + return await deployToStorageAccount(context, context.fsPath, context.site); + } +} diff --git a/appservice/src/deploy/wizard/deployZip/DeployWarExecuteStep.ts b/appservice/src/deploy/wizard/deployZip/DeployWarExecuteStep.ts new file mode 100644 index 0000000000..66579f8f48 --- /dev/null +++ b/appservice/src/deploy/wizard/deployZip/DeployWarExecuteStep.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import { l10n } from "vscode"; +import { publisherName } from '../../../constants'; +import { getFileExtension } from "../../../utils/pathUtils"; +import { InnerDeployContext } from "../../IDeployContext"; +import { DeployZipBaseExecuteStep } from "./DeployZipBaseExecuteStep"; + +export class DeployWarExecuteStep extends DeployZipBaseExecuteStep { + public async deployZip(context: InnerDeployContext): Promise { + if (getFileExtension(context.workspaceFolder.uri.fsPath) !== 'war') { + throw new Error(l10n.t('Path specified is not a war file')); + } + + const kuduClient = await context.site.createClient(context); + await kuduClient.warPushDeploy(context, () => fs.createReadStream(context.workspaceFolder.uri.fsPath), { + isAsync: true, + author: publisherName, + deployer: publisherName, + trackDeploymentId: true + }); + } +} diff --git a/appservice/src/deploy/wizard/deployZip/DeployZipBaseExecuteStep.ts b/appservice/src/deploy/wizard/deployZip/DeployZipBaseExecuteStep.ts new file mode 100644 index 0000000000..7416d6325e --- /dev/null +++ b/appservice/src/deploy/wizard/deployZip/DeployZipBaseExecuteStep.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzExtPipelineResponse } from "@microsoft/vscode-azext-azureutils"; +import { AzExtFsExtra } from "@microsoft/vscode-azext-utils"; +import { l10n } from "vscode"; +import { InnerDeployContext } from "../../IDeployContext"; +import { DeployExecuteStepBase } from "../DeployExecuteStepBase"; + + +export abstract class DeployZipBaseExecuteStep extends DeployExecuteStepBase { + public constructor(readonly pathFileMap?: Map) { + super(); + } + + public async deployCore(context: InnerDeployContext): Promise { + const fsPath = context.workspaceFolder.uri.fsPath; + if (!(await AzExtFsExtra.pathExists(fsPath))) { + throw new Error(l10n.t('Failed to deploy path that does not exist: {0}', fsPath)); + } + const response = await this.deployZip(context); + try { + if (response) { + context.telemetry.properties.deploymentId = response.headers.get('scm-deployment-id'); + context.locationUrl = response.headers.get('location'); + } + } catch (e) { + // swallow errors, we don't want a failure here to block deployment + } + } + + protected abstract deployZip(context: InnerDeployContext): Promise; +} diff --git a/appservice/src/deploy/wizard/deployZip/DeployZipPushExecuteStep.ts b/appservice/src/deploy/wizard/deployZip/DeployZipPushExecuteStep.ts new file mode 100644 index 0000000000..7f2d179fdd --- /dev/null +++ b/appservice/src/deploy/wizard/deployZip/DeployZipPushExecuteStep.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RequestBodyType } from "@azure/core-rest-pipeline"; +import { AzExtPipelineResponse } from "@microsoft/vscode-azext-azureutils"; +import { publisherName } from "../../../constants"; +import { InnerDeployContext } from "../../IDeployContext"; +import { runWithZipStream } from "../../runWithZipStream"; +import { DeployZipBaseExecuteStep } from "./DeployZipBaseExecuteStep"; + +export class DeployZipPushExecuteStep extends DeployZipBaseExecuteStep { + public async deployZip(context: InnerDeployContext): Promise { + const kuduClient = await context.site.createClient(context); + const callback = async zipStream => { + return await kuduClient.zipPushDeploy(context, () => zipStream as RequestBodyType, { + author: publisherName, + deployer: publisherName, + isAsync: true, + trackDeploymentId: true + }); + }; + + return await runWithZipStream(context, { + fsPath: context.workspaceFolder.uri.fsPath, + site: context.site, + pathFileMap: this.pathFileMap, + callback, + progress: this.progress + }); + } +} diff --git a/appservice/src/deploy/wizard/deployZip/WaitForDeploymentToCompleteStep.ts b/appservice/src/deploy/wizard/deployZip/WaitForDeploymentToCompleteStep.ts new file mode 100644 index 0000000000..56e53f3826 --- /dev/null +++ b/appservice/src/deploy/wizard/deployZip/WaitForDeploymentToCompleteStep.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils"; +import { Progress } from "vscode"; +import { InnerDeployContext } from "../../IDeployContext"; +import { waitForDeploymentToComplete } from "../../waitForDeploymentToComplete"; + +export class WaitForDeploymentToCompleteStep extends AzureWizardExecuteStep { + public priority: number = 210; + public async execute(context: InnerDeployContext, progress: Progress<{ message?: string; increment?: number }>): Promise { + return await waitForDeploymentToComplete(context, nonNullProp(context, 'site'), { locationUrl: context.locationUrl, progress }); + } + + public shouldExecute(_context: InnerDeployContext): boolean { + return true; + } +}