Merge branch 'main' into user/winstonliu/upstream-language-server
This commit is contained in:
Коммит
82e7f68cee
|
@ -35,8 +35,8 @@ steps:
|
|||
|
||||
# Acquire the `vsce` tool and use it to package
|
||||
- script: |
|
||||
npm install -g vsce
|
||||
vsce package --githubBranch main
|
||||
npm install -g @vscode/vsce
|
||||
vsce package
|
||||
displayName: Create VSIX
|
||||
|
||||
- script: |
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
coverage
|
||||
dist
|
||||
out
|
||||
node_modules
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// @ts-check
|
||||
|
||||
import { defineConfig } from '@vscode/test-cli';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'out/test/**/*.test.js',
|
||||
workspaceFolder: path.join(import.meta.dirname, 'src', 'test', 'workspace'),
|
||||
mocha: {
|
||||
timeout: 100000,
|
||||
},
|
||||
coverage: {
|
||||
reporter: ['cobertura', 'text', 'html'],
|
||||
output: './coverage',
|
||||
}
|
||||
});
|
|
@ -2,6 +2,7 @@
|
|||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.extension-test-runner"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"name": "Extension Tests",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"testConfiguration": "${workspaceFolder}/.vscode-test.js",
|
||||
"args": [
|
||||
"${workspaceFolder}/src/test/workspace",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.vscode-test.mjs
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
webpack.config.js
|
||||
.azure-pipelines/
|
||||
.vscode/
|
||||
.vscode-test/
|
||||
coverage/
|
||||
dist/**/*.map
|
||||
examples/
|
||||
node_modules/
|
||||
|
|
|
@ -3,6 +3,12 @@ All notable changes to the Azure Pipelines extension will be documented in this
|
|||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/). Versioning follows an internal Azure DevOps format that is not compatible with SemVer.
|
||||
|
||||
## 1.237.0
|
||||
### Added
|
||||
- Added go-to-definition support for local templates (thanks @Stuart-Wilcox!)
|
||||
### Updated
|
||||
- M235 schema
|
||||
|
||||
## 1.228.0
|
||||
### Added
|
||||
- Added support for using [1ES Pipeline Template schema Intellisense](https://aka.ms/1espt) for users working on pipelines extending 1ES Pipeline Templates. This feature is available for users with `@microsoft.com` account only.
|
||||
|
|
28
README.md
28
README.md
|
@ -56,38 +56,10 @@ Add this to your `settings.json`:
|
|||
|
||||
Both format on save and the `Format document` command should now work!
|
||||
|
||||
## Pipeline configuration
|
||||
|
||||
![Configure Pipeline Demo](https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/main/resources/configure-pipeline.gif)
|
||||
|
||||
To set up a pipeline, choose *Azure Pipelines: Configure Pipeline* from the command palette (Ctrl/Cmd + Shift + P) or right-click in the file explorer. The guided workflow will generate a starter YAML file defining the build and deploy process.
|
||||
|
||||
You can customize the pipeline using all the features offered by [Azure Pipelines.](https://azure.microsoft.com/services/devops/pipelines/).
|
||||
|
||||
Once the setup is completed, an automatic CI/CD trigger will fire for every code push. To set this up, the extension will ask for a GitHub PAT with *repo* and *admin:repo_hook* scope.
|
||||
|
||||
![GitHub PAT scope](resources/gitHubPatScope.png)
|
||||
|
||||
## Telemetry
|
||||
|
||||
VS Code collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](https://go.microsoft.com/fwlink/?LinkID=528096&clcid=0x409) to learn more. If you don’t wish to send usage data to Microsoft, you can set the `telemetry.enableTelemetry` setting to `false`. Learn more in our [FAQ](https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting).
|
||||
|
||||
## Troubleshooting failures
|
||||
|
||||
- **Selected workspace is not a Git repository**: You can configure a pipeline for a Git repository backed by GitHub or Azure Repos. Initialize your workspace as a Git repo, commit your files, and add a remote to GitHub or Azure Repos. Run the following commands to configure git repository:
|
||||
|
||||
`git init`
|
||||
|
||||
`git add *`
|
||||
|
||||
`git commit -m <commit-message>`
|
||||
|
||||
`git remote add <remote-name> <remote-url>`
|
||||
|
||||
- **The current branch doesn't have a tracking branch, and the selected repository has no remotes**: You can configure a pipeline for a Git repository backed by GitHub or Azure Repos. To add a new remote Git repository, run `git remote add <remote-name> <remote-url>`
|
||||
|
||||
- **Failed to determine Azure Repo details from remote url**: If you're configuring a pipeline for a Git repository backed by Azure Repos, ensure that it has a remote pointing to a valid Azure Repos Git repo URL.
|
||||
|
||||
## Extension Development
|
||||
|
||||
If you are only working on the extension (i.e. syntax highlighting, configure pipeline, and the language client):
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
58
package.json
58
package.json
|
@ -2,7 +2,7 @@
|
|||
"name": "azure-pipelines",
|
||||
"displayName": "Azure Pipelines",
|
||||
"description": "Syntax highlighting, IntelliSense, and more for Azure Pipelines YAML",
|
||||
"version": "1.228.0",
|
||||
"version": "1.237.0",
|
||||
"publisher": "ms-azure-devops",
|
||||
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
|
||||
"repository": {
|
||||
|
@ -18,8 +18,7 @@
|
|||
"theme": "light"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.64.0",
|
||||
"node": ">=12.20.0"
|
||||
"vscode": "^1.82.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
|
@ -37,10 +36,6 @@
|
|||
"continuous integration",
|
||||
"CI/CD"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage:azure-pipelines",
|
||||
"onCommand:azure-pipelines.configure-pipeline"
|
||||
],
|
||||
"main": "./dist/extension",
|
||||
"capabilities": {
|
||||
"untrustedWorkspaces": {
|
||||
|
@ -83,12 +78,6 @@
|
|||
"configuration": {
|
||||
"title": "Azure Pipelines",
|
||||
"properties": {
|
||||
"azure-pipelines.configure": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable 'Configure Pipeline' feature",
|
||||
"order": 0
|
||||
},
|
||||
"azure-pipelines.1ESPipelineTemplatesSchemaFile": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
|
@ -115,26 +104,12 @@
|
|||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "azure-pipelines.configure-pipeline",
|
||||
"title": "Configure Pipeline",
|
||||
"category": "Azure Pipelines"
|
||||
},
|
||||
{
|
||||
"command": "azure-pipelines.reset-state",
|
||||
"title": "Reset 'do not ask again' messages",
|
||||
"category": "Azure Pipelines"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"explorer/context": [
|
||||
{
|
||||
"command": "azure-pipelines.configure-pipeline",
|
||||
"group": "Azure Pipelines",
|
||||
"when": "explorerResourceIsFolder == true"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
|
@ -143,22 +118,17 @@
|
|||
"compile:test": "tsc --project ./tsconfig.test.json",
|
||||
"lint": "eslint .",
|
||||
"watch": "webpack --mode development --progress --color --watch",
|
||||
"test": "npm run compile:test && node ./out/test/runTest.js"
|
||||
"test": "npm run compile:test && vscode-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/html-to-text": "^5.1.2",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/mustache": "0.8.32",
|
||||
"@types/node": "^14.16.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/vscode": "~1.64.0",
|
||||
"@types/node": "~20.15.0",
|
||||
"@types/vscode": "~1.82.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"@vscode/test-electron": "^1.6.2",
|
||||
"copy-webpack-plugin": "^10.2.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"eslint": "^8.54.0",
|
||||
"glob": "^7.1.6",
|
||||
"mocha": "^9.1.1",
|
||||
"ts-loader": "^8.0.14",
|
||||
"typescript": "~5.2.2",
|
||||
|
@ -166,20 +136,10 @@
|
|||
"webpack-cli": "^4.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/arm-appservice": "^6.1.0",
|
||||
"@azure/arm-subscriptions": "^3.0.0",
|
||||
"@azure/ms-rest-azure-env": "^2.0.0",
|
||||
"@azure/ms-rest-nodeauth": "^3.0.6",
|
||||
"@vscode/extension-telemetry": "^0.5.1",
|
||||
"azure-devops-node-api": "^11.0.1",
|
||||
"azure-pipelines-language-server": "0.8.0",
|
||||
"html-to-text": "^5.1.1",
|
||||
"mustache": "^4.2.0",
|
||||
"uuid": "^8.3.2",
|
||||
"vscode-languageclient": "^7.0.0",
|
||||
"vscode-uri": "^3.0.2"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"ms-vscode.azure-account"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Двоичные данные
resources/configure-pipeline.gif
Двоичные данные
resources/configure-pipeline.gif
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 586 KiB |
Двоичные данные
resources/gitHubPatScope.png
Двоичные данные
resources/gitHubPatScope.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 83 KiB |
7334
service-schema.json
7334
service-schema.json
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,55 @@
|
|||
import { ConnectionData } from 'azure-devops-node-api/interfaces/LocationsInterfaces';
|
||||
|
||||
import { telemetryHelper, extensionVersion } from '../../helpers/telemetryHelper';
|
||||
|
||||
export interface Organization {
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
accountUri: string;
|
||||
properties: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class OrganizationsClient {
|
||||
private organizations?: Organization[];
|
||||
|
||||
constructor(private token: string) { }
|
||||
|
||||
public async listOrganizations(forceRefresh?: boolean): Promise<Organization[]> {
|
||||
if (this.organizations && !forceRefresh) {
|
||||
return this.organizations;
|
||||
}
|
||||
|
||||
const { authenticatedUser } = await this.fetch<ConnectionData>("https://app.vssps.visualstudio.com/_apis/connectiondata");
|
||||
if (authenticatedUser === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { value: organizations } = await this.fetch<{ value: Organization[] }>(`https://app.vssps.visualstudio.com/_apis/accounts?memberId=${authenticatedUser.id}&api-version=7.0`);
|
||||
this.organizations = organizations.sort((org1, org2) => {
|
||||
const account1 = org1.accountName.toLowerCase();
|
||||
const account2 = org2.accountName.toLowerCase();
|
||||
if (account1 < account2) {
|
||||
return -1;
|
||||
} else if (account1 > account2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return this.organizations;
|
||||
}
|
||||
|
||||
private async fetch<T>(...[request, init]: Parameters<typeof fetch>): Promise<T> {
|
||||
const response = await fetch(request, {
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': `azure-pipelines-vscode ${extensionVersion}`,
|
||||
'X-TFS-Session': telemetryHelper.getJourneyId(),
|
||||
}
|
||||
});
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
import { configurePipeline } from './configure';
|
||||
import { telemetryHelper } from '../helpers/telemetryHelper';
|
||||
|
||||
export function activateConfigurePipeline(): void {
|
||||
vscode.commands.registerCommand('azure-pipelines.configure-pipeline', async () => {
|
||||
await telemetryHelper.callWithTelemetryAndErrorHandling('azurePipelines.configure-pipeline', async () => {
|
||||
await configurePipeline();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import { WebSiteManagementClient, WebSiteManagementModels } from '@azure/arm-appservice';
|
||||
import { TokenCredentialsBase } from '@azure/ms-rest-nodeauth';
|
||||
|
||||
import { WebAppKind, ValidatedSite } from '../../model/models';
|
||||
|
||||
export class AppServiceClient {
|
||||
private webSiteManagementClient: WebSiteManagementClient;
|
||||
|
||||
constructor(credentials: TokenCredentialsBase, subscriptionId: string) {
|
||||
this.webSiteManagementClient = new WebSiteManagementClient(credentials, subscriptionId);
|
||||
}
|
||||
|
||||
public async getAppServices(filterForResourceKind: WebAppKind): Promise<WebSiteManagementModels.Site[]> {
|
||||
const sites = await this.webSiteManagementClient.webApps.list();
|
||||
return sites.filter(site => site.kind === filterForResourceKind);
|
||||
}
|
||||
|
||||
public async getAppServiceConfig(site: ValidatedSite): Promise<WebSiteManagementModels.SiteConfigResource> {
|
||||
return this.webSiteManagementClient.webApps.getConfiguration(site.resourceGroup, site.name);
|
||||
}
|
||||
|
||||
public async updateScmType(site: ValidatedSite): Promise<WebSiteManagementModels.SiteConfigResource> {
|
||||
const siteConfig = await this.getAppServiceConfig(site);
|
||||
siteConfig.scmType = ScmType.VSTSRM;
|
||||
return this.webSiteManagementClient.webApps.updateConfiguration(site.resourceGroup, site.name, siteConfig);
|
||||
}
|
||||
|
||||
public async getAppServiceMetadata(site: ValidatedSite): Promise<WebSiteManagementModels.StringDictionary> {
|
||||
return this.webSiteManagementClient.webApps.listMetadata(site.resourceGroup, site.name);
|
||||
}
|
||||
|
||||
public async updateAppServiceMetadata(site: ValidatedSite, metadata: WebSiteManagementModels.StringDictionary): Promise<WebSiteManagementModels.StringDictionary> {
|
||||
return this.webSiteManagementClient.webApps.updateMetadata(site.resourceGroup, site.name, metadata);
|
||||
}
|
||||
|
||||
public async publishDeploymentToAppService(site: ValidatedSite, buildDefinitionUrl: string, releaseDefinitionUrl: string, triggeredBuildUrl: string): Promise<WebSiteManagementModels.Deployment> {
|
||||
// create deployment object
|
||||
const deploymentId = uuid();
|
||||
const deployment = this.createDeploymentObject(deploymentId, buildDefinitionUrl, releaseDefinitionUrl, triggeredBuildUrl);
|
||||
return this.webSiteManagementClient.webApps.createDeployment(site.resourceGroup, site.name, deploymentId, deployment);
|
||||
}
|
||||
|
||||
private createDeploymentObject(deploymentId: string, buildDefinitionUrl: string, releaseDefinitionUrl: string, triggeredBuildUrl: string): WebSiteManagementModels.Deployment {
|
||||
const message: DeploymentMessage = {
|
||||
type: "CDDeploymentConfiguration",
|
||||
message: "Successfully set up continuous delivery from VS Code and triggered deployment to Azure Web App.",
|
||||
VSTSRM_BuildDefinitionWebAccessUrl: buildDefinitionUrl,
|
||||
VSTSRM_ConfiguredCDEndPoint: '',
|
||||
VSTSRM_BuildWebAccessUrl: triggeredBuildUrl,
|
||||
};
|
||||
|
||||
return {
|
||||
id: deploymentId,
|
||||
status: 4,
|
||||
author: 'VSTS',
|
||||
deployer: 'VSTS',
|
||||
message: JSON.stringify(message),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export enum ScmType {
|
||||
VSTSRM = 'VSTSRM',
|
||||
NONE = 'NONE'
|
||||
}
|
||||
|
||||
interface DeploymentMessage {
|
||||
type: string;
|
||||
message: string;
|
||||
VSTSRM_BuildDefinitionWebAccessUrl: string;
|
||||
VSTSRM_ConfiguredCDEndPoint: string;
|
||||
VSTSRM_BuildWebAccessUrl: string;
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
import { RequestPrepareOptions } from '@azure/ms-rest-js';
|
||||
import { TokenCredentialsBase } from '@azure/ms-rest-nodeauth';
|
||||
import { ConnectionData } from 'azure-devops-node-api/interfaces/LocationsInterfaces';
|
||||
|
||||
import { RestClient } from '../restClient';
|
||||
import { Organization } from '../../model/models';
|
||||
import { telemetryHelper } from '../../../helpers/telemetryHelper';
|
||||
|
||||
export class OrganizationsClient {
|
||||
private restClient: RestClient;
|
||||
private organizations?: Organization[];
|
||||
|
||||
constructor(credentials: TokenCredentialsBase) {
|
||||
this.restClient = new RestClient(credentials);
|
||||
}
|
||||
|
||||
public async sendRequest<T>(requestPrepareOptions: RequestPrepareOptions): Promise<T> {
|
||||
if (requestPrepareOptions.headers) {
|
||||
requestPrepareOptions.headers['X-TFS-Session'] = telemetryHelper.getJourneyId();
|
||||
}
|
||||
else {
|
||||
requestPrepareOptions.headers = { 'X-TFS-Session': telemetryHelper.getJourneyId() };
|
||||
}
|
||||
|
||||
return this.restClient.sendRequest<T>(requestPrepareOptions);
|
||||
}
|
||||
|
||||
public async listOrganizations(forceRefresh?: boolean): Promise<Organization[]> {
|
||||
if (this.organizations && !forceRefresh) {
|
||||
return this.organizations;
|
||||
}
|
||||
|
||||
const { authenticatedUser } = await this.getUserData();
|
||||
if (authenticatedUser === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const response = await this.sendRequest<{ value: Organization[] }>({
|
||||
url: "https://app.vssps.visualstudio.com/_apis/accounts",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "GET",
|
||||
queryParameters: {
|
||||
"memberId": authenticatedUser.id,
|
||||
"api-version": "7.0",
|
||||
},
|
||||
});
|
||||
|
||||
this.organizations = response.value.sort((org1, org2) => {
|
||||
const account1 = org1.accountName.toLowerCase();
|
||||
const account2 = org2.accountName.toLowerCase();
|
||||
if (account1 < account2) {
|
||||
return -1;
|
||||
} else if (account1 > account2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return this.organizations;
|
||||
}
|
||||
|
||||
private async getUserData(): Promise<ConnectionData> {
|
||||
try {
|
||||
return this.getConnectionData();
|
||||
} catch {
|
||||
await this.createUserProfile();
|
||||
return this.getConnectionData();
|
||||
}
|
||||
}
|
||||
|
||||
private getConnectionData(): Promise<ConnectionData> {
|
||||
return this.sendRequest({
|
||||
url: "https://app.vssps.visualstudio.com/_apis/connectiondata",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Need to verify this signature
|
||||
private createUserProfile(): Promise<void> {
|
||||
return this.sendRequest({
|
||||
url: "https://app.vssps.visualstudio.com/_apis/_AzureProfile/CreateProfile",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
import * as azdev from 'azure-devops-node-api';
|
||||
import { AadApplication } from '../../model/models';
|
||||
|
||||
// Definitive interface at https://github.com/microsoft/azure-devops-node-api/blob/master/api/interfaces/ServiceEndpointInterfaces.ts,
|
||||
// but it isn't exported :(.
|
||||
interface ServiceConnection {
|
||||
allPipelines: {
|
||||
authorized: boolean;
|
||||
}
|
||||
id: string;
|
||||
isReady: boolean;
|
||||
type: string;
|
||||
operationStatus: {
|
||||
state: string;
|
||||
statusMessage: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ServiceConnectionClient {
|
||||
constructor(private connection: azdev.WebApi, private project: string) {
|
||||
}
|
||||
|
||||
public async createGitHubServiceConnection(endpointName: string, gitHubPat: string): Promise<ServiceConnection> {
|
||||
const url = `${this.connection.serverUrl}/${this.project}/_apis/serviceendpoint/endpoints`;
|
||||
|
||||
const response = await this.connection.rest.create<ServiceConnection>(url, {
|
||||
"administratorsGroup": null,
|
||||
"authorization": {
|
||||
"parameters": {
|
||||
"accessToken": gitHubPat
|
||||
},
|
||||
"scheme": "PersonalAccessToken"
|
||||
},
|
||||
"description": "",
|
||||
"groupScopeId": null,
|
||||
"name": endpointName,
|
||||
"operationStatus": null,
|
||||
"readersGroup": null,
|
||||
"type": "github",
|
||||
"url": "http://github.com"
|
||||
}, {
|
||||
acceptHeader: "application/json;api-version=5.1-preview.2;excludeUrls=true",
|
||||
additionalHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.result) {
|
||||
return response.result;
|
||||
} else {
|
||||
throw new Error(`Failed to create GitHub service connection: ${response.statusCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async createAzureServiceConnection(endpointName: string, tenantId: string, subscriptionId: string, scope: string, aadApp: AadApplication): Promise<ServiceConnection> {
|
||||
const url = `${this.connection.serverUrl}/${this.project}/_apis/serviceendpoint/endpoints`;
|
||||
|
||||
const response = await this.connection.rest.create<ServiceConnection>(url, {
|
||||
"administratorsGroup": null,
|
||||
"authorization": {
|
||||
"parameters": {
|
||||
"authenticationType": "spnKey",
|
||||
"scope": scope,
|
||||
"serviceprincipalid": aadApp.appId,
|
||||
"serviceprincipalkey": aadApp.secret,
|
||||
"tenantid": tenantId
|
||||
},
|
||||
"scheme": "ServicePrincipal"
|
||||
},
|
||||
"data": {
|
||||
"creationMode": "Manual",
|
||||
"subscriptionId": subscriptionId,
|
||||
"subscriptionName": subscriptionId
|
||||
},
|
||||
"description": "",
|
||||
"groupScopeId": null,
|
||||
"name": endpointName,
|
||||
"operationStatus": null,
|
||||
"readersGroup": null,
|
||||
"type": "azurerm",
|
||||
"url": "https://management.azure.com/"
|
||||
}, {
|
||||
acceptHeader: "application/json;api-version=5.1-preview.2;excludeUrls=true",
|
||||
additionalHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.result) {
|
||||
return response.result;
|
||||
} else {
|
||||
throw new Error(`Failed to create Azure service connection: ${response.statusCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getEndpointStatus(endpointId: string): Promise<ServiceConnection> {
|
||||
const url = `${this.connection.serverUrl}/${this.project}/_apis/serviceendpoint/endpoints/${endpointId}`;
|
||||
|
||||
const response = await this.connection.rest.get<ServiceConnection>(url, {
|
||||
acceptHeader: "application/json;api-version=5.1-preview.2;excludeUrls=true",
|
||||
additionalHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.result) {
|
||||
return response.result;
|
||||
} else {
|
||||
throw new Error(`Failed to get service connection status: ${response.statusCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Authorize individual pipelines instead of all pipelines.
|
||||
public async authorizeEndpointForAllPipelines(endpointId: string): Promise<ServiceConnection> {
|
||||
const url = `${this.connection.serverUrl}/${this.project}/_apis/pipelines/pipelinePermissions/endpoint/${endpointId}`;
|
||||
|
||||
const response = await this.connection.rest.update<ServiceConnection>(url, {
|
||||
"allPipelines": {
|
||||
"authorized": true,
|
||||
"authorizedBy": null,
|
||||
"authorizedOn": null
|
||||
},
|
||||
"pipelines": null,
|
||||
"resource": {
|
||||
"id": endpointId,
|
||||
"type": "endpoint"
|
||||
}
|
||||
}, {
|
||||
acceptHeader: "application/json;api-version=5.1-preview.1;excludeUrls=true;enumsAsNumbers=true;msDateFormat=true;noArrayWrap=true",
|
||||
additionalHeaders: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.result) {
|
||||
return response.result;
|
||||
} else {
|
||||
throw new Error(`Failed to authorize service connection: ${response.statusCode}`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { ServiceClient, RequestPrepareOptions } from "@azure/ms-rest-js";
|
||||
|
||||
export class RestClient extends ServiceClient {
|
||||
public sendRequest<TResult>(options: RequestPrepareOptions): Promise<TResult> {
|
||||
return new Promise<TResult>((resolve, reject) => {
|
||||
super.sendRequest(options)
|
||||
.then(response => {
|
||||
if (response.status >= 300) {
|
||||
reject(response.parsedBody as Error);
|
||||
}
|
||||
resolve(response.parsedBody as TResult);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error as Error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,791 +0,0 @@
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import { AppServiceClient } from './clients/azure/appServiceClient';
|
||||
import { OrganizationsClient } from './clients/devOps/organizationsClient';
|
||||
import * as AzureDevOpsHelper from './helper/devOps/azureDevOpsHelper';
|
||||
import * as Messages from '../messages';
|
||||
import { ServiceConnectionHelper } from './helper/devOps/serviceConnectionHelper';
|
||||
import { SourceOptions, RepositoryProvider, QuickPickItemWithData, GitRepositoryDetails, PipelineTemplate, AzureDevOpsDetails, ValidatedBuild, ValidatedProject, WebAppKind, TargetResourceType, ValidatedSite } from './model/models';
|
||||
import * as constants from './resources/constants';
|
||||
import * as TracePoints from './resources/tracePoints';
|
||||
import { getAzureAccountExtensionApi, getGitExtensionApi } from '../extensionApis';
|
||||
import { telemetryHelper } from '../helpers/telemetryHelper';
|
||||
import * as TelemetryKeys from '../helpers/telemetryKeys';
|
||||
import * as utils from 'util';
|
||||
import * as vscode from 'vscode';
|
||||
import { URI, Utils } from 'vscode-uri';
|
||||
import * as templateHelper from './helper/templateHelper';
|
||||
import { getAvailableFileName } from './helper/commonHelper';
|
||||
import { showInputBox, showQuickPick } from './helper/controlProvider';
|
||||
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
|
||||
import { AzureAccount, AzureSession } from '../typings/azure-account.api';
|
||||
import { Repository } from '../typings/git';
|
||||
import { AzureSiteDetails } from './model/models';
|
||||
import { GraphHelper } from './helper/graphHelper';
|
||||
import { TeamProject } from 'azure-devops-node-api/interfaces/CoreInterfaces';
|
||||
import { WebApi, getBearerHandler } from 'azure-devops-node-api';
|
||||
import * as GitHubHelper from './helper/gitHubHelper';
|
||||
import { WebSiteManagementModels } from '@azure/arm-appservice';
|
||||
|
||||
const Layer: string = 'configure';
|
||||
|
||||
export async function configurePipeline(): Promise<void> {
|
||||
const azureAccount = await getAzureAccountExtensionApi();
|
||||
if (!(await azureAccount.waitForLogin())) {
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.AzureLoginRequired, 'true');
|
||||
|
||||
const signIn = await vscode.window.showInformationMessage(Messages.azureLoginRequired, Messages.signInLabel);
|
||||
if (signIn?.toLowerCase() === Messages.signInLabel.toLowerCase()) {
|
||||
await vscode.commands.executeCommand("azure-account.login");
|
||||
} else {
|
||||
void vscode.window.showWarningMessage(Messages.azureLoginRequired);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const gitExtension = await getGitExtensionApi();
|
||||
const workspaceUri = await getWorkspace();
|
||||
if (workspaceUri === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = gitExtension.getRepository(workspaceUri);
|
||||
if (repo === null) {
|
||||
void vscode.window.showWarningMessage(Messages.notAGitRepository);
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh the repo status so that we have accurate info.
|
||||
await repo.status();
|
||||
|
||||
const configurer = new PipelineConfigurer(workspaceUri, repo, azureAccount);
|
||||
await configurer.configure();
|
||||
}
|
||||
|
||||
async function getWorkspace(): Promise<URI | undefined> {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (workspaceFolders !== undefined) {
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.SourceRepoLocation, SourceOptions.CurrentWorkspace);
|
||||
|
||||
if (workspaceFolders.length === 1) {
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.MultipleWorkspaceFolders, 'false');
|
||||
return workspaceFolders[0].uri;
|
||||
} else {
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.MultipleWorkspaceFolders, 'true');
|
||||
|
||||
const workspaceFolderOptions: QuickPickItemWithData<vscode.WorkspaceFolder>[] =
|
||||
workspaceFolders.map(folder => ({ label: folder.name, data: folder }));
|
||||
const selectedWorkspaceFolder = await showQuickPick(
|
||||
constants.SelectFromMultipleWorkSpace,
|
||||
workspaceFolderOptions,
|
||||
{ placeHolder: Messages.selectWorkspaceFolder });
|
||||
if (selectedWorkspaceFolder === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selectedWorkspaceFolder.data.uri;
|
||||
}
|
||||
} else {
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.SourceRepoLocation, SourceOptions.BrowseLocalMachine);
|
||||
const selectedFolders = await vscode.window.showOpenDialog({
|
||||
openLabel: Messages.selectFolderLabel,
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
});
|
||||
|
||||
if (selectedFolders === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selectedFolders[0];
|
||||
}
|
||||
}
|
||||
|
||||
class PipelineConfigurer {
|
||||
private azureDevOpsClient: WebApi | undefined;
|
||||
private uniqueResourceNameSuffix: string;
|
||||
|
||||
public constructor(
|
||||
private workspaceUri: URI,
|
||||
private repo: Repository,
|
||||
private azureAccount: AzureAccount) {
|
||||
this.uniqueResourceNameSuffix = uuid().substring(0, 5);
|
||||
}
|
||||
|
||||
public async configure(): Promise<void> {
|
||||
telemetryHelper.setCurrentStep('GetAllRequiredInputs');
|
||||
const repoDetails = await this.getGitDetailsFromRepository();
|
||||
if (repoDetails === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const template = await this.getSelectedPipeline();
|
||||
if (template === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adoDetails = await this.getAzureDevOpsDetails(repoDetails);
|
||||
if (adoDetails === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let azureSiteDetails: AzureSiteDetails | undefined;
|
||||
if (template.target.type !== TargetResourceType.None) {
|
||||
azureSiteDetails = await this.getAzureResourceDetails(adoDetails.session, template.target.kind);
|
||||
if (azureSiteDetails === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
telemetryHelper.setCurrentStep('CreatePreRequisites');
|
||||
const serviceConnectionHelper = new ServiceConnectionHelper(adoDetails.adoClient, adoDetails.project.name);
|
||||
|
||||
let repositoryProperties: Record<string, string> | undefined;
|
||||
if (repoDetails.repositoryProvider === RepositoryProvider.Github) {
|
||||
const gitHubServiceConnection = await this.createGitHubServiceConnection(
|
||||
serviceConnectionHelper,
|
||||
repoDetails,
|
||||
this.uniqueResourceNameSuffix);
|
||||
if (gitHubServiceConnection === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
repositoryProperties = {
|
||||
apiUrl: `https://api.github.com/repos/${repoDetails.ownerName}/${repoDetails.repositoryName}`,
|
||||
branchesUrl: `https://api.github.com/repos/${repoDetails.ownerName}/${repoDetails.repositoryName}/branches`,
|
||||
cloneUrl: repoDetails.remoteUrl,
|
||||
connectedServiceId: gitHubServiceConnection,
|
||||
defaultBranch: repoDetails.branch,
|
||||
fullName: `${repoDetails.ownerName}/${repoDetails.repositoryName}`,
|
||||
refsUrl: `https://api.github.com/repos/${repoDetails.ownerName}/${repoDetails.repositoryName}/git/refs`
|
||||
};
|
||||
}
|
||||
|
||||
let azureServiceConnection: string | undefined;
|
||||
if (azureSiteDetails !== undefined) {
|
||||
azureServiceConnection = await this.createAzureServiceConnection(
|
||||
serviceConnectionHelper,
|
||||
adoDetails,
|
||||
azureSiteDetails,
|
||||
this.uniqueResourceNameSuffix);
|
||||
if (azureServiceConnection === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
telemetryHelper.setCurrentStep('CheckInPipeline');
|
||||
const pipelineFileName = await this.createPipelineFile(
|
||||
template,
|
||||
repoDetails.branch,
|
||||
azureSiteDetails,
|
||||
azureServiceConnection);
|
||||
if (pipelineFileName === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commit = await this.checkInPipelineFileToRepository(pipelineFileName, repoDetails);
|
||||
if (commit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
telemetryHelper.setCurrentStep('CreateAndRunPipeline');
|
||||
const queuedPipeline = await this.createAndRunPipeline(
|
||||
repoDetails,
|
||||
adoDetails,
|
||||
template,
|
||||
azureSiteDetails,
|
||||
repositoryProperties,
|
||||
pipelineFileName,
|
||||
commit);
|
||||
if (queuedPipeline === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
telemetryHelper.setCurrentStep('PostPipelineCreation');
|
||||
if (azureSiteDetails !== undefined) {
|
||||
// This step should be determined by the
|
||||
// - resource target provider type (azure app service, function app, aks)
|
||||
// - pipeline provider (azure pipeline vs github)
|
||||
await this.updateScmType(queuedPipeline, adoDetails, azureSiteDetails);
|
||||
}
|
||||
|
||||
telemetryHelper.setCurrentStep('DisplayCreatedPipeline');
|
||||
void vscode.window.showInformationMessage(Messages.pipelineSetupSuccessfully, Messages.browsePipeline)
|
||||
.then(action => {
|
||||
if (action === Messages.browsePipeline) {
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.BrowsePipelineClicked, 'true');
|
||||
// _links is weakly typed and it's not worth the effort to verify.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
void vscode.env.openExternal(URI.parse(queuedPipeline._links.web.href));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getGitDetailsFromRepository(): Promise<GitRepositoryDetails | undefined> {
|
||||
const { HEAD } = this.repo.state;
|
||||
if (!HEAD) {
|
||||
void vscode.window.showWarningMessage(Messages.branchHeadMissing);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { name } = HEAD;
|
||||
let { remote } = HEAD;
|
||||
if (!name) {
|
||||
void vscode.window.showWarningMessage(Messages.branchNameMissing);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!remote) {
|
||||
// Remote tracking branch is not set, see if we have any remotes we can use.
|
||||
const remotes = this.repo.state.remotes;
|
||||
if (remotes.length === 0) {
|
||||
void vscode.window.showWarningMessage(Messages.branchRemoteMissing);
|
||||
return undefined;
|
||||
} else if (remotes.length === 1) {
|
||||
remote = remotes[0].name;
|
||||
} else {
|
||||
// Show an option to user to select remote to be configured
|
||||
const selectedRemote = await showQuickPick(
|
||||
constants.SelectRemoteForRepo,
|
||||
remotes.map(remote => ({ label: remote.name })),
|
||||
{ placeHolder: Messages.selectRemoteForBranch });
|
||||
if (selectedRemote === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
remote = selectedRemote.label;
|
||||
}
|
||||
}
|
||||
|
||||
let repoDetails: GitRepositoryDetails;
|
||||
let remoteUrl = this.repo.state.remotes.find(remoteObj => remoteObj.name === remote)?.fetchUrl;
|
||||
if (remoteUrl !== undefined) {
|
||||
if (AzureDevOpsHelper.isAzureReposUrl(remoteUrl)) {
|
||||
remoteUrl = AzureDevOpsHelper.getFormattedRemoteUrl(remoteUrl);
|
||||
const {
|
||||
organizationName,
|
||||
projectName,
|
||||
repositoryName
|
||||
} = AzureDevOpsHelper.getRepositoryDetailsFromRemoteUrl(remoteUrl);
|
||||
repoDetails = {
|
||||
repositoryProvider: RepositoryProvider.AzureRepos,
|
||||
organizationName,
|
||||
projectName,
|
||||
repositoryName,
|
||||
remoteName: remote,
|
||||
remoteUrl,
|
||||
branch: name,
|
||||
};
|
||||
} else if (GitHubHelper.isGitHubUrl(remoteUrl)) {
|
||||
remoteUrl = GitHubHelper.getFormattedRemoteUrl(remoteUrl);
|
||||
const { ownerName, repositoryName } = GitHubHelper.getRepositoryDetailsFromRemoteUrl(remoteUrl);
|
||||
repoDetails = {
|
||||
repositoryProvider: RepositoryProvider.Github,
|
||||
ownerName,
|
||||
repositoryName,
|
||||
remoteName: remote,
|
||||
remoteUrl,
|
||||
branch: name,
|
||||
};
|
||||
} else {
|
||||
void vscode.window.showWarningMessage(Messages.cannotIdentifyRepositoryDetails);
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
void vscode.window.showWarningMessage(Messages.remoteRepositoryNotConfigured);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.RepoProvider, repoDetails.repositoryProvider);
|
||||
|
||||
return repoDetails;
|
||||
}
|
||||
|
||||
private async getSelectedPipeline(): Promise<PipelineTemplate | undefined> {
|
||||
const appropriateTemplates: PipelineTemplate[] = await vscode.window.withProgress(
|
||||
{ location: vscode.ProgressLocation.Notification, title: Messages.analyzingRepo },
|
||||
() => templateHelper.analyzeRepoAndListAppropriatePipeline(this.workspaceUri)
|
||||
);
|
||||
|
||||
// TODO: Get applicable pipelines for the repo type and azure target type if target already selected
|
||||
const template = await showQuickPick(
|
||||
constants.SelectPipelineTemplate,
|
||||
appropriateTemplates.map((template) => { return { label: template.label, data: template }; }),
|
||||
{ placeHolder: Messages.selectPipelineTemplate },
|
||||
TelemetryKeys.PipelineTempateListCount);
|
||||
|
||||
if (template === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.ChosenTemplate, template.data.label);
|
||||
return template.data;
|
||||
}
|
||||
|
||||
private async getAzureDevOpsDetails(repoDetails: GitRepositoryDetails): Promise<AzureDevOpsDetails | undefined> {
|
||||
if (repoDetails.repositoryProvider === RepositoryProvider.AzureRepos) {
|
||||
for (const session of this.azureAccount.filters.map(({ session }) => session)) {
|
||||
const organizationsClient = new OrganizationsClient(session.credentials2);
|
||||
const organizations = await organizationsClient.listOrganizations();
|
||||
if (organizations.find(org =>
|
||||
org.accountName.toLowerCase() === repoDetails.organizationName.toLowerCase())) {
|
||||
const adoClient = await this.getAzureDevOpsClient(repoDetails.organizationName, session);
|
||||
const coreApi = await adoClient.getCoreApi();
|
||||
const project = await coreApi.getProject(repoDetails.projectName);
|
||||
if (isValidProject(project)) {
|
||||
return {
|
||||
session,
|
||||
adoClient,
|
||||
organizationName: repoDetails.organizationName,
|
||||
project,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void vscode.window.showWarningMessage("You are not signed in to the Azure DevOps organization that contains this repository.");
|
||||
return undefined;
|
||||
} else {
|
||||
// Lazily construct list of organizations so that we can immediately show the quick pick,
|
||||
// then fill in the choices as they come in.
|
||||
const getOrganizationsAndSessions = async (): Promise<QuickPickItemWithData<AzureSession | undefined>[]> => {
|
||||
return [
|
||||
...(await Promise.all(this.azureAccount.filters.map(async ({ session }) => {
|
||||
const organizationsClient = new OrganizationsClient(session.credentials2);
|
||||
const organizations = await organizationsClient.listOrganizations();
|
||||
return organizations.map(organization => ({
|
||||
label: organization.accountName,
|
||||
data: session,
|
||||
}));
|
||||
}))).flat(),
|
||||
{
|
||||
// This is safe because ADO orgs can't have spaces in them.
|
||||
label: "Create new Azure DevOps organization...",
|
||||
data: undefined,
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const result = await showQuickPick(
|
||||
'organization',
|
||||
getOrganizationsAndSessions(), {
|
||||
placeHolder: "Select the Azure DevOps organization to create this pipeline in",
|
||||
}, TelemetryKeys.OrganizationListCount);
|
||||
if (result === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { label: organizationName, data: session } = result;
|
||||
if (session === undefined) {
|
||||
// Special flag telling us to create a new organization.
|
||||
await vscode.env.openExternal(vscode.Uri.parse("https://dev.azure.com/"));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const adoClient = await this.getAzureDevOpsClient(organizationName, session);
|
||||
|
||||
// Ditto for the projects.
|
||||
const getProjects = async (): Promise<QuickPickItemWithData<ValidatedProject | undefined>[]> => {
|
||||
const coreApi = await adoClient.getCoreApi();
|
||||
const projects = await coreApi.getProjects();
|
||||
return [
|
||||
...projects
|
||||
.filter(isValidProject)
|
||||
.map(project => { return { label: project.name, data: project }; }),
|
||||
{
|
||||
// This is safe because ADO projects can't end with periods.
|
||||
label: "Create new project...",
|
||||
data: undefined,
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const selectedProject = await showQuickPick(
|
||||
constants.SelectProject,
|
||||
getProjects(),
|
||||
{ placeHolder: Messages.selectProject },
|
||||
TelemetryKeys.ProjectListCount);
|
||||
if (selectedProject === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const project = selectedProject.data;
|
||||
if (project === undefined) {
|
||||
// Special flag telling us to create a new project.
|
||||
await vscode.env.openExternal(vscode.Uri.parse(`https://dev.azure.com/${organizationName}`));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
adoClient,
|
||||
organizationName,
|
||||
project,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async getAzureResourceDetails(
|
||||
session: AzureSession,
|
||||
kind: WebAppKind): Promise<AzureSiteDetails | undefined> {
|
||||
// show available subscriptions and get the chosen one
|
||||
const subscriptionList = this.azureAccount.filters
|
||||
.filter(filter =>
|
||||
// session is actually an AzureSessionInternal which makes a naive === check fail.
|
||||
filter.session.environment === session.environment &&
|
||||
filter.session.tenantId === session.tenantId &&
|
||||
filter.session.userId === session.userId)
|
||||
.map(subscriptionObject => {
|
||||
return {
|
||||
label: subscriptionObject.subscription.displayName ?? "Unknown subscription",
|
||||
data: subscriptionObject,
|
||||
description: subscriptionObject.subscription.subscriptionId ?? undefined
|
||||
};
|
||||
});
|
||||
|
||||
const selectedSubscription = await showQuickPick(
|
||||
constants.SelectSubscription,
|
||||
subscriptionList,
|
||||
{ placeHolder: Messages.selectSubscription });
|
||||
if (selectedSubscription === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { subscriptionId } = selectedSubscription.data.subscription;
|
||||
if (subscriptionId === undefined) {
|
||||
void vscode.window.showErrorMessage("Unable to get ID for subscription, please file a bug at https://github.com/microsoft/azure-pipelines-vscode/issues/new");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// show available resources and get the chosen one
|
||||
const appServiceClient = new AppServiceClient(session.credentials2, subscriptionId);
|
||||
|
||||
// TODO: Refactor kind so we don't need three kind.includes
|
||||
|
||||
const sites = await appServiceClient.getAppServices(kind);
|
||||
const items: QuickPickItemWithData<ValidatedSite | undefined>[] = sites
|
||||
.filter(isValidSite)
|
||||
.map(site => { return { label: site.name, data: site }; });
|
||||
const appType = kind.includes("functionapp") ? "Function App" : "Web App";
|
||||
|
||||
items.push({
|
||||
// This is safe because apps can't have spaces in them.
|
||||
label: `Create new ${appType.toLowerCase()}...`,
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const selectedResource = await showQuickPick(
|
||||
kind.includes("functionapp") ? "selectFunctionApp" : "selectWebApp",
|
||||
items,
|
||||
{ placeHolder: `Select ${appType}` },
|
||||
TelemetryKeys.WebAppListCount);
|
||||
if (selectedResource === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { data: site } = selectedResource;
|
||||
if (site === undefined) {
|
||||
// Special flag telling us to create a new app.
|
||||
// URL format is documented at
|
||||
// https://github.com/Azure/portaldocs/blob/main/portal-sdk/generated/portalfx-links.md#create-blades
|
||||
const packageId = kind.includes("functionapp") ? "Microsoft.FunctionApp" : "Microsoft.WebSite";
|
||||
await vscode.env.openExternal(vscode.Uri.parse(`https://portal.azure.com/#create/${packageId}`));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
appServiceClient,
|
||||
site,
|
||||
subscriptionId,
|
||||
};
|
||||
}
|
||||
|
||||
private async createGitHubServiceConnection(
|
||||
serviceConnectionHelper: ServiceConnectionHelper,
|
||||
repoDetails: GitRepositoryDetails,
|
||||
uniqueResourceNameSuffix: string,
|
||||
): Promise<string | undefined> {
|
||||
const token = await telemetryHelper.executeFunctionWithTimeTelemetry(
|
||||
async () => showInputBox(
|
||||
constants.GitHubPat, {
|
||||
placeHolder: Messages.enterGitHubPat,
|
||||
prompt: Messages.githubPatHelpMessage,
|
||||
validateInput: input => input.length === 0 ? Messages.gitHubPatErrorMessage : null
|
||||
}
|
||||
), TelemetryKeys.GitHubPatDuration
|
||||
);
|
||||
|
||||
if (token === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: Messages.creatingGitHubServiceConnection
|
||||
},
|
||||
async () => {
|
||||
const serviceConnectionName = `${repoDetails.repositoryName}-github-${uniqueResourceNameSuffix}`;
|
||||
try {
|
||||
return serviceConnectionHelper.createGitHubServiceConnection(serviceConnectionName, token);
|
||||
} catch (error) {
|
||||
telemetryHelper.logError(Layer, TracePoints.GitHubServiceConnectionError, error as Error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async createAzureServiceConnection(
|
||||
serviceConnectionHelper: ServiceConnectionHelper,
|
||||
adoDetails: AzureDevOpsDetails,
|
||||
azureSiteDetails: AzureSiteDetails,
|
||||
uniqueResourceNameSuffix: string,
|
||||
): Promise<string | undefined> {
|
||||
// TODO: should SPN created be scoped to resource group of target azure resource.
|
||||
return vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: utils.format(Messages.creatingAzureServiceConnection, azureSiteDetails.subscriptionId)
|
||||
},
|
||||
async () => {
|
||||
const scope = azureSiteDetails.site.id;
|
||||
try {
|
||||
const aadAppName = GraphHelper.generateAadApplicationName(
|
||||
adoDetails.organizationName,
|
||||
adoDetails.project.name);
|
||||
const aadApp = await GraphHelper.createSpnAndAssignRole(adoDetails.session, aadAppName, scope);
|
||||
const serviceConnectionName = `${azureSiteDetails.site.name}-${uniqueResourceNameSuffix}`;
|
||||
return serviceConnectionHelper.createAzureServiceConnection(
|
||||
serviceConnectionName,
|
||||
adoDetails.session.tenantId,
|
||||
azureSiteDetails.subscriptionId,
|
||||
scope,
|
||||
aadApp);
|
||||
}
|
||||
catch (error) {
|
||||
telemetryHelper.logError(Layer, TracePoints.AzureServiceConnectionCreateFailure, error as Error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async createPipelineFile(
|
||||
template: PipelineTemplate,
|
||||
branch: string,
|
||||
azureSiteDetails: AzureSiteDetails | undefined,
|
||||
azureServiceConnection: string | undefined,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const pipelineFileName = await getAvailableFileName("azure-pipelines.yml", this.workspaceUri);
|
||||
const fileUri = Utils.joinPath(this.workspaceUri, pipelineFileName);
|
||||
const content = await templateHelper.renderContent(
|
||||
template.path,
|
||||
branch,
|
||||
azureSiteDetails?.site.name,
|
||||
azureServiceConnection);
|
||||
await vscode.workspace.fs.writeFile(fileUri, Buffer.from(content));
|
||||
await vscode.window.showTextDocument(fileUri);
|
||||
return pipelineFileName;
|
||||
} catch (error) {
|
||||
telemetryHelper.logError(Layer, TracePoints.AddingContentToPipelineFileFailed, error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async checkInPipelineFileToRepository(
|
||||
pipelineFileName: string,
|
||||
repoDetails: GitRepositoryDetails,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const commitOrDiscard = await vscode.window.showInformationMessage(
|
||||
utils.format(
|
||||
Messages.modifyAndCommitFile,
|
||||
Messages.commitAndPush,
|
||||
repoDetails.branch,
|
||||
repoDetails.remoteName),
|
||||
Messages.commitAndPush,
|
||||
Messages.discardPipeline);
|
||||
if (commitOrDiscard?.toLowerCase() === Messages.commitAndPush.toLowerCase()) {
|
||||
return vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: Messages.pushingPipelineFile
|
||||
}, async () => {
|
||||
try {
|
||||
// TODO: Only commit the YAML file. Need to file a feature request on VS Code for this.
|
||||
await this.repo.add([Utils.joinPath(this.workspaceUri, pipelineFileName).fsPath]);
|
||||
await this.repo.commit(Messages.addYmlFile);
|
||||
await this.repo.push(repoDetails.remoteName);
|
||||
|
||||
const commit = this.repo.state.HEAD?.commit;
|
||||
if (commit === undefined) {
|
||||
void vscode.window.showErrorMessage("Unable to get commit after pushing pipeline, please file a bug at https://github.com/microsoft/azure-pipelines-vscode/issues/new");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return commit;
|
||||
} catch (error) {
|
||||
telemetryHelper.logError(Layer, TracePoints.CheckInPipelineFailure, error as Error);
|
||||
void vscode.window.showErrorMessage(
|
||||
utils.format(Messages.commitFailedErrorMessage, (error as Error).message));
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.PipelineDiscarded, 'true');
|
||||
return undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
telemetryHelper.logError(Layer, TracePoints.PipelineFileCheckInFailed, error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async createAndRunPipeline(
|
||||
repoDetails: GitRepositoryDetails,
|
||||
adoDetails: AzureDevOpsDetails,
|
||||
template: PipelineTemplate,
|
||||
azureSiteDetails: AzureSiteDetails | undefined,
|
||||
repositoryProperties: Record<string, string> | undefined,
|
||||
pipelineFileName: string,
|
||||
commit: string,
|
||||
): Promise<ValidatedBuild | undefined> {
|
||||
return vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: Messages.configuringPipelineAndDeployment
|
||||
}, async () => {
|
||||
try {
|
||||
const taskAgentApi = await adoDetails.adoClient.getTaskAgentApi();
|
||||
const queues = await taskAgentApi.getAgentQueuesByNames(
|
||||
[constants.HostedVS2017QueueName],
|
||||
adoDetails.project.name);
|
||||
if (queues.length === 0) {
|
||||
void vscode.window.showErrorMessage(
|
||||
utils.format(Messages.noAgentQueueFound, constants.HostedVS2017QueueName));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const pipelineName = `${(azureSiteDetails?.site.name ?? template.label)}-${this.uniqueResourceNameSuffix}`;
|
||||
const definitionPayload = AzureDevOpsHelper.getBuildDefinitionPayload(
|
||||
pipelineName,
|
||||
queues[0],
|
||||
repoDetails,
|
||||
adoDetails,
|
||||
repositoryProperties,
|
||||
pipelineFileName
|
||||
);
|
||||
const buildApi = await adoDetails.adoClient.getBuildApi();
|
||||
const definition = await buildApi.createDefinition(definitionPayload, adoDetails.project.name);
|
||||
const build = await buildApi.queueBuild({
|
||||
definition,
|
||||
project: adoDetails.project,
|
||||
sourceBranch: repoDetails.branch,
|
||||
sourceVersion: commit
|
||||
}, adoDetails.project.name);
|
||||
|
||||
if (!isValidBuild(build)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return build;
|
||||
}
|
||||
catch (error) {
|
||||
telemetryHelper.logError(Layer, TracePoints.CreateAndQueuePipelineFailed, error as Error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async updateScmType(
|
||||
queuedPipeline: ValidatedBuild,
|
||||
adoDetails: AzureDevOpsDetails,
|
||||
azureSiteDetails: AzureSiteDetails,
|
||||
): Promise<void> {
|
||||
await vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: Messages.runningPostDeploymentActions
|
||||
}, async () => {
|
||||
try {
|
||||
// update SCM type
|
||||
await azureSiteDetails.appServiceClient.updateScmType(azureSiteDetails.site);
|
||||
|
||||
const buildDefinitionUrl = AzureDevOpsHelper.getOldFormatBuildDefinitionUrl(
|
||||
adoDetails.organizationName,
|
||||
adoDetails.project.id,
|
||||
queuedPipeline.definition.id);
|
||||
const buildUrl = AzureDevOpsHelper.getOldFormatBuildUrl(
|
||||
adoDetails.organizationName,
|
||||
adoDetails.project.id,
|
||||
queuedPipeline.id);
|
||||
|
||||
const locationsApi = await adoDetails.adoClient.getLocationsApi();
|
||||
const { instanceId } = await locationsApi.getConnectionData();
|
||||
if (instanceId === undefined) {
|
||||
void vscode.window.showErrorMessage("Unable to determine the organization ID, please file a bug at https://github.com/microsoft/azure-pipelines-vscode/issues/new");
|
||||
return;
|
||||
}
|
||||
|
||||
// update metadata of app service to store information about the pipeline deploying to web app.
|
||||
const metadata = await azureSiteDetails.appServiceClient.getAppServiceMetadata(azureSiteDetails.site);
|
||||
metadata.properties = {
|
||||
...metadata.properties,
|
||||
VSTSRM_ProjectId: adoDetails.project.id,
|
||||
VSTSRM_AccountId: instanceId,
|
||||
VSTSRM_BuildDefinitionId: queuedPipeline.definition.id.toString(),
|
||||
VSTSRM_BuildDefinitionWebAccessUrl: buildDefinitionUrl,
|
||||
VSTSRM_ConfiguredCDEndPoint: '',
|
||||
VSTSRM_ReleaseDefinitionId: '',
|
||||
};
|
||||
|
||||
await azureSiteDetails.appServiceClient.updateAppServiceMetadata(azureSiteDetails.site, metadata);
|
||||
|
||||
// send a deployment log with information about the setup pipeline and links.
|
||||
await azureSiteDetails.appServiceClient.publishDeploymentToAppService(
|
||||
azureSiteDetails.site,
|
||||
buildDefinitionUrl,
|
||||
buildDefinitionUrl,
|
||||
buildUrl);
|
||||
} catch (error) {
|
||||
telemetryHelper.logError(Layer, TracePoints.PostDeploymentActionFailed, error as Error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getAzureDevOpsClient(organization: string, session: AzureSession): Promise<WebApi> {
|
||||
if (this.azureDevOpsClient) {
|
||||
return this.azureDevOpsClient;
|
||||
}
|
||||
|
||||
const { accessToken } = await session.credentials2.getToken();
|
||||
const authHandler = getBearerHandler(accessToken);
|
||||
this.azureDevOpsClient = new WebApi(`https://dev.azure.com/${organization}`, authHandler);
|
||||
return this.azureDevOpsClient;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidProject(project: TeamProject): project is ValidatedProject {
|
||||
if (project.name === undefined || project.id === undefined) {
|
||||
void vscode.window.showErrorMessage("Unable to get name or ID for project, please file a bug at https://github.com/microsoft/azure-pipelines-vscode/issues/new");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isValidSite(resource: WebSiteManagementModels.Site): resource is ValidatedSite {
|
||||
if (resource.name === undefined || resource.id === undefined) {
|
||||
void vscode.window.showErrorMessage("Unable to get name or ID for resource, please file a bug at https://github.com/microsoft/azure-pipelines-vscode/issues/new");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isValidBuild(build: Build): build is ValidatedBuild {
|
||||
if (build.definition === undefined || build.definition.id === undefined || build.id === undefined) {
|
||||
void vscode.window.showErrorMessage("Unable to get definition or ID for build, please file a bug at https://github.com/microsoft/azure-pipelines-vscode/issues/new");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import * as vscode from 'vscode';
|
||||
import { URI } from 'vscode-uri';
|
||||
import * as util from 'util';
|
||||
import * as Messages from '../../messages';
|
||||
import * as logger from '../../logger';
|
||||
|
||||
export async function sleepForMilliSeconds(timeInMs: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, timeInMs);
|
||||
});
|
||||
}
|
||||
|
||||
export function generateRandomPassword(length: number = 20): string {
|
||||
const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#%^*()-+";
|
||||
const charTypeSize = [26, 26, 10, 10];
|
||||
const charTypeStartIndex = [0, 26, 52, 62];
|
||||
let password = "";
|
||||
for (let x = 0; x < length; x++) {
|
||||
const i = Math.floor(Math.random() * charTypeSize[x % 4]);
|
||||
password += characters.charAt(i + charTypeStartIndex[x % 4]);
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
export async function executeFunctionWithRetry<T>(
|
||||
func: () => Promise<T>,
|
||||
retryCount: number = 20,
|
||||
retryIntervalTimeInSec: number = 2,
|
||||
errorMessage?: string): Promise<T> {
|
||||
let internalError = null;
|
||||
for (; retryCount > 0; retryCount--) {
|
||||
try {
|
||||
return func();
|
||||
} catch (error) {
|
||||
internalError = error;
|
||||
logger.log(JSON.stringify(error));
|
||||
await sleepForMilliSeconds(retryIntervalTimeInSec * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(errorMessage ?
|
||||
errorMessage.concat(util.format(Messages.retryFailedMessage, retryCount, JSON.stringify(internalError))) :
|
||||
util.format(Messages.retryFailedMessage, retryCount, JSON.stringify(internalError)));
|
||||
}
|
||||
|
||||
export async function getAvailableFileName(fileName: string, repoPath: URI): Promise<string> {
|
||||
const files = (await vscode.workspace.fs.readDirectory(repoPath)).map(entries => entries[0]);
|
||||
if (!files.includes(fileName)) {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
for (let i = 1; i < 100; i++) {
|
||||
const incrementalFileName = getIncrementalFileName(fileName, i);
|
||||
if (!files.includes(incrementalFileName)) {
|
||||
return incrementalFileName;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(Messages.noAvailableFileNames);
|
||||
}
|
||||
|
||||
function getIncrementalFileName(fileName: string, count: number): string {
|
||||
const periodIndex = fileName.indexOf('.');
|
||||
return fileName.substring(0, periodIndex).concat(` (${count})`, fileName.substring(periodIndex));
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
import { BuildDefinition, ContinuousIntegrationTrigger, DefinitionQuality, DefinitionTriggerType, DefinitionType, YamlProcess } from 'azure-devops-node-api/interfaces/BuildInterfaces';
|
||||
import { TaskAgentQueue } from 'azure-devops-node-api/interfaces/TaskAgentInterfaces';
|
||||
|
||||
import { RepositoryProvider, GitRepositoryDetails, AzureDevOpsDetails } from '../../model/models';
|
||||
import * as Messages from '../../../messages';
|
||||
|
||||
// https://dev.azure.com/ OR https://org@dev.azure.com/
|
||||
const AzureReposUrl = 'dev.azure.com/';
|
||||
|
||||
// git@ssh.dev.azure.com:v3/
|
||||
const SSHAzureReposUrl = 'ssh.dev.azure.com:v3/';
|
||||
|
||||
// https://org.visualstudio.com/
|
||||
const VSOUrl = '.visualstudio.com/';
|
||||
|
||||
// org@vs-ssh.visualstudio.com:v3/
|
||||
const SSHVsoReposUrl = 'vs-ssh.visualstudio.com:v3/';
|
||||
|
||||
export function isAzureReposUrl(remoteUrl: string): boolean {
|
||||
return remoteUrl.includes(AzureReposUrl) ||
|
||||
remoteUrl.includes(VSOUrl) ||
|
||||
remoteUrl.includes(SSHAzureReposUrl) ||
|
||||
remoteUrl.includes(SSHVsoReposUrl);
|
||||
}
|
||||
|
||||
// TODO: Use ADO instead.
|
||||
export function getFormattedRemoteUrl(remoteUrl: string): string {
|
||||
// Convert SSH based url to https based url as pipeline service doesn't accept SSH based URL
|
||||
if (remoteUrl.includes(SSHAzureReposUrl) || remoteUrl.includes(SSHVsoReposUrl)) {
|
||||
const details = getRepositoryDetailsFromRemoteUrl(remoteUrl);
|
||||
return `https://${details.organizationName}${VSOUrl}${details.projectName}/_git/${details.repositoryName}`;
|
||||
}
|
||||
|
||||
return remoteUrl;
|
||||
}
|
||||
|
||||
export function getRepositoryDetailsFromRemoteUrl(remoteUrl: string): { organizationName: string, projectName: string, repositoryName: string } {
|
||||
if (remoteUrl.includes(AzureReposUrl)) {
|
||||
const part = remoteUrl.substring(remoteUrl.indexOf(AzureReposUrl) + AzureReposUrl.length);
|
||||
const parts = part.split('/');
|
||||
if (parts.length !== 4) {
|
||||
throw new Error(Messages.failedToDetermineAzureRepoDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
organizationName: parts[0].trim(),
|
||||
projectName: parts[1].trim(),
|
||||
repositoryName: parts[3].trim()
|
||||
};
|
||||
} else if (remoteUrl.includes(VSOUrl)) {
|
||||
const part = remoteUrl.substring(remoteUrl.indexOf(VSOUrl) + VSOUrl.length);
|
||||
const organizationName = remoteUrl.substring(remoteUrl.indexOf('https://') + 'https://'.length, remoteUrl.indexOf('.visualstudio.com'));
|
||||
const parts = part.split('/');
|
||||
|
||||
if (parts.length === 4 && parts[0].toLowerCase() === 'defaultcollection') {
|
||||
// Handle scenario where part is 'DefaultCollection/<project>/_git/<repository>'
|
||||
parts.shift();
|
||||
}
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error(Messages.failedToDetermineAzureRepoDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
organizationName: organizationName,
|
||||
projectName: parts[0].trim(),
|
||||
repositoryName: parts[2].trim()
|
||||
};
|
||||
} else if (remoteUrl.includes(SSHAzureReposUrl) || remoteUrl.includes(SSHVsoReposUrl)) {
|
||||
const urlFormat = remoteUrl.includes(SSHAzureReposUrl) ? SSHAzureReposUrl : SSHVsoReposUrl;
|
||||
const part = remoteUrl.substring(remoteUrl.indexOf(urlFormat) + urlFormat.length);
|
||||
const parts = part.split('/');
|
||||
if (parts.length !== 3) {
|
||||
throw new Error(Messages.failedToDetermineAzureRepoDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
organizationName: parts[0].trim(),
|
||||
projectName: parts[1].trim(),
|
||||
repositoryName: parts[2].trim()
|
||||
};
|
||||
} else {
|
||||
throw new Error(Messages.notAzureRepoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBuildDefinitionPayload(
|
||||
pipelineName: string,
|
||||
queue: TaskAgentQueue,
|
||||
repoDetails: GitRepositoryDetails,
|
||||
adoDetails: AzureDevOpsDetails,
|
||||
repositoryProperties: Record<string, string> | undefined,
|
||||
pipelineFileName: string,
|
||||
): BuildDefinition {
|
||||
return {
|
||||
name: pipelineName,
|
||||
type: DefinitionType.Build,
|
||||
quality: DefinitionQuality.Definition,
|
||||
path: "\\", //Folder path of build definition. Root folder in this case
|
||||
project: adoDetails.project,
|
||||
process: {
|
||||
type: 2,
|
||||
yamlFileName: pipelineFileName,
|
||||
} as YamlProcess,
|
||||
queue: {
|
||||
id: queue.id,
|
||||
},
|
||||
triggers: [
|
||||
{
|
||||
triggerType: DefinitionTriggerType.ContinuousIntegration, // Continuous integration trigger type
|
||||
settingsSourceType: 2, // Use trigger source as specified in YAML
|
||||
batchChanges: false,
|
||||
} as ContinuousIntegrationTrigger,
|
||||
],
|
||||
repository: {
|
||||
id: repoDetails.repositoryProvider === RepositoryProvider.Github
|
||||
? `${repoDetails.ownerName}/${repoDetails.repositoryName}`
|
||||
: undefined,
|
||||
name: repoDetails.repositoryProvider === RepositoryProvider.Github
|
||||
? `${repoDetails.ownerName}/${repoDetails.repositoryName}`
|
||||
: repoDetails.repositoryName,
|
||||
type: repoDetails.repositoryProvider,
|
||||
defaultBranch: repoDetails.branch,
|
||||
url: repoDetails.remoteUrl,
|
||||
properties: repositoryProperties,
|
||||
},
|
||||
properties: {
|
||||
source: 'ms-azure-devops.azure-pipelines',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: These should be able to be changed to use ADO instead.
|
||||
export function getOldFormatBuildDefinitionUrl(accountName: string, projectName: string, buildDefinitionId: number) {
|
||||
return `https://${accountName}.visualstudio.com/${projectName}/_build?definitionId=${buildDefinitionId}&_a=summary`;
|
||||
}
|
||||
|
||||
export function getOldFormatBuildUrl(accountName: string, projectName: string, buildId: number) {
|
||||
return `https://${accountName}.visualstudio.com/${projectName}/_build/results?buildId=${buildId}&view=results`;
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import * as azdev from 'azure-devops-node-api';
|
||||
import * as util from 'util';
|
||||
|
||||
import { sleepForMilliSeconds } from '../commonHelper';
|
||||
import { ServiceConnectionClient } from '../../clients/devOps/serviceConnectionClient';
|
||||
import { AadApplication } from '../../model/models';
|
||||
import * as Messages from '../../../messages';
|
||||
|
||||
export class ServiceConnectionHelper {
|
||||
private serviceConnectionClient: ServiceConnectionClient;
|
||||
|
||||
public constructor(connection: azdev.WebApi, project: string) {
|
||||
this.serviceConnectionClient = new ServiceConnectionClient(connection, project);
|
||||
}
|
||||
|
||||
public async createGitHubServiceConnection(name: string, gitHubPat: string): Promise<string> {
|
||||
const connection = await this.serviceConnectionClient.createGitHubServiceConnection(name, gitHubPat);
|
||||
const endpointId = connection.id;
|
||||
await this.waitForEndpointToBeReady(endpointId);
|
||||
const authorizationResponse = await this.serviceConnectionClient.authorizeEndpointForAllPipelines(endpointId);
|
||||
if (!authorizationResponse.allPipelines.authorized) {
|
||||
throw new Error(Messages.couldNotAuthorizeEndpoint);
|
||||
}
|
||||
|
||||
return endpointId;
|
||||
}
|
||||
|
||||
public async createAzureServiceConnection(name: string, tenantId: string, subscriptionId: string, scope: string, aadApp: AadApplication): Promise<string> {
|
||||
const connection = await this.serviceConnectionClient.createAzureServiceConnection(name, tenantId, subscriptionId, scope, aadApp);
|
||||
const endpointId: string = connection.id;
|
||||
await this.waitForEndpointToBeReady(endpointId);
|
||||
const authorizationResponse = await this.serviceConnectionClient.authorizeEndpointForAllPipelines(endpointId);
|
||||
if (!authorizationResponse.allPipelines.authorized) {
|
||||
throw new Error(Messages.couldNotAuthorizeEndpoint);
|
||||
}
|
||||
|
||||
return endpointId;
|
||||
}
|
||||
|
||||
private async waitForEndpointToBeReady(endpointId: string): Promise<void> {
|
||||
for (let attempt = 0; attempt < 30; attempt++) {
|
||||
const connection = await this.serviceConnectionClient.getEndpointStatus(endpointId);
|
||||
|
||||
if (connection.isReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { operationStatus } = connection;
|
||||
if (operationStatus.state.toLowerCase() === "failed") {
|
||||
throw Error(util.format(Messages.unableToCreateServiceConnection, connection.type, operationStatus.state, operationStatus.statusMessage));
|
||||
}
|
||||
|
||||
await sleepForMilliSeconds(2000);
|
||||
}
|
||||
|
||||
throw Error(util.format(Messages.timedOutCreatingServiceConnection));
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// Note that this is not intended to be be completely accurate.
|
||||
// This is the canonical URL that GitHub provides when cloning,
|
||||
// and the only one that we'll support to keep the code simple.
|
||||
const GitHubUrl = 'https://github.com/';
|
||||
const SSHGitHubUrl = 'git@github.com:';
|
||||
|
||||
export function isGitHubUrl(remoteUrl: string): boolean {
|
||||
return remoteUrl.startsWith(GitHubUrl) || remoteUrl.startsWith(SSHGitHubUrl);
|
||||
}
|
||||
|
||||
export function getRepositoryDetailsFromRemoteUrl(remoteUrl: string): { ownerName: string, repositoryName: string } {
|
||||
// https://github.com/microsoft/azure-pipelines-vscode.git
|
||||
// => ['https:', '', 'github.com', 'microsoft', 'azure-pipelines-vscode.git']
|
||||
// => { ownerName: 'microsoft', repositoryName: 'azure-pipelines-vscode'}
|
||||
// ===============================================
|
||||
// git@github.com:microsoft/azure-pipelines-vscode
|
||||
// => microsoft/zure-pipelines-vscode
|
||||
// => ['microsoft', 'azure-pipelines-vscode']
|
||||
// => { ownerName: 'microsoft', repositoryName: 'azure-pipelines-vscode'}
|
||||
const parts = remoteUrl.replace(SSHGitHubUrl, '').split('/');
|
||||
return {
|
||||
ownerName: parts[parts.length - 2],
|
||||
repositoryName: parts[parts.length - 1].replace(/\.git$/, '')
|
||||
};
|
||||
}
|
||||
|
||||
export function getFormattedRemoteUrl(remoteUrl: string): string {
|
||||
if (remoteUrl.startsWith(SSHGitHubUrl)) {
|
||||
return `https://github.com/${remoteUrl.substring(SSHGitHubUrl.length)}`;
|
||||
}
|
||||
|
||||
return remoteUrl;
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import { AadApplication } from '../model/models';
|
||||
import { generateRandomPassword, executeFunctionWithRetry } from './commonHelper';
|
||||
import * as Messages from '../../messages';
|
||||
import { RestClient } from '../clients/restClient';
|
||||
import { TokenCredentials } from '@azure/ms-rest-js';
|
||||
import { TokenCredentialsBase } from '@azure/ms-rest-nodeauth';
|
||||
import * as util from 'util';
|
||||
import { AzureSession } from '../../typings/azure-account.api';
|
||||
|
||||
// TODO: Replace this class with @microsoft/microsoft-graph-client and @azure/arm-authorization
|
||||
// client.api("/applications").post()
|
||||
// client.api("/servicePrincipals").post()
|
||||
// new AuthorizationManagementClient().roleAssignments.create()
|
||||
// Disable ESLint rules because there won't be any investment into this file; see above.
|
||||
/* eslint-disable */
|
||||
export class GraphHelper {
|
||||
|
||||
private static contributorRoleId = "b24988ac-6180-42a0-ab88-20f7382dd24c";
|
||||
private static retryTimeIntervalInSec = 2;
|
||||
private static retryCount = 20;
|
||||
|
||||
public static async createSpnAndAssignRole(session: AzureSession, aadAppName: string, scope: string): Promise<AadApplication> {
|
||||
const accessToken = await this.getGraphToken(session);
|
||||
const tokenCredentials = new TokenCredentials(accessToken);
|
||||
const graphClient = new RestClient(tokenCredentials);
|
||||
const tenantId = session.tenantId;
|
||||
let aadApp: AadApplication;
|
||||
|
||||
return this.createAadApp(graphClient, aadAppName, tenantId)
|
||||
.then((aadApplication) => {
|
||||
aadApp = aadApplication;
|
||||
return this.createSpn(graphClient, aadApp.appId, tenantId);
|
||||
})
|
||||
.then((spn) => {
|
||||
aadApp.objectId = spn.objectId;
|
||||
return this.createRoleAssignment(session.credentials2, scope, aadApp.objectId);
|
||||
})
|
||||
.then(() => {
|
||||
return aadApp;
|
||||
})
|
||||
.catch((error) => {
|
||||
let errorMessage = error && error.message;
|
||||
if (!errorMessage && error["odata.error"]) {
|
||||
errorMessage = error["odata.error"]["message"];
|
||||
if (typeof errorMessage === "object") {
|
||||
errorMessage = errorMessage.value;
|
||||
}
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
public static generateAadApplicationName(accountName: string, projectName: string): string {
|
||||
let spnLengthAllowed = 92;
|
||||
const guid = uuid();
|
||||
projectName = projectName.replace(/[^a-zA-Z0-9_-]/g, "");
|
||||
accountName = accountName.replace(/[^a-zA-Z0-9_-]/g, "");
|
||||
const spnName = accountName + "-" + projectName + "-" + guid;
|
||||
if (spnName.length <= spnLengthAllowed) {
|
||||
return spnName;
|
||||
}
|
||||
|
||||
// 2 is subtracted for delimiter '-'
|
||||
spnLengthAllowed = spnLengthAllowed - guid.length - 2;
|
||||
if (accountName.length > spnLengthAllowed / 2 && projectName.length > spnLengthAllowed / 2) {
|
||||
accountName = accountName.substr(0, spnLengthAllowed / 2);
|
||||
projectName = projectName.substr(0, spnLengthAllowed - accountName.length);
|
||||
}
|
||||
else if (accountName.length > spnLengthAllowed / 2 && accountName.length + projectName.length > spnLengthAllowed) {
|
||||
accountName = accountName.substr(0, spnLengthAllowed - projectName.length);
|
||||
}
|
||||
else if (projectName.length > spnLengthAllowed / 2 && accountName.length + projectName.length > spnLengthAllowed) {
|
||||
projectName = projectName.substr(0, spnLengthAllowed - accountName.length);
|
||||
}
|
||||
|
||||
return accountName + "-" + projectName + "-" + guid;
|
||||
}
|
||||
|
||||
private static async getGraphToken(session: AzureSession): Promise<string> {
|
||||
const { activeDirectoryGraphResourceId } = session.environment;
|
||||
if (activeDirectoryGraphResourceId === undefined) {
|
||||
throw new Error(util.format(Messages.acquireAccessTokenFailed, "Active Directory Graph resource ID is undefined."));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const credentials = session.credentials2;
|
||||
credentials.authContext.acquireToken(activeDirectoryGraphResourceId, session.userId, credentials.clientId, function (err, tokenResponse) {
|
||||
if (err) {
|
||||
reject(new Error(util.format(Messages.acquireAccessTokenFailed, err.message)));
|
||||
} else if (tokenResponse.error) {
|
||||
reject(new Error(util.format(Messages.acquireAccessTokenFailed, tokenResponse.error)));
|
||||
} else {
|
||||
// This little casting workaround here allows us to not have to import adal-node
|
||||
// just for the typings. Really it's on adal-node for making the type
|
||||
// TokenResponse | ErrorResponse, even though TokenResponse has the same
|
||||
// error properties as ErrorResponse.
|
||||
resolve((tokenResponse as any).accessToken);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static async createAadApp(graphClient: RestClient, name: string, tenantId: string): Promise<AadApplication> {
|
||||
const secret = generateRandomPassword(20);
|
||||
const startDate = new Date(Date.now());
|
||||
|
||||
return graphClient.sendRequest<any>({
|
||||
url: `https://graph.windows.net/${tenantId}/applications`,
|
||||
queryParameters: {
|
||||
"api-version": "1.6"
|
||||
},
|
||||
method: "POST",
|
||||
body: {
|
||||
"availableToOtherTenants": false,
|
||||
"displayName": name,
|
||||
"homepage": "https://" + name,
|
||||
"passwordCredentials": [
|
||||
{
|
||||
"startDate": startDate,
|
||||
"endDate": new Date(startDate.getFullYear() + 1, startDate.getMonth()),
|
||||
"value": secret
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
return <AadApplication>{
|
||||
appId: data.appId,
|
||||
secret: secret
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static async createSpn(graphClient: RestClient, appId: string, tenantId: string): Promise<any> {
|
||||
const createSpnPromise = () => {
|
||||
return graphClient.sendRequest<any>({
|
||||
url: `https://graph.windows.net/${tenantId}/servicePrincipals`,
|
||||
queryParameters: {
|
||||
"api-version": "1.6"
|
||||
},
|
||||
method: "POST",
|
||||
body: {
|
||||
"appId": appId,
|
||||
"accountEnabled": "true"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return executeFunctionWithRetry(
|
||||
createSpnPromise,
|
||||
GraphHelper.retryCount,
|
||||
GraphHelper.retryTimeIntervalInSec,
|
||||
Messages.azureServicePrincipalFailedMessage);
|
||||
}
|
||||
|
||||
private static async createRoleAssignment(credentials: TokenCredentialsBase, scope: string, objectId: string): Promise<any> {
|
||||
const restClient = new RestClient(credentials);
|
||||
const createRoleAssignmentPromise = () => {
|
||||
return restClient.sendRequest<any>({
|
||||
url: `https://management.azure.com/${scope}/providers/Microsoft.Authorization/roleAssignments/${uuid()}`,
|
||||
queryParameters: {
|
||||
"api-version": "2021-04-01-preview" // So we have access to the "principalType" property
|
||||
},
|
||||
method: "PUT",
|
||||
body: {
|
||||
"properties": {
|
||||
"roleDefinitionId": `${scope}/providers/Microsoft.Authorization/roleDefinitions/${this.contributorRoleId}`,
|
||||
"principalId": objectId,
|
||||
"principalType": "ServicePrincipal", // Makes the assignment work for newly-created service principals
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return executeFunctionWithRetry(
|
||||
createRoleAssignmentPromise,
|
||||
GraphHelper.retryCount,
|
||||
GraphHelper.retryTimeIntervalInSec,
|
||||
Messages.roleAssignmentFailedMessage);
|
||||
}
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
import { PipelineTemplate, TargetResourceType, WebAppKind } from '../model/models';
|
||||
import * as fs from 'fs/promises';
|
||||
import Mustache from 'mustache';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from 'vscode-uri';
|
||||
|
||||
export async function analyzeRepoAndListAppropriatePipeline(repoUri: URI): Promise<PipelineTemplate[]> {
|
||||
// TO-DO: To populate the possible templates on the basis of azure target resource.
|
||||
let templateList = simpleWebAppTemplates;
|
||||
const analysisResult = await analyzeRepo(repoUri);
|
||||
|
||||
if (analysisResult.isNodeApplication) {
|
||||
// add all node application templates
|
||||
templateList = nodeTemplates.concat(templateList);
|
||||
}
|
||||
|
||||
if (analysisResult.isPythonApplication) {
|
||||
templateList = pythonTemplates.concat(templateList);
|
||||
}
|
||||
|
||||
if (analysisResult.isFunctionApplication) {
|
||||
templateList = functionTemplates.concat(templateList);
|
||||
}
|
||||
|
||||
if (analysisResult.isDotnetApplication) {
|
||||
templateList = dotnetTemplates.concat(templateList);
|
||||
}
|
||||
|
||||
// add all possible templates as we could not detect the appropriate onesı
|
||||
return templateList;
|
||||
}
|
||||
|
||||
export async function renderContent(templateFilePath: string, branch: string, resource: string | undefined, azureServiceConnection: string | undefined): Promise<string> {
|
||||
const data = await fs.readFile(templateFilePath, { encoding: "utf8" });
|
||||
return Mustache.render(data, {
|
||||
branch,
|
||||
resource,
|
||||
azureServiceConnection,
|
||||
});
|
||||
}
|
||||
|
||||
async function analyzeRepo(repoUri: URI): Promise<{ isNodeApplication: boolean, isFunctionApplication: boolean, isPythonApplication: boolean, isDotnetApplication: boolean }> {
|
||||
let contents: [string, vscode.FileType][];
|
||||
try {
|
||||
contents = await vscode.workspace.fs.readDirectory(repoUri);
|
||||
} catch {
|
||||
return {
|
||||
isNodeApplication: true,
|
||||
isFunctionApplication: true,
|
||||
isPythonApplication: true,
|
||||
isDotnetApplication: true,
|
||||
};
|
||||
}
|
||||
|
||||
const files = contents.filter(file => file[1] !== vscode.FileType.Directory).map(file => file[0]);
|
||||
|
||||
return {
|
||||
isNodeApplication: isNodeRepo(files),
|
||||
isFunctionApplication: isFunctionApp(files),
|
||||
isPythonApplication: isPythonRepo(files),
|
||||
isDotnetApplication: isDotnetApplication(files),
|
||||
// isContainerApplication: isDockerRepo(files)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function isNodeRepo(files: string[]): boolean {
|
||||
const nodeFilesRegex = /\.ts$|\.js$|package\.json$|node_modules/;
|
||||
return files.some((file) => nodeFilesRegex.test(file.toLowerCase()));
|
||||
}
|
||||
|
||||
function isFunctionApp(files: string[]): boolean {
|
||||
return files.some((file) => file.toLowerCase().endsWith("host.json"));
|
||||
}
|
||||
|
||||
function isPythonRepo(files: string[]): boolean {
|
||||
return files.some((file) => path.extname(file).toLowerCase() === '.py');
|
||||
}
|
||||
|
||||
function isDotnetApplication(files: string[]): boolean {
|
||||
return files.some((file) => ['.sln', '.csproj', '.fsproj'].includes(path.extname(file).toLowerCase()));
|
||||
}
|
||||
|
||||
const nodeTemplates: Array<PipelineTemplate> = [
|
||||
{
|
||||
label: 'Node.js with npm to Windows Web App',
|
||||
path: path.join(__dirname, 'configure/templates/nodejsWindowsWebApp.yml'),
|
||||
language: 'node',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.WindowsApp,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Node.js with Angular to Windows Web App',
|
||||
path: path.join(__dirname, 'configure/templates/nodejsWindowsWebAppAngular.yml'),
|
||||
language: 'node',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.WindowsApp,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Node.js with Gulp to Windows Web App',
|
||||
path: path.join(__dirname, 'configure/templates/nodejsWindowsWebAppGulp.yml'),
|
||||
language: 'node',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.WindowsApp,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Node.js with Grunt to Windows Web App',
|
||||
path: path.join(__dirname, 'configure/templates/nodejsWindowsWebAppGrunt.yml'),
|
||||
language: 'node',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.WindowsApp,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Node.js with Webpack to Windows Web App',
|
||||
path: path.join(__dirname, 'configure/templates/nodejsWindowsWebAppWebpack.yml'),
|
||||
language: 'node',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.WindowsApp,
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const pythonTemplates: Array<PipelineTemplate> = [
|
||||
{
|
||||
label: 'Python to Linux Web App on Azure',
|
||||
path: path.join(__dirname, 'configure/templates/pythonLinuxWebApp.yml'),
|
||||
language: 'python',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.LinuxApp,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Build and Test Python Django App',
|
||||
path: path.join(__dirname, 'configure/templates/pythonDjango.yml'),
|
||||
language: 'python',
|
||||
target: {
|
||||
type: TargetResourceType.None,
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const dotnetTemplates: Array<PipelineTemplate> = [
|
||||
{
|
||||
label: '.NET Web App to Windows on Azure',
|
||||
path: path.join(__dirname, 'configure/templates/dotnetWindowsWebApp.yml'),
|
||||
language: 'dotnet',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.WindowsApp,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '.NET Web App to Linux on Azure',
|
||||
path: path.join(__dirname, 'configure/templates/dotnetLinuxWebApp.yml'),
|
||||
language: 'dotnet',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.LinuxApp,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const simpleWebAppTemplates: Array<PipelineTemplate> = [
|
||||
{
|
||||
label: 'Simple application to Windows Web App',
|
||||
path: path.join(__dirname, 'configure/templates/simpleWebApp.yml'),
|
||||
language: 'none',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.WindowsApp,
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const functionTemplates: Array<PipelineTemplate> = [
|
||||
{
|
||||
label: 'Python Function App to Linux Azure Function',
|
||||
path: path.join(__dirname, 'configure/templates/pythonLinuxFunctionApp.yml'),
|
||||
language: 'python',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.FunctionAppLinux,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Node.js Function App to Linux Azure Function',
|
||||
path: path.join(__dirname, 'configure/templates/nodejsLinuxFunctionApp.yml'),
|
||||
language: 'node',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.FunctionAppLinux,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '.NET Function App to Windows Azure Function',
|
||||
path: path.join(__dirname, 'configure/templates/dotnetWindowsFunctionApp.yml'),
|
||||
language: 'dotnet',
|
||||
target: {
|
||||
type: TargetResourceType.WebApp,
|
||||
kind: WebAppKind.FunctionApp,
|
||||
},
|
||||
},
|
||||
]
|
|
@ -1,8 +0,0 @@
|
|||
// https://github.com/microsoft/vscode-azuretools/blob/5999c2ad4423e86f22d2c648027242d8816a50e4/ui/src/errors.ts
|
||||
// minus localization
|
||||
|
||||
export class UserCancelledError extends Error {
|
||||
constructor() {
|
||||
super('Operation cancelled.');
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
import { QuickPickItem } from 'vscode';
|
||||
import { TeamProject } from 'azure-devops-node-api/interfaces/CoreInterfaces';
|
||||
import { WebApi } from 'azure-devops-node-api';
|
||||
import { AppServiceClient } from '../clients/azure/appServiceClient';
|
||||
import { Build, BuildDefinition } from 'azure-devops-node-api/interfaces/BuildInterfaces';
|
||||
import { WebSiteManagementModels } from '@azure/arm-appservice';
|
||||
import { AzureSession } from '../../typings/azure-account.api';
|
||||
|
||||
export interface Organization {
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
accountUri: string;
|
||||
properties: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identical to @see {TeamProject} except with name & id verified.
|
||||
*/
|
||||
export interface ValidatedProject extends TeamProject {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identical to @see {WebSiteManagementModels.Site} except with name, id, & resourceGroup verified.
|
||||
*/
|
||||
export interface ValidatedSite extends WebSiteManagementModels.Site {
|
||||
name: string;
|
||||
id: string;
|
||||
resourceGroup: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identical to @see {Build} except with definition & id verified.
|
||||
*/
|
||||
export interface ValidatedBuild extends Build {
|
||||
definition: Required<BuildDefinition>;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export type OrganizationAvailability = {
|
||||
isAvailable: true;
|
||||
name: string;
|
||||
unavailabilityReason: null;
|
||||
} | {
|
||||
isAvailable: false;
|
||||
name: string;
|
||||
unavailabilityReason: string;
|
||||
};
|
||||
|
||||
export interface AzureSiteDetails {
|
||||
appServiceClient: AppServiceClient;
|
||||
subscriptionId: string;
|
||||
site: ValidatedSite;
|
||||
}
|
||||
|
||||
export type GitRepositoryDetails = {
|
||||
repositoryName: string;
|
||||
remoteName: string;
|
||||
remoteUrl: string;
|
||||
branch: string;
|
||||
} & ({
|
||||
repositoryProvider: RepositoryProvider.AzureRepos;
|
||||
organizationName: string;
|
||||
projectName: string;
|
||||
} | {
|
||||
repositoryProvider: RepositoryProvider.Github;
|
||||
ownerName: string;
|
||||
});
|
||||
|
||||
export interface AzureDevOpsDetails {
|
||||
session: AzureSession;
|
||||
adoClient: WebApi;
|
||||
organizationName: string;
|
||||
project: ValidatedProject;
|
||||
}
|
||||
|
||||
export interface PipelineTemplate {
|
||||
path: string;
|
||||
label: string;
|
||||
language: string;
|
||||
target: TargetResource;
|
||||
}
|
||||
|
||||
export enum SourceOptions {
|
||||
CurrentWorkspace = 'Current workspace',
|
||||
BrowseLocalMachine = 'Browse local machine',
|
||||
GithubRepository = 'Github repository'
|
||||
}
|
||||
|
||||
export enum RepositoryProvider {
|
||||
Github = 'github',
|
||||
AzureRepos = 'tfsgit'
|
||||
}
|
||||
|
||||
export type TargetResource = {
|
||||
type: TargetResourceType.None;
|
||||
} | {
|
||||
type: TargetResourceType.WebApp;
|
||||
kind: WebAppKind;
|
||||
};
|
||||
|
||||
export enum TargetResourceType {
|
||||
None = 'none',
|
||||
WebApp = 'Microsoft.Web/sites'
|
||||
}
|
||||
|
||||
export enum WebAppKind {
|
||||
WindowsApp = 'app',
|
||||
FunctionApp = 'functionapp',
|
||||
FunctionAppLinux = 'functionapp,linux',
|
||||
LinuxApp = 'app,linux',
|
||||
LinuxContainerApp = 'app,linux,container'
|
||||
}
|
||||
|
||||
export interface QuickPickItemWithData<T> extends QuickPickItem {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface AadApplication {
|
||||
appId: string;
|
||||
secret: string;
|
||||
objectId: string;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export const HostedVS2017QueueName: string = "Hosted VS2017";
|
||||
export const SelectProject = 'selectProject';
|
||||
export const SelectPipelineTemplate = 'selectPipelineTemplate';
|
||||
export const SelectSubscription = 'selectSubscription';
|
||||
export const GitHubPat = 'gitHubPat';
|
||||
export const SelectFromMultipleWorkSpace = 'selectFromMultipleWorkSpace';
|
||||
export const SelectRemoteForRepo = 'selectRemoteForRepo';
|
|
@ -1,8 +0,0 @@
|
|||
// Failure trace points
|
||||
export const AddingContentToPipelineFileFailed = 'AddingContentToPipelineFileFailed';
|
||||
export const AzureServiceConnectionCreateFailure = 'AzureServiceConnectionCreateFailure';
|
||||
export const CheckInPipelineFailure = 'checkInPipelineFailure';
|
||||
export const CreateAndQueuePipelineFailed = 'createAndBuildPipelineFailed';
|
||||
export const GitHubServiceConnectionError = 'gitHubServiceConnectionError';
|
||||
export const PipelineFileCheckInFailed = 'PipelineFileCheckInFailed';
|
||||
export const PostDeploymentActionFailed = 'PostDeploymentActionFailed';
|
|
@ -1,95 +0,0 @@
|
|||
# .NET Web App to Linux on Azure
|
||||
# Build a .NET Web App and deploy it to Azure as a Linux Web App.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/dotnet-core
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection created during pipeline creation
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web App name
|
||||
webAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'ubuntu-latest'
|
||||
|
||||
# Build Configuration
|
||||
buildConfiguration: 'Release'
|
||||
|
||||
# Build Projects
|
||||
buildProjects: "**/*.csproj"
|
||||
|
||||
# Test Projects
|
||||
testProjects: "**/*[Tt]est*/*.csproj"
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Restore
|
||||
inputs:
|
||||
command: 'restore'
|
||||
publishWebProjects: true
|
||||
projects: $(buildProjects)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Build
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: $(buildProjects)
|
||||
arguments: --configuration $(buildConfiguration)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Test
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: $(testProjects)
|
||||
publishWebProjects: true
|
||||
arguments: --configuration $(buildConfiguration)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Publish
|
||||
inputs:
|
||||
command: 'publish'
|
||||
publishWebProjects: true
|
||||
arguments: --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)
|
||||
zipAfterPublish: true
|
||||
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
displayName: Deploy
|
||||
environment: $(webAppName)
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureRMWebAppDeployment@4
|
||||
displayName: "Deploy Azure Web App"
|
||||
inputs:
|
||||
ConnectionType: "AzureRM"
|
||||
ConnectedServiceName: $(azureSubscription)
|
||||
WebAppName: $(webAppName)
|
||||
WebAppKind: webAppLinux
|
||||
Package: $(Pipeline.Workspace)/**/*.zip
|
||||
DeploymentType: "webDeploy"
|
|
@ -1,95 +0,0 @@
|
|||
# .NET Function App to Windows on Azure
|
||||
# Build a .NET function app and deploy it to Azure as a Windows function App.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/dotnet-core
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection created during pipeline creation
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Function app name
|
||||
functionAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'windows-latest'
|
||||
|
||||
# Build Configuration
|
||||
buildConfiguration: 'Release'
|
||||
|
||||
# Build Projects
|
||||
buildProjects: "**/*.csproj"
|
||||
|
||||
# Test Projects
|
||||
testProjects: "**/*[Tt]est*/*.csproj"
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Restore
|
||||
inputs:
|
||||
command: 'restore'
|
||||
publishWebProjects: true
|
||||
projects: $(buildProjects)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Build
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: $(buildProjects)
|
||||
arguments: --configuration $(buildConfiguration)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Test
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: $(testProjects)
|
||||
publishWebProjects: true
|
||||
arguments: --configuration $(buildConfiguration)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Publish
|
||||
inputs:
|
||||
command: 'publish'
|
||||
publishWebProjects: true
|
||||
arguments: --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)
|
||||
zipAfterPublish: true
|
||||
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
displayName: Deploy
|
||||
environment: $(functionAppName)
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
|
||||
steps:
|
||||
- task: AzureFunctionApp@1
|
||||
displayName: 'Azure functions app deploy'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
appType: functionApp
|
||||
appName: $(functionAppName)
|
||||
package: '$(Pipeline.Workspace)/**/*.zip'
|
|
@ -1,95 +0,0 @@
|
|||
# .NET Web App to Windows on Azure
|
||||
# Build a .NET Web App and deploy it to Azure as a Windows Web App.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/dotnet-core
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection created during pipeline creation
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web App name
|
||||
webAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'windows-latest'
|
||||
|
||||
# Build Configuration
|
||||
buildConfiguration: 'Release'
|
||||
|
||||
# Build Projects
|
||||
buildProjects: "**/*.csproj"
|
||||
|
||||
# Test Projects
|
||||
testProjects: "**/*[Tt]est*/*.csproj"
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Restore
|
||||
inputs:
|
||||
command: 'restore'
|
||||
publishWebProjects: true
|
||||
projects: $(buildProjects)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Build
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: $(buildProjects)
|
||||
arguments: --configuration $(buildConfiguration)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Test
|
||||
inputs:
|
||||
command: 'test'
|
||||
projects: $(testProjects)
|
||||
publishWebProjects: true
|
||||
arguments: --configuration $(buildConfiguration)
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Publish
|
||||
inputs:
|
||||
command: 'publish'
|
||||
publishWebProjects: true
|
||||
arguments: --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)
|
||||
zipAfterPublish: true
|
||||
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
displayName: Deploy
|
||||
environment: $(webAppName)
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureRMWebAppDeployment@4
|
||||
displayName: "Deploy Azure Web App"
|
||||
inputs:
|
||||
ConnectionType: "AzureRM"
|
||||
ConnectedServiceName: $(azureSubscription)
|
||||
WebAppName: $(webAppName)
|
||||
WebAppKind: webApp
|
||||
Package: $(Pipeline.Workspace)/**/*.zip
|
||||
DeploymentType: "webDeploy"
|
|
@ -1,78 +0,0 @@
|
|||
# Node.js Function App to Linux on Azure
|
||||
# Build a Node.js function app and deploy it to Azure as a Windows function app.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web app name
|
||||
functionAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'ubuntu-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
displayName: 'Install Node.js'
|
||||
|
||||
- script: |
|
||||
if [ -f extensions.csproj ]
|
||||
then
|
||||
dotnet build extensions.csproj --runtime ubuntu.16.04-x64 --output ./bin
|
||||
fi
|
||||
displayName: 'Build extensions'
|
||||
- script: |
|
||||
npm install
|
||||
npm run build --if-present
|
||||
npm run test --if-present
|
||||
displayName: 'Prepare binaries'
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '.'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
displayName: Deploy
|
||||
environment: $(functionAppName)
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureFunctionApp@1
|
||||
displayName: 'Azure Functions App Deploy: {{ functionAppName }}'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
appType: functionAppLinux
|
||||
appName: $(functionAppName)
|
||||
package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'
|
|
@ -1,66 +0,0 @@
|
|||
# Node.js App on Windows Web App
|
||||
# Build a Node.js app and deploy it to Azure as a Windows web app.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web app name
|
||||
webAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'windows-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
npm install
|
||||
npm build
|
||||
displayName: 'npm install and build'
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '.'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
environment: 'development'
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Azure Web App Deploy'
|
||||
inputs:
|
||||
azureSubscription: $(azureSubscription)
|
||||
appType: webApp
|
||||
appName: $(webAppName)
|
||||
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
|
|
@ -1,68 +0,0 @@
|
|||
# Node.js with Angular on Windows Web App
|
||||
# Build a Node.js project that uses Angular and deploy it to Azure as a Windows web app.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web app name
|
||||
webAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'windows-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
npm install -g @angular/cli
|
||||
npm install
|
||||
ng build --prod
|
||||
displayName: 'npm install and build'
|
||||
workingDirectory: $(workingDirectory)
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: $(workingDirectory)
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
environment: 'development'
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Azure Web App Deploy'
|
||||
inputs:
|
||||
azureSubscription: $(azureSubscription)
|
||||
appType: webApp
|
||||
appName: $(webAppName)
|
||||
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
|
|
@ -1,66 +0,0 @@
|
|||
# Node.js with Grunt on Windows Web App
|
||||
# Build a Node.js project using the Grunt task runner and deploy it to Azure as a Windows web app.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web app name
|
||||
webAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'windows-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
npm install
|
||||
grunt --gruntfile Gruntfile.js
|
||||
displayName: 'npm install and run grunt'
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '.'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
environment: 'development'
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Azure Web App Deploy'
|
||||
inputs:
|
||||
azureSubscription: $(azureSubscription)
|
||||
appType: webApp
|
||||
appName: $(webAppName)
|
||||
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
|
|
@ -1,66 +0,0 @@
|
|||
# Node.js with gulp on Windows Web App
|
||||
# Build a Node.js project using the gulp task runner and deploy it to Azure as a Windows web app.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web app name
|
||||
webAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'windows-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
npm install
|
||||
gulp default --gulpfile gulpfile.js
|
||||
displayName: 'npm install and run gulp'
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '.'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
environment: 'development'
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Azure Web App Deploy'
|
||||
inputs:
|
||||
azureSubscription: $(azureSubscription)
|
||||
appType: webApp
|
||||
appName: $(webAppName)
|
||||
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
|
|
@ -1,67 +0,0 @@
|
|||
# Node.js with webpack on Windows Web App
|
||||
# Build a Node.js project using the webpack CLI and deploy it to Azure as a Windows web app.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web app name
|
||||
webAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'windows-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
npm install -g webpack webpack-cli --save-dev
|
||||
npm install
|
||||
npx webpack --config webpack.config.js
|
||||
displayName: 'npm install, run webpack'
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '.'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
environment: 'development'
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Azure Web App Deploy'
|
||||
inputs:
|
||||
azureSubscription: $(azureSubscription)
|
||||
appType: webApp
|
||||
appName: $(webAppName)
|
||||
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
|
|
@ -1,43 +0,0 @@
|
|||
# Python Django
|
||||
# Test a Django project on multiple versions of Python.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/python
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- task: PythonScript@0
|
||||
displayName: 'Export project path'
|
||||
inputs:
|
||||
scriptSource: 'inline'
|
||||
script: |
|
||||
"""Search all subdirectories for `manage.py`."""
|
||||
from glob import iglob
|
||||
from os import path
|
||||
manage_py = next(iglob(path.join('**', 'manage.py'), recursive=True), None)
|
||||
if not manage_py:
|
||||
raise SystemExit('Could not find a Django project')
|
||||
project_location = path.dirname(path.abspath(manage_py))
|
||||
print('Found Django project in', project_location)
|
||||
print('##vso[task.setvariable variable=projectRoot]{}'.format(project_location))
|
||||
|
||||
- script: |
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
pip install -r requirements.txt
|
||||
pip install unittest-xml-reporting
|
||||
displayName: 'Install prerequisites'
|
||||
|
||||
- script: |
|
||||
pushd '$(projectRoot)'
|
||||
python manage.py test --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner --no-input
|
||||
displayName: 'Run tests'
|
||||
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFiles: "**/TEST-*.xml"
|
||||
testRunTitle: 'Python $(PYTHON_VERSION)'
|
||||
condition: succeededOrFailed()
|
|
@ -1,76 +0,0 @@
|
|||
# Python Function App to Linux on Azure
|
||||
# Build a Python function app and deploy it to Azure as a Linux function app.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/python
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web app name
|
||||
functionAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'ubuntu-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- bash: |
|
||||
if [ -f extensions.csproj ]
|
||||
then
|
||||
dotnet build extensions.csproj --output ./bin
|
||||
fi
|
||||
displayName: 'Build extensions'
|
||||
|
||||
- bash: |
|
||||
python -m venv worker_venv
|
||||
source worker_venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
displayName: 'Install application dependencies'
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '.'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
displayName: Deploy
|
||||
environment: $(functionAppName)
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureFunctionApp@1
|
||||
displayName: 'Azure functions app deploy'
|
||||
inputs:
|
||||
azureSubscription: '$(azureSubscription)'
|
||||
appType: functionAppLinux
|
||||
appName: $(functionAppName)
|
||||
package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'
|
|
@ -1,70 +0,0 @@
|
|||
# Python to Linux Web App on Azure
|
||||
# Build a Python project and deploy it to Azure as a Linux Web App.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/python
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection created during pipeline creation
|
||||
azureServiceConnectionId: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web app name
|
||||
webAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'ubuntu-latest'
|
||||
|
||||
# Environment name
|
||||
environmentName: '{{{ resource }}}'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: BuildJob
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- script: |
|
||||
python -m venv antenv
|
||||
source antenv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
pip install setup
|
||||
pip install -r requirements.txt
|
||||
displayName: "Install requirements"
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '.'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
displayName: 'Upload package'
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: 'Deploy Web App'
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: DeploymentJob
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
environment: $(environmentName)
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Deploy Azure Web App : {{ webAppName }}'
|
||||
inputs:
|
||||
azureSubscription: $(azureServiceConnectionId)
|
||||
appName: $(webAppName)
|
||||
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
|
|
@ -1,60 +0,0 @@
|
|||
# Simple App on Windows Web App
|
||||
# Package and deploy a simple web application and deploy it to Azure as Windows web app.
|
||||
|
||||
trigger:
|
||||
- {{{ branch }}}
|
||||
|
||||
variables:
|
||||
# Azure Resource Manager connection
|
||||
azureSubscription: '{{{ azureServiceConnection }}}'
|
||||
|
||||
# Web app name
|
||||
webAppName: '{{{ resource }}}'
|
||||
|
||||
# Agent VM image name
|
||||
vmImageName: 'windows-latest'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: Build stage
|
||||
jobs:
|
||||
- job: BuildJob
|
||||
displayName: Build
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Archive files'
|
||||
inputs:
|
||||
rootFolderOrFile: '.'
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
replaceExistingArchive: true
|
||||
|
||||
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
|
||||
artifact: drop
|
||||
|
||||
- stage: Deploy
|
||||
displayName: Deploy stage
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
environment: 'development'
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Azure Web App Deploy'
|
||||
inputs:
|
||||
azureSubscription: $(azureSubscription)
|
||||
appType: webApp
|
||||
appName: $(webAppName)
|
||||
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
|
|
@ -11,7 +11,6 @@ import * as logger from './logger';
|
|||
import { getSchemaAssociation, locateSchemaFile, onDidSelectOrganization, resetDoNotAskState, SchemaAssociationNotification } from './schema-association-service';
|
||||
import { schemaContributor, CUSTOM_SCHEMA_REQUEST, CUSTOM_CONTENT_REQUEST } from './schema-contributor';
|
||||
import { telemetryHelper } from './helpers/telemetryHelper';
|
||||
import { getAzureAccountExtensionApi } from './extensionApis';
|
||||
|
||||
/**
|
||||
* The unique string that identifies the Azure Pipelines languge.
|
||||
|
@ -27,15 +26,9 @@ const DOCUMENT_SELECTOR = [
|
|||
]
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const configurePipelineEnabled = vscode.workspace.getConfiguration(LANGUAGE_IDENTIFIER).get<boolean>('configure', true);
|
||||
telemetryHelper.setTelemetry('isActivationEvent', 'true');
|
||||
telemetryHelper.setTelemetry('configurePipelineEnabled', `${configurePipelineEnabled}`);
|
||||
await telemetryHelper.callWithTelemetryAndErrorHandling('azurePipelines.activate', async () => {
|
||||
await activateYmlContributor(context);
|
||||
if (configurePipelineEnabled) {
|
||||
const { activateConfigurePipeline } = await import('./configure/activate');
|
||||
activateConfigurePipeline();
|
||||
}
|
||||
});
|
||||
|
||||
context.subscriptions.push(telemetryHelper);
|
||||
|
@ -103,9 +96,8 @@ async function activateYmlContributor(context: vscode.ExtensionContext) {
|
|||
|
||||
// Re-request the schema when sessions change since auto-detection is dependent on
|
||||
// being able to query ADO organizations, check if 1ESPT schema can be used using session credentials.
|
||||
const azureAccountApi = await getAzureAccountExtensionApi();
|
||||
context.subscriptions.push(azureAccountApi.onSessionsChanged(async () => {
|
||||
if (azureAccountApi.status === 'LoggedIn' || azureAccountApi.status === 'LoggedOut') {
|
||||
context.subscriptions.push(vscode.authentication.onDidChangeSessions(async session => {
|
||||
if (session.provider.id === 'microsoft') {
|
||||
await loadSchema(context, client);
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -1,26 +1,7 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as Messages from './messages';
|
||||
import { AzureAccount } from './typings/azure-account.api';
|
||||
import { API, GitExtension } from './typings/git';
|
||||
|
||||
let azureAccountExtensionApi: AzureAccount | undefined;
|
||||
export async function getAzureAccountExtensionApi(): Promise<AzureAccount> {
|
||||
if (azureAccountExtensionApi === undefined) {
|
||||
const azureAccountExtension = vscode.extensions.getExtension<AzureAccount>("ms-vscode.azure-account");
|
||||
if (!azureAccountExtension) {
|
||||
throw new Error(Messages.azureAccountExtensionUnavailable);
|
||||
}
|
||||
|
||||
if (!azureAccountExtension.isActive) {
|
||||
await azureAccountExtension.activate();
|
||||
}
|
||||
|
||||
azureAccountExtensionApi = azureAccountExtension.exports;
|
||||
}
|
||||
|
||||
return azureAccountExtensionApi;
|
||||
}
|
||||
|
||||
let gitExtensionApi: API | undefined;
|
||||
export async function getGitExtensionApi(): Promise<API> {
|
||||
if (gitExtensionApi === undefined) {
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import * as Messages from '../messages';
|
||||
|
||||
// https://dev.azure.com/ OR https://org@dev.azure.com/
|
||||
const AzureReposUrl = 'dev.azure.com/';
|
||||
|
||||
// git@ssh.dev.azure.com:v3/
|
||||
const SSHAzureReposUrl = 'ssh.dev.azure.com:v3/';
|
||||
|
||||
// https://org.visualstudio.com/
|
||||
const VSOUrl = '.visualstudio.com/';
|
||||
|
||||
// org@vs-ssh.visualstudio.com:v3/
|
||||
const SSHVsoReposUrl = 'vs-ssh.visualstudio.com:v3/';
|
||||
|
||||
export function isAzureReposUrl(remoteUrl: string): boolean {
|
||||
return remoteUrl.includes(AzureReposUrl) ||
|
||||
remoteUrl.includes(VSOUrl) ||
|
||||
remoteUrl.includes(SSHAzureReposUrl) ||
|
||||
remoteUrl.includes(SSHVsoReposUrl);
|
||||
}
|
||||
|
||||
export function getRepositoryDetailsFromRemoteUrl(remoteUrl: string): { organizationName: string, projectName: string, repositoryName: string } {
|
||||
if (remoteUrl.includes(AzureReposUrl)) {
|
||||
const part = remoteUrl.substring(remoteUrl.indexOf(AzureReposUrl) + AzureReposUrl.length);
|
||||
const parts = part.split('/');
|
||||
if (parts.length !== 4) {
|
||||
throw new Error(Messages.failedToDetermineAzureRepoDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
organizationName: parts[0].trim(),
|
||||
projectName: parts[1].trim(),
|
||||
repositoryName: parts[3].trim()
|
||||
};
|
||||
} else if (remoteUrl.includes(VSOUrl)) {
|
||||
const part = remoteUrl.substring(remoteUrl.indexOf(VSOUrl) + VSOUrl.length);
|
||||
const organizationName = remoteUrl.substring(remoteUrl.indexOf('https://') + 'https://'.length, remoteUrl.indexOf('.visualstudio.com'));
|
||||
const parts = part.split('/');
|
||||
|
||||
if (parts.length === 4 && parts[0].toLowerCase() === 'defaultcollection') {
|
||||
// Handle scenario where part is 'DefaultCollection/<project>/_git/<repository>'
|
||||
parts.shift();
|
||||
}
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error(Messages.failedToDetermineAzureRepoDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
organizationName: organizationName,
|
||||
projectName: parts[0].trim(),
|
||||
repositoryName: parts[2].trim()
|
||||
};
|
||||
} else if (remoteUrl.includes(SSHAzureReposUrl) || remoteUrl.includes(SSHVsoReposUrl)) {
|
||||
const urlFormat = remoteUrl.includes(SSHAzureReposUrl) ? SSHAzureReposUrl : SSHVsoReposUrl;
|
||||
const part = remoteUrl.substring(remoteUrl.indexOf(urlFormat) + urlFormat.length);
|
||||
const parts = part.split('/');
|
||||
if (parts.length !== 3) {
|
||||
throw new Error(Messages.failedToDetermineAzureRepoDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
organizationName: parts[0].trim(),
|
||||
projectName: parts[1].trim(),
|
||||
repositoryName: parts[2].trim()
|
||||
};
|
||||
} else {
|
||||
throw new Error(Messages.notAzureRepoUrl);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { InputBoxOptions, QuickPickItem, QuickPickOptions, window } from 'vscode';
|
||||
import { telemetryHelper } from '../../helpers/telemetryHelper';
|
||||
import * as TelemetryKeys from '../../helpers/telemetryKeys';
|
||||
import { telemetryHelper } from './telemetryHelper';
|
||||
import * as TelemetryKeys from './telemetryKeys';
|
||||
|
||||
export async function showQuickPick<T extends QuickPickItem>(listName: string, listItems: T[] | Thenable<T[]>, options: QuickPickOptions, itemCountTelemetryKey?: string): Promise<T | undefined> {
|
||||
try {
|
|
@ -1,247 +0,0 @@
|
|||
// Copied from https://github.com/microsoft/vscode-azuretools/blob/5999c2ad4423e86f22d2c648027242d8816a50e4/ui/src/parseError.ts
|
||||
// with inline IParsedError interface and no localization
|
||||
|
||||
// Disable linting precisely because this file is copied.
|
||||
/* eslint-disable */
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as htmlToText from 'html-to-text';
|
||||
|
||||
export interface IParsedError {
|
||||
errorType: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
isUserCancelledError: boolean;
|
||||
}
|
||||
|
||||
// tslint:disable:no-unsafe-any
|
||||
// tslint:disable:no-any
|
||||
export function parseError(error: any): IParsedError {
|
||||
let errorType: string = '';
|
||||
let message: string = '';
|
||||
let stack: string | undefined;
|
||||
|
||||
if (typeof (error) === 'object' && error !== null) {
|
||||
if (error.constructor !== Object) {
|
||||
errorType = error.constructor.name;
|
||||
}
|
||||
|
||||
stack = getCallstack(error);
|
||||
errorType = getCode(error, errorType);
|
||||
|
||||
// See https://github.com/Microsoft/vscode-azureappservice/issues/419 for an example error that requires these 'unpack's
|
||||
error = unpackErrorFromField(error, 'value');
|
||||
error = unpackErrorFromField(error, '_value');
|
||||
error = unpackErrorFromField(error, 'error');
|
||||
error = unpackErrorFromField(error, 'error');
|
||||
if (Array.isArray(error.errors) && error.errors.length) {
|
||||
error = error.errors[0];
|
||||
}
|
||||
|
||||
errorType = getCode(error, errorType);
|
||||
message = getMessage(error, message);
|
||||
|
||||
if (!errorType || !message || /error.*deserializing.*response.*body/i.test(message)) {
|
||||
error = unpackErrorFromField(error, 'response');
|
||||
error = unpackErrorFromField(error, 'body');
|
||||
|
||||
errorType = getCode(error, errorType);
|
||||
message = getMessage(error, message);
|
||||
}
|
||||
|
||||
// Azure errors have a JSON object in the message
|
||||
let parsedMessage: any = parseIfJson(error.message);
|
||||
// For some reason, the message is sometimes serialized twice and we need to parse it again
|
||||
parsedMessage = parseIfJson(parsedMessage);
|
||||
// Extract out the "internal" error if it exists
|
||||
if (parsedMessage && parsedMessage.error) {
|
||||
parsedMessage = parsedMessage.error;
|
||||
}
|
||||
|
||||
errorType = getCode(parsedMessage, errorType);
|
||||
message = getMessage(parsedMessage, message);
|
||||
|
||||
message = message || convertCodeToError(errorType) || JSON.stringify(error);
|
||||
} else if (error !== undefined && error !== null && error.toString && error.toString().trim() !== '') {
|
||||
errorType = typeof (error);
|
||||
message = error.toString();
|
||||
}
|
||||
|
||||
message = unpackErrorsInMessage(message);
|
||||
|
||||
[message, errorType] = parseIfFileSystemError(message, errorType);
|
||||
|
||||
// tslint:disable-next-line:strict-boolean-expressions
|
||||
errorType = errorType || typeof (error);
|
||||
message = message || 'Unknown Error';
|
||||
|
||||
message = parseIfHtml(message);
|
||||
|
||||
// Azure storage SDK errors are presented in XML
|
||||
// https://github.com/Azure/azure-sdk-for-js/issues/6927
|
||||
message = parseIfXml(message);
|
||||
|
||||
return {
|
||||
errorType: errorType,
|
||||
message: message,
|
||||
stack: stack,
|
||||
// NOTE: Intentionally not using 'error instanceof UserCancelledError' because that doesn't work if multiple versions of the UI package are used in one extension
|
||||
// See https://github.com/Microsoft/vscode-azuretools/issues/51 for more info
|
||||
isUserCancelledError: errorType === 'UserCancelledError'
|
||||
};
|
||||
}
|
||||
|
||||
function convertCodeToError(errorType: string | undefined): string | undefined {
|
||||
if (errorType) {
|
||||
const code: number = parseInt(errorType, 10);
|
||||
if (!isNaN(code)) {
|
||||
return `Failed with code "${code}".`;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseIfJson(o: any): any {
|
||||
if (typeof o === 'string' && o.indexOf('{') >= 0) {
|
||||
try {
|
||||
return JSON.parse(o);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
function parseIfHtml(message: string): string {
|
||||
if (/<html/i.test(message)) {
|
||||
try {
|
||||
return htmlToText.fromString(message, { wordwrap: false, uppercaseHeadings: false, ignoreImage: true });
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function parseIfXml(message: string): string {
|
||||
const matches: RegExpMatchArray | null = message.match(/<Message>(.*)<\/Message>/si);
|
||||
if (matches) {
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function getMessage(o: any, defaultMessage: string): string {
|
||||
return (o && (o.message || o.Message || o.detail || (typeof parseIfJson(o.body) === 'string' && o.body))) || defaultMessage;
|
||||
}
|
||||
|
||||
function getCode(o: any, defaultCode: string): string {
|
||||
const code: any = o && (o.code || o.Code || o.errorCode || o.statusCode);
|
||||
return code ? String(code) : defaultCode;
|
||||
}
|
||||
|
||||
function unpackErrorsInMessage(message: string): string {
|
||||
// Handle messages like this from Azure (just handle first error for now)
|
||||
// ["Errors":["The offer should have valid throughput]]",
|
||||
if (message) {
|
||||
const errorsInMessage: RegExpMatchArray | null = message.match(/"Errors":\[\s*"([^"]+)"/);
|
||||
if (errorsInMessage !== null) {
|
||||
const [, firstError] = errorsInMessage;
|
||||
return firstError;
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function unpackErrorFromField(error: any, prop: string): any {
|
||||
// Handle objects from Azure SDK that contain the error information in a "body" field (serialized or not)
|
||||
let field: any = error && error[prop];
|
||||
if (field) {
|
||||
if (typeof field === 'string' && field.indexOf('{') >= 0) {
|
||||
try {
|
||||
field = JSON.parse(field);
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof field === 'object') {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example line in the stack:
|
||||
* at FileService.StorageServiceClient._processResponse (/path/ms-azuretools.vscode-azurestorage-0.6.0/node_modules/azure-storage/lib/common/services/storageserviceclient.js:751:50)
|
||||
*
|
||||
* Final minified line:
|
||||
* FileService.StorageServiceClient._processResponse azure-storage/storageserviceclient.js:751:50
|
||||
*/
|
||||
function getCallstack(error: { stack?: string }): string | undefined {
|
||||
// tslint:disable-next-line: strict-boolean-expressions
|
||||
const stack: string = error.stack || '';
|
||||
|
||||
const minifiedLines: (string | undefined)[] = stack
|
||||
.split(/(\r\n|\n)/g) // split by line ending
|
||||
.map(l => {
|
||||
let result: string = '';
|
||||
// Get just the file name, line number and column number
|
||||
// From above example: storageserviceclient.js:751:50
|
||||
const fileMatch: RegExpMatchArray | null = l.match(/[^\/\\\(\s]+\.(t|j)s:[0-9]+:[0-9]+/i);
|
||||
|
||||
// Ignore any lines without a file match (e.g. "at Generator.next (<anonymous>)")
|
||||
if (fileMatch) {
|
||||
// Get the function name
|
||||
// From above example: FileService.StorageServiceClient._processResponse
|
||||
const functionMatch: RegExpMatchArray | null = l.match(/^[\s]*at ([^\(\\\/]+(?:\\|\/)?)+/i);
|
||||
if (functionMatch) {
|
||||
result += functionMatch[1];
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
// Get the name of the node module (and any sub modules) containing the file
|
||||
// From above example: azure-storage
|
||||
const moduleRegExp: RegExp = /node_modules(?:\\|\/)([^\\\/]+)/ig;
|
||||
let moduleMatch: RegExpExecArray | null;
|
||||
do {
|
||||
moduleMatch = moduleRegExp.exec(l);
|
||||
if (moduleMatch) {
|
||||
parts.push(moduleMatch[1]);
|
||||
}
|
||||
} while (moduleMatch);
|
||||
|
||||
parts.push(fileMatch[0]);
|
||||
result += parts.join('/');
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.filter(l => !!l);
|
||||
|
||||
return minifiedLines.length > 0 ? minifiedLines.join('\n') : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://github.com/microsoft/vscode-cosmosdb/issues/1580 for an example error
|
||||
*/
|
||||
function parseIfFileSystemError(message: string, errorType: string): [string, string] {
|
||||
const match: RegExpMatchArray | null = message.match(/\((([a-z]*) \(FileSystemError\).*)\)$/i);
|
||||
if (match) {
|
||||
message = match[1];
|
||||
errorType = match[2];
|
||||
}
|
||||
return [message, errorType];
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
|
||||
import * as TelemetryKeys from './telemetryKeys';
|
||||
import * as logger from '../logger';
|
||||
import { parseError } from './parseError';
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const extensionName = 'ms-azure-devops.azure-pipelines';
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
|
||||
const packageJSON = vscode.extensions.getExtension(extensionName)?.packageJSON; // Guaranteed to exist
|
||||
const extensionVersion: string = packageJSON.version;
|
||||
export const extensionVersion: string = packageJSON.version;
|
||||
const aiKey: string = packageJSON.aiKey;
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
|
@ -18,19 +18,11 @@ interface TelemetryProperties {
|
|||
[key: string]: string;
|
||||
}
|
||||
|
||||
enum Result {
|
||||
Succeeded = 'Succeeded',
|
||||
Failed = 'Failed',
|
||||
Canceled = 'Canceled'
|
||||
}
|
||||
|
||||
|
||||
class TelemetryHelper {
|
||||
private journeyId: string = uuid();
|
||||
private journeyId: string = crypto.randomUUID();
|
||||
|
||||
private properties: TelemetryProperties = {
|
||||
[TelemetryKeys.JourneyId]: this.journeyId,
|
||||
[TelemetryKeys.Result]: Result.Succeeded,
|
||||
};
|
||||
|
||||
private static reporter = new TelemetryReporter(extensionName, extensionVersion, aiKey);
|
||||
|
@ -47,10 +39,6 @@ class TelemetryHelper {
|
|||
this.properties[key] = value;
|
||||
}
|
||||
|
||||
public setCurrentStep(stepName: string): void {
|
||||
this.properties.cancelStep = stepName;
|
||||
}
|
||||
|
||||
// Log an error.
|
||||
// No custom properties are logged alongside the error.
|
||||
// FIXME: This should really be sendTelemetryException but I'm maintaining
|
||||
|
@ -74,7 +62,7 @@ class TelemetryHelper {
|
|||
public async executeFunctionWithTimeTelemetry<T>(callback: () => Promise<T>, telemetryKey: string): Promise<T> {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
return await callback();
|
||||
return callback();
|
||||
}
|
||||
finally {
|
||||
this.setTelemetry(telemetryKey, ((Date.now() - startTime) / 1000).toString());
|
||||
|
@ -84,41 +72,24 @@ class TelemetryHelper {
|
|||
// Wraps the given function in a telemetry event.
|
||||
// The telemetry event sent ater function execution will contain how long the function took as well as any custom properties
|
||||
// supplied through initialize() or setTelemetry().
|
||||
// If the function errors, the telemetry event will additionally contain metadata about the error that occurred.
|
||||
// https://github.com/microsoft/vscode-azuretools/blob/5999c2ad4423e86f22d2c648027242d8816a50e4/ui/src/callWithTelemetryAndErrorHandling.ts
|
||||
public async callWithTelemetryAndErrorHandling<T>(command: string, callback: () => Promise<T>): Promise<T | undefined> {
|
||||
try {
|
||||
return await this.executeFunctionWithTimeTelemetry(callback, 'duration');
|
||||
return this.executeFunctionWithTimeTelemetry(callback, 'duration');
|
||||
} catch (error) {
|
||||
const parsedError = parseError(error);
|
||||
if (parsedError.isUserCancelledError) {
|
||||
this.setTelemetry(TelemetryKeys.Result, Result.Canceled);
|
||||
} else {
|
||||
this.setTelemetry(TelemetryKeys.Result, Result.Failed);
|
||||
this.setTelemetry('error', parsedError.errorType);
|
||||
this.setTelemetry('errorMessage', parsedError.message);
|
||||
this.setTelemetry('stack', parsedError.stack ?? '');
|
||||
TelemetryHelper.reporter.sendTelemetryErrorEvent(
|
||||
command, {
|
||||
...this.properties,
|
||||
[TelemetryKeys.JourneyId]: this.journeyId,
|
||||
});
|
||||
|
||||
logger.log(parsedError.message);
|
||||
if (parsedError.message.includes('\n')) {
|
||||
void vscode.window.showErrorMessage('An error has occurred. Check the output window for more details.');
|
||||
} else {
|
||||
void vscode.window.showErrorMessage(parsedError.message);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (this.properties.result === Result.Failed.toString()) {
|
||||
TelemetryHelper.reporter.sendTelemetryErrorEvent(
|
||||
command, {
|
||||
...this.properties,
|
||||
[TelemetryKeys.JourneyId]: this.journeyId,
|
||||
}, undefined, ['error', 'errorMesage', 'stack']);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
logger.log(message, command);
|
||||
|
||||
if (message.includes('\n')) {
|
||||
void vscode.window.showErrorMessage('An error has occurred. Check the output window for more details.');
|
||||
} else {
|
||||
TelemetryHelper.reporter.sendTelemetryEvent(
|
||||
command, {
|
||||
...this.properties,
|
||||
[TelemetryKeys.JourneyId]: this.journeyId,
|
||||
});
|
||||
void vscode.window.showErrorMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,2 @@
|
|||
export const CurrentUserInput = 'currentUserInput';
|
||||
export const RepoProvider = 'repoProvider';
|
||||
export const AzureLoginRequired = 'azureLoginRequired';
|
||||
export const JourneyId = 'journeyId';
|
||||
export const Result = 'result';
|
||||
export const SourceRepoLocation = 'sourceRepoLocation';
|
||||
export const ChosenTemplate = 'chosenTemplate';
|
||||
export const PipelineDiscarded = 'pipelineDiscarded';
|
||||
export const BrowsePipelineClicked = 'browsePipelineClicked';
|
||||
export const MultipleWorkspaceFolders = 'multipleWorkspaceFolders';
|
||||
export const GitHubPatDuration = 'gitHubPatDuration';
|
||||
|
||||
// Count of drop down items
|
||||
export const OrganizationListCount = 'OrganizationListCount';
|
||||
export const ProjectListCount = 'ProjectListCount';
|
||||
export const WebAppListCount = 'WebAppListCount';
|
||||
export const PipelineTempateListCount = 'pipelineTempateListCount';
|
||||
|
|
|
@ -1,53 +1,14 @@
|
|||
export const acquireAccessTokenFailed: string = 'Acquiring access token failed. Error: %s.';
|
||||
export const addYmlFile: string = 'Add Azure Pipelines YAML definition.';
|
||||
export const analyzingRepo: string = 'Analyzing your repo';
|
||||
export const azureAccountExtensionUnavailable: string = 'Azure Account extension could not be fetched. Please ensure it\'s installed and activated.';
|
||||
export const gitExtensionUnavailable: string = 'Git extension could not be fetched. Please ensure it\'s installed and activated.';
|
||||
export const gitExtensionNotEnabled: string = 'Git extension is not enabled. Please change the `git.enabled` setting to true.';
|
||||
export const azureLoginRequired: string = 'Please sign in to your Azure account first.';
|
||||
export const branchHeadMissing: string = `The current repository doesn't have any commits. Please [create a commit](https://git-scm.com/docs/git-commit) first, and then try this again.`;
|
||||
export const branchNameMissing: string = `The current repository isn't on a branch. Please [checkout a branch](https://git-scm.com/docs/git-checkout) first, and then try this again.`;
|
||||
export const branchRemoteMissing: string = `The current branch doesn't have a tracking branch, and the selected repository has no remotes. We're unable to create a remote tracking branch. Please [set a remote tracking branch](https://git-scm.com/docs/git-branch#Documentation/git-branch.txt---track) first, and then try this again.`;
|
||||
export const browsePipeline: string = 'Browse Pipeline';
|
||||
export const cannotIdentifyRepositoryDetails: string = 'Couldn\'t get repository details. Ensure your repo is hosted on [Azure Repos](https://docs.microsoft.com/azure/devops/repos/get-started) or [GitHub](https://guides.github.com/activities/hello-world/).';
|
||||
export const commitAndPush: string = 'Commit & push';
|
||||
export const commitFailedErrorMessage: string =`Commit failed due to error: %s`;
|
||||
export const pushingPipelineFile: string = 'Pushing pipeline file...';
|
||||
export const configuringPipelineAndDeployment: string = 'Configuring pipeline and proceeding to deployment...';
|
||||
export const runningPostDeploymentActions: string = 'Running post-deployment actions...';
|
||||
export const couldNotAuthorizeEndpoint: string = 'Couldn\'t authorize endpoint for use in Azure Pipelines.';
|
||||
export const creatingAzureServiceConnection: string = 'Connecting Azure Pipelines with your subscription: %s';
|
||||
export const creatingGitHubServiceConnection: string = 'Creating GitHub service connection';
|
||||
export const discardPipeline: string = 'Discard pipeline';
|
||||
export const enterGitHubPat: string = 'Enter GitHub personal access token (PAT)';
|
||||
export const failedToDetermineAzureRepoDetails: string = 'Failed to determine Azure Repo details from remote url. Please ensure that the remote points to a valid Azure Repos url.';
|
||||
export const gitHubPatErrorMessage: string = 'GitHub PAT cannot be empty.';
|
||||
export const githubPatHelpMessage: string = 'GitHub personal access token (PAT) with following permissions: full access to repository webhooks and services, read and write access to all repository data.';
|
||||
export const modifyAndCommitFile: string = 'Modify and save your YAML file. %s will commit this file, push the branch \'%s\' to remote \'%s\' and proceed with deployment.';
|
||||
export const noAgentQueueFound: string = 'No agent pool found named "%s".';
|
||||
export const noAvailableFileNames: string = 'No available filenames found.';
|
||||
export const notAGitRepository: string = 'Selected workspace is not a [Git](https://git-scm.com/docs/git) repository. Please select a Git repository.';
|
||||
export const notAzureRepoUrl: string = 'The repo isn\'t hosted with Azure Repos.';
|
||||
export const pipelineSetupSuccessfully: string = 'Pipeline set up successfully!';
|
||||
export const remoteRepositoryNotConfigured: string = 'Remote repository is not configured. This extension is compatible with [Azure Repos](https://docs.microsoft.com/en-us/azure/devops/repos/get-started) or [GitHub](https://guides.github.com/activities/hello-world/).';
|
||||
export const selectFolderLabel: string = 'Select source folder for configuring pipeline';
|
||||
export const selectOrganizationForEnhancedIntelliSense: string = 'Select Azure DevOps organization associated with the %s repository for enhanced Azure Pipelines IntelliSense.';
|
||||
export const selectOrganizationLabel: string = 'Select organization';
|
||||
export const selectOrganizationPlaceholder: string = 'Select Azure DevOps organization associated with the %s repository';
|
||||
export const selectPipelineTemplate: string = 'Select an Azure Pipelines template...';
|
||||
export const selectProject: string = 'Select an Azure DevOps project';
|
||||
export const selectRemoteForBranch: string = 'Select the remote repository where you want to track your current branch';
|
||||
export const selectSubscription: string = 'Select an Azure subscription';
|
||||
export const selectWorkspaceFolder: string = 'Select a folder from your workspace to deploy';
|
||||
export const signInLabel: string = 'Sign In';
|
||||
export const unableToAccessOrganization: string = 'Unable to access the "%s" organization. Make sure you\'re signed into the right Azure account.';
|
||||
export const unableToCreateServiceConnection: string = `Unable to create %s service connection.\nOperation Status: %s\nMessage: %s\nService connection is not in ready state.`;
|
||||
export const timedOutCreatingServiceConnection: string =`Timed out creating %s service connection.\nService connection is not in ready state.`;
|
||||
export const retryFailedMessage: string =`Failed after retrying: %s times. Internal Error: %s`;
|
||||
export const azureServicePrincipalFailedMessage: string =`Failed while creating Azure service principal.`;
|
||||
export const roleAssignmentFailedMessage: string =`Failed while role assignement.`;
|
||||
export const waitForAzureSignIn: string = `Waiting for Azure sign-in...`;
|
||||
export const signInForEnhancedIntelliSense = 'Sign in to Azure for enhanced Azure Pipelines IntelliSense';
|
||||
export const signInWithADifferentAccountLabel: string = 'Sign in with a different account';
|
||||
export const unableToAccessOrganization: string = 'Unable to access the "%s" organization. Make sure you\'re signed into the right Microsoft account.';
|
||||
export const signInForEnhancedIntelliSense = 'Sign in to Microsoft for enhanced Azure Pipelines IntelliSense';
|
||||
export const userEligibleForEnahanced1ESPTIntellisense = 'Enable 1ESPT Schema in Azure Pipelines Extension settings for enhanced Intellisense';
|
||||
export const notUsing1ESPTSchemaAsUserNotSignedInMessage = '1ESPT Schema is not used for Intellisense as you are not signed in with a `@microsoft.com` account';
|
||||
export const enable1ESPTSchema = 'Enable';
|
||||
|
|
|
@ -7,14 +7,14 @@ import * as vscode from 'vscode';
|
|||
import { URI, Utils } from 'vscode-uri';
|
||||
import * as azdev from 'azure-devops-node-api';
|
||||
import * as logger from './logger';
|
||||
import { AzureSession } from './typings/azure-account.api';
|
||||
import * as Messages from './messages';
|
||||
import { getAzureDevOpsSessions } from './schema-association-service';
|
||||
|
||||
const milliseconds24hours = 86400000;
|
||||
|
||||
export async function get1ESPTSchemaUri(azureDevOpsClient: azdev.WebApi, organizationName: string, session: AzureSession, context: vscode.ExtensionContext, repoId1espt: string): Promise<URI | undefined> {
|
||||
export async function get1ESPTSchemaUri(azureDevOpsClient: azdev.WebApi, organizationName: string, session: vscode.AuthenticationSession, context: vscode.ExtensionContext, repoId1espt: string): Promise<URI | undefined> {
|
||||
try {
|
||||
if (session.userId.endsWith("@microsoft.com")) {
|
||||
if (session.account.label.endsWith("@microsoft.com")) {
|
||||
const gitApi = await azureDevOpsClient.getGitApi();
|
||||
// Using getItem from GitApi: getItem(repositoryId: string, path: string, project?: string, scopePath?: string, recursionLevel?: GitInterfaces.VersionControlRecursionType, includeContentMetadata?: boolean, latestProcessedChange?: boolean, download?: boolean, versionDescriptor?: GitInterfaces.GitVersionDescriptor, includeContent?: boolean, resolveLfs?: boolean, sanitize?: boolean): Promise<GitInterfaces.GitItem>;
|
||||
const schemaFile = await gitApi.getItem(repoId1espt, "schema/1espt-base-schema.json", "1ESPipelineTemplates", undefined, undefined, true, true, true, undefined, true, true);
|
||||
|
@ -35,7 +35,7 @@ export async function get1ESPTSchemaUri(azureDevOpsClient: azdev.WebApi, organiz
|
|||
}
|
||||
}
|
||||
catch (error) {
|
||||
logger.log(`Error : ${String(error)} while fetching 1ESPT schema for org: ${organizationName} : `, 'SchemaDetection');
|
||||
logger.log(`Error: ${error instanceof Error ? error.message : String(error)} while fetching 1ESPT schema for org: ${organizationName} : `, 'SchemaDetection');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ export async function get1ESPTSchemaUri(azureDevOpsClient: azdev.WebApi, organiz
|
|||
* @param lastUpdated1ESPTSchema
|
||||
* @returns
|
||||
*/
|
||||
export async function getCached1ESPTSchema(context: vscode.ExtensionContext, organizationName: string, session: AzureSession, lastUpdated1ESPTSchema: Map<string, Date>): Promise<URI | undefined> {
|
||||
export async function getCached1ESPTSchema(context: vscode.ExtensionContext, organizationName: string, session: vscode.AuthenticationSession, lastUpdated1ESPTSchema: Map<string, Date>): Promise<URI | undefined> {
|
||||
const lastUpdatedDate = lastUpdated1ESPTSchema.get(organizationName);
|
||||
if (!lastUpdatedDate) {
|
||||
return undefined;
|
||||
|
@ -61,7 +61,7 @@ export async function getCached1ESPTSchema(context: vscode.ExtensionContext, org
|
|||
const schemaUri1ESPT = Utils.joinPath(context.globalStorageUri, '1ESPTSchema', `${organizationName}-1espt-schema.json`);
|
||||
|
||||
try {
|
||||
if (session.userId.endsWith("@microsoft.com")) {
|
||||
if (session.account.label.endsWith("@microsoft.com")) {
|
||||
if ((new Date().getTime() - lastUpdatedDate.getTime()) < milliseconds24hours) {
|
||||
try {
|
||||
await vscode.workspace.fs.stat(schemaUri1ESPT);
|
||||
|
@ -77,20 +77,20 @@ export async function getCached1ESPTSchema(context: vscode.ExtensionContext, org
|
|||
}
|
||||
}
|
||||
else {
|
||||
const signInAction = await vscode.window.showInformationMessage(Messages.notUsing1ESPTSchemaAsUserNotSignedInMessage, Messages.signInLabel);
|
||||
if (signInAction == Messages.signInLabel) {
|
||||
await vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: Messages.waitForAzureSignIn,
|
||||
}, async () => {
|
||||
await vscode.commands.executeCommand("azure-account.login");
|
||||
void vscode.window.showInformationMessage(Messages.notUsing1ESPTSchemaAsUserNotSignedInMessage, Messages.signInWithADifferentAccountLabel)
|
||||
.then(async action => {
|
||||
if (action === Messages.signInWithADifferentAccountLabel) {
|
||||
await getAzureDevOpsSessions(context, {
|
||||
clearSessionPreference: true,
|
||||
createIfNone: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
logger.log(`Skipping cached 1ESPT schema for ${organizationName} as user is not signed in with Microsoft account`, `SchemaDetection`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
logger.log(`Error : ${String(error)} while fetching cached 1ESPT schema for org: ${organizationName}. It's possible that the schema does not exist.`, 'SchemaDetection');
|
||||
logger.log(`Error: ${error instanceof Error ? error.message : String(error)} while fetching cached 1ESPT schema for org: ${organizationName}. It's possible that the schema does not exist.`, 'SchemaDetection');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -105,13 +105,9 @@ export async function getCached1ESPTSchema(context: vscode.ExtensionContext, org
|
|||
export async function get1ESPTRepoIdIfAvailable(azureDevOpsClient: azdev.WebApi, organizationName: string): Promise<string> {
|
||||
try {
|
||||
const gitApi = await azureDevOpsClient.getGitApi();
|
||||
const repositories = await gitApi.getRepositories('1ESPipelineTemplates');
|
||||
if (repositories.length === 0) {
|
||||
logger.log(`1ESPipelineTemplates ADO project not found for org ${organizationName}`, `SchemaDetection`);
|
||||
return ""; // 1ESPT ADO project not found
|
||||
}
|
||||
|
||||
const repository = repositories.find(repo => repo.name === "1ESPipelineTemplates");
|
||||
const repository = await gitApi.getRepository('1ESPipelineTemplates', '1ESPipelineTemplates');
|
||||
// Types are wrong and getRepository cah return null.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (repository?.id === undefined) {
|
||||
logger.log(`1ESPipelineTemplates repo not found for org ${organizationName}`, `SchemaDetection`);
|
||||
return ""; // 1ESPT repo not found
|
||||
|
@ -120,7 +116,7 @@ export async function get1ESPTRepoIdIfAvailable(azureDevOpsClient: azdev.WebApi,
|
|||
return repository.id;
|
||||
}
|
||||
catch (error) {
|
||||
logger.log(`Error : ${String(error)} while checking eligibility for enhanced Intellisense for 1ESPT schema for org: ${organizationName}.`, 'SchemaDetection');
|
||||
logger.log(`Error: ${error instanceof Error ? error.message : String(error)} while checking eligibility for enhanced Intellisense for 1ESPT schema for org: ${organizationName}.`, 'SchemaDetection');
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
@ -130,6 +126,6 @@ export async function delete1ESPTSchemaFileIfPresent(context: vscode.ExtensionCo
|
|||
await vscode.workspace.fs.delete(Utils.joinPath(context.globalStorageUri, '1ESPTSchema'), { recursive: true });
|
||||
}
|
||||
catch (error) {
|
||||
logger.log(`Error: ${String(error)} while deleting 1ESPT schema. It's possible that the schema file does not exist`, 'SchemaDetection');
|
||||
logger.log(`Error: ${error instanceof Error ? error.message : String(error)} while deleting 1ESPT schema. It's possible that the schema file does not exist`, 'SchemaDetection');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,14 +9,13 @@ import { Utils } from 'vscode-uri';
|
|||
import * as languageclient from 'vscode-languageclient/node';
|
||||
import * as azdev from 'azure-devops-node-api';
|
||||
import { format } from 'util';
|
||||
import { getAzureAccountExtensionApi, getGitExtensionApi } from './extensionApis';
|
||||
import { OrganizationsClient } from './configure/clients/devOps/organizationsClient';
|
||||
import { getRepositoryDetailsFromRemoteUrl, isAzureReposUrl } from './configure/helper/devOps/azureDevOpsHelper';
|
||||
import { showQuickPick } from './configure/helper/controlProvider';
|
||||
import { QuickPickItemWithData } from './configure/model/models';
|
||||
import { getGitExtensionApi } from './extensionApis';
|
||||
import { OrganizationsClient } from './clients/devOps/organizationsClient';
|
||||
import { getRepositoryDetailsFromRemoteUrl, isAzureReposUrl } from './helpers/azureDevOpsHelper';
|
||||
import { showQuickPick } from './helpers/controlProvider';
|
||||
import { extensionVersion } from './helpers/telemetryHelper';
|
||||
import * as logger from './logger';
|
||||
import * as Messages from './messages';
|
||||
import { AzureSession } from './typings/azure-account.api';
|
||||
import { get1ESPTSchemaUri, getCached1ESPTSchema, get1ESPTRepoIdIfAvailable, delete1ESPTSchemaFileIfPresent } from './schema-association-service-1espt';
|
||||
|
||||
const selectOrganizationEvent = new vscode.EventEmitter<vscode.WorkspaceFolder>();
|
||||
|
@ -28,8 +27,24 @@ export const onDidSelectOrganization = selectOrganizationEvent.event;
|
|||
const seenOrganizations = new Set<string>();
|
||||
const lastUpdated1ESPTSchema = new Map<string, Date>();
|
||||
|
||||
export const DO_NOT_ASK_SIGN_IN_KEY = "DO_NOT_ASK_SIGN_IN_KEY";
|
||||
export const DO_NOT_ASK_SELECT_ORG_KEY = "DO_NOT_ASK_SELECT_ORG_KEY";
|
||||
const DO_NOT_ASK_SIGN_IN_KEY = "DO_NOT_ASK_SIGN_IN_KEY";
|
||||
const DO_NOT_ASK_SELECT_ORG_KEY = "DO_NOT_ASK_SELECT_ORG_KEY";
|
||||
|
||||
const AZURE_MANAGEMENT_SCOPES = [
|
||||
// Get tenants
|
||||
'https://management.core.windows.net/.default',
|
||||
];
|
||||
|
||||
const AZURE_DEVOPS_SCOPES = [
|
||||
// It would be better to use the fine-grained scopes,
|
||||
// but we need to wait for VS Code to support them.
|
||||
// https://github.com/microsoft/vscode/issues/201679
|
||||
'499b84ac-1321-427f-aa17-267ca6975798/.default',
|
||||
// // Get YAML schema
|
||||
// '499b84ac-1321-427f-aa17-267ca6975798/vso.agentpools',
|
||||
// // Get ADO orgs
|
||||
// '499b84ac-1321-427f-aa17-267ca6975798/vso.profile',
|
||||
];
|
||||
|
||||
let repoId1espt: string | undefined = undefined;
|
||||
|
||||
|
@ -105,47 +120,9 @@ export function getSchemaAssociation(schemaFilePath: string): ISchemaAssociation
|
|||
async function autoDetectSchema(
|
||||
context: vscode.ExtensionContext,
|
||||
workspaceFolder: vscode.WorkspaceFolder): Promise<vscode.Uri | undefined> {
|
||||
const azureAccountApi = await getAzureAccountExtensionApi();
|
||||
|
||||
// We could care less about the subscriptions; all we need are the sessions.
|
||||
// However, there's no waitForSessions API, and waitForLogin returns before
|
||||
// the underlying account information is guaranteed to finish loading.
|
||||
// The next-best option is then waitForSubscriptions which, by definition,
|
||||
// can't return until the sessions are also available.
|
||||
// This only returns false if there is no login.
|
||||
if (!(await azureAccountApi.waitForSubscriptions())) {
|
||||
const doNotAskAgainSignIn = context.globalState.get<boolean>(DO_NOT_ASK_SIGN_IN_KEY);
|
||||
if (doNotAskAgainSignIn) {
|
||||
logger.log(`Not prompting for login - do not ask again was set`, 'SchemaDetection');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
logger.log(`Waiting for login`, 'SchemaDetection');
|
||||
|
||||
try {
|
||||
await delete1ESPTSchemaFileIfPresent(context);
|
||||
logger.log("1ESPTSchema folder deleted as user is not signed in", 'SchemaDetection')
|
||||
}
|
||||
catch (error) {
|
||||
logger.log(`Error ${String(error)} while trying to delete 1ESPTSchema folder. Either the folder does not exist or there is an actual error.`, 'SchemaDetection')
|
||||
}
|
||||
|
||||
// Don't await this message so that we can return the fallback schema instead of blocking.
|
||||
// We'll detect the login in extension.ts and then re-request the schema.
|
||||
void vscode.window.showInformationMessage(Messages.signInForEnhancedIntelliSense, Messages.signInLabel, Messages.doNotAskAgain)
|
||||
.then(async action => {
|
||||
if (action === Messages.signInLabel) {
|
||||
await vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: Messages.waitForAzureSignIn,
|
||||
}, async () => {
|
||||
await vscode.commands.executeCommand("azure-account.login");
|
||||
});
|
||||
} else if (action === Messages.doNotAskAgain) {
|
||||
await context.globalState.update(DO_NOT_ASK_SIGN_IN_KEY, true);
|
||||
}
|
||||
});
|
||||
|
||||
const azureDevOpsSessions = await getAzureDevOpsSessions(context);
|
||||
if (azureDevOpsSessions === undefined) {
|
||||
logger.log(`Not logged in`, 'SchemaDetection');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -177,20 +154,11 @@ async function autoDetectSchema(
|
|||
}
|
||||
|
||||
let organizationName: string;
|
||||
let session: AzureSession | undefined;
|
||||
if (remoteUrl !== undefined && isAzureReposUrl(remoteUrl)) {
|
||||
logger.log(`${workspaceFolder.name} is an Azure repo`, 'SchemaDetection');
|
||||
|
||||
// If we're in an Azure repo, we can silently determine the organization name and session.
|
||||
// If we're in an Azure repo, we can silently determine the organization name.
|
||||
organizationName = getRepositoryDetailsFromRemoteUrl(remoteUrl).organizationName;
|
||||
for (const azureSession of azureAccountApi.sessions) {
|
||||
const organizationsClient = new OrganizationsClient(azureSession.credentials2);
|
||||
const organizations = await organizationsClient.listOrganizations();
|
||||
if (organizations.find(org => org.accountName.toLowerCase() === organizationName.toLowerCase())) {
|
||||
session = azureSession;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.log(`${workspaceFolder.name} has no remote URL or is not an Azure repo`, 'SchemaDetection');
|
||||
|
||||
|
@ -201,77 +169,94 @@ async function autoDetectSchema(
|
|||
// If we already have cached information for this workspace folder, use it.
|
||||
const details = azurePipelinesDetails[workspaceFolder.name];
|
||||
organizationName = details.organization;
|
||||
session = azureAccountApi.sessions.find(session => session.tenantId === details.tenant);
|
||||
|
||||
logger.log(
|
||||
`Using cached information for ${workspaceFolder.name}: ${organizationName}, ${session?.tenantId}`,
|
||||
`Using cached organization for ${workspaceFolder.name}: ${organizationName}`,
|
||||
'SchemaDetection');
|
||||
} else {
|
||||
const doNotAskAgainSelectOrg = context.globalState.get<boolean>(DO_NOT_ASK_SELECT_ORG_KEY);
|
||||
if (doNotAskAgainSelectOrg) {
|
||||
logger.log(`Not prompting for organization - do not ask again was set`, 'SchemaDetection');
|
||||
return;
|
||||
}
|
||||
logger.log(`Retrieving organizations for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
|
||||
logger.log(`Prompting for organization for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
const organizations = (await Promise.all(azureDevOpsSessions.map(async session => {
|
||||
const organizationsClient = new OrganizationsClient(session.accessToken);
|
||||
const organizations = await organizationsClient.listOrganizations();
|
||||
return organizations.map(({ accountName }) => accountName);
|
||||
}))).flat();
|
||||
|
||||
// Otherwise, we need to manually prompt.
|
||||
// We do this by asking them to select an organization via an information message,
|
||||
// then displaying the quick pick of all the organizations they have access to.
|
||||
// We *do not* await this message so that we can use the fallback schema while waiting.
|
||||
// We'll detect when they choose the organization in extension.ts and then re-request the schema.
|
||||
void vscode.window.showInformationMessage(
|
||||
format(Messages.selectOrganizationForEnhancedIntelliSense, workspaceFolder.name),
|
||||
Messages.selectOrganizationLabel, Messages.doNotAskAgain)
|
||||
.then(async action => {
|
||||
if (action === Messages.selectOrganizationLabel) {
|
||||
// Lazily construct list of organizations so that we can immediately show the quick pick,
|
||||
// then fill in the choices as they come in.
|
||||
const getOrganizationsAndSessions = async (): Promise<QuickPickItemWithData<AzureSession>[]> => {
|
||||
return (await Promise.all(azureAccountApi.sessions.map(async session => {
|
||||
const organizationsClient = new OrganizationsClient(session.credentials2);
|
||||
const organizations = await organizationsClient.listOrganizations();
|
||||
return organizations.map(organization => ({
|
||||
label: organization.accountName,
|
||||
data: session,
|
||||
}));
|
||||
}))).flat();
|
||||
};
|
||||
// If there's only one organization, we can just use that.
|
||||
if (organizations.length === 1) {
|
||||
organizationName = organizations[0];
|
||||
logger.log(`Using only available organization ${organizationName} for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
} else {
|
||||
const doNotAskAgainSelectOrg = context.globalState.get<boolean>(DO_NOT_ASK_SELECT_ORG_KEY);
|
||||
if (doNotAskAgainSelectOrg) {
|
||||
logger.log(`Not prompting for organization - do not ask again was set`, 'SchemaDetection');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedOrganizationAndSession = await showQuickPick(
|
||||
'organization',
|
||||
getOrganizationsAndSessions(), {
|
||||
placeHolder: format(Messages.selectOrganizationPlaceholder, workspaceFolder.name),
|
||||
});
|
||||
logger.log(`${organizations.length} organizations found - prompting for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
|
||||
if (selectedOrganizationAndSession === undefined) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, we need to manually prompt.
|
||||
// We do this by asking them to select an organization via an information message,
|
||||
// then displaying the quick pick of all the organizations they have access to.
|
||||
// We *do not* await this message so that we can use the fallback schema while waiting.
|
||||
// We'll detect when they choose the organization in extension.ts and then re-request the schema.
|
||||
void vscode.window.showInformationMessage(
|
||||
format(Messages.selectOrganizationForEnhancedIntelliSense, workspaceFolder.name),
|
||||
Messages.selectOrganizationLabel, Messages.doNotAskAgain)
|
||||
.then(async action => {
|
||||
if (action === Messages.selectOrganizationLabel) {
|
||||
const selectedOrganization = await showQuickPick(
|
||||
'organization',
|
||||
organizations.map(organization => ({ label: organization })), {
|
||||
placeHolder: format(Messages.selectOrganizationPlaceholder, workspaceFolder.name),
|
||||
});
|
||||
|
||||
organizationName = selectedOrganizationAndSession.label;
|
||||
session = selectedOrganizationAndSession.data;
|
||||
|
||||
await context.workspaceState.update('azurePipelinesDetails', {
|
||||
...azurePipelinesDetails,
|
||||
[workspaceFolder.name]: {
|
||||
organization: organizationName,
|
||||
tenant: session.tenantId,
|
||||
if (selectedOrganization === undefined) {
|
||||
logger.log(`No organization picked for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
selectOrganizationEvent.fire(workspaceFolder);
|
||||
} else if (action === Messages.doNotAskAgain) {
|
||||
await context.globalState.update(DO_NOT_ASK_SELECT_ORG_KEY, true);
|
||||
}
|
||||
});
|
||||
return undefined;
|
||||
organizationName = selectedOrganization.label;
|
||||
|
||||
await context.workspaceState.update('azurePipelinesDetails', {
|
||||
...azurePipelinesDetails,
|
||||
[workspaceFolder.name]: {
|
||||
organization: organizationName,
|
||||
}
|
||||
});
|
||||
|
||||
selectOrganizationEvent.fire(workspaceFolder);
|
||||
} else if (action === Messages.doNotAskAgain) {
|
||||
await context.globalState.update(DO_NOT_ASK_SELECT_ORG_KEY, true);
|
||||
}
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let azureDevOpsSession: vscode.AuthenticationSession | undefined;
|
||||
for (const session of azureDevOpsSessions) {
|
||||
const organizationsClient = new OrganizationsClient(session.accessToken);
|
||||
const organizations = await organizationsClient.listOrganizations();
|
||||
if (organizations.map(({ accountName }) => accountName).includes(organizationName)) {
|
||||
azureDevOpsSession = session;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Not logged into an account that has access.
|
||||
if (session === undefined) {
|
||||
logger.log(`No organization found for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
void vscode.window.showErrorMessage(format(Messages.unableToAccessOrganization, organizationName));
|
||||
if (azureDevOpsSession === undefined) {
|
||||
logger.log(`No account found for ${organizationName}`, 'SchemaDetection');
|
||||
void vscode.window.showErrorMessage(format(Messages.unableToAccessOrganization, organizationName), Messages.signInWithADifferentAccountLabel)
|
||||
.then(async action => {
|
||||
if (action === Messages.signInWithADifferentAccountLabel) {
|
||||
await getAzureDevOpsSessions(context, {
|
||||
clearSessionPreference: true,
|
||||
createIfNone: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
await delete1ESPTSchemaFileIfPresent(context);
|
||||
return undefined;
|
||||
}
|
||||
|
@ -279,15 +264,14 @@ async function autoDetectSchema(
|
|||
// Create the global storage folder to guarantee that it exists.
|
||||
await vscode.workspace.fs.createDirectory(context.globalStorageUri);
|
||||
|
||||
logger.log(`Retrieving schema for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
logger.log(`Retrieving ${organizationName} schema for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
|
||||
// Try to fetch schema in the following order:
|
||||
// 1. Cached 1ESPT schema
|
||||
// 2. 1ESPT schema if user is signed in with microsoft account and has enabled 1ESPT schema
|
||||
// 3. Cached Organization specific schema
|
||||
// 4. Organization specific schema
|
||||
const token = await session.credentials2.getToken();
|
||||
const authHandler = azdev.getBearerHandler(token.accessToken);
|
||||
const authHandler = azdev.getBearerHandler(azureDevOpsSession.accessToken);
|
||||
const azureDevOpsClient = new azdev.WebApi(`https://dev.azure.com/${organizationName}`, authHandler);
|
||||
|
||||
// Cache the response - this is why this method returns empty strings instead of undefined.
|
||||
|
@ -298,13 +282,13 @@ async function autoDetectSchema(
|
|||
if (repoId1espt.length > 0) {
|
||||
// user has enabled 1ESPT schema
|
||||
if (vscode.workspace.getConfiguration('azure-pipelines', workspaceFolder).get<boolean>('1ESPipelineTemplatesSchemaFile', false)) {
|
||||
const cachedSchemaUri1ESPT = await getCached1ESPTSchema(context, organizationName, session, lastUpdated1ESPTSchema);
|
||||
const cachedSchemaUri1ESPT = await getCached1ESPTSchema(context, organizationName, azureDevOpsSession, lastUpdated1ESPTSchema);
|
||||
if (cachedSchemaUri1ESPT) {
|
||||
return cachedSchemaUri1ESPT;
|
||||
}
|
||||
else {
|
||||
// if user is signed in with microsoft account and has enabled 1ESPipeline Template Schema, then give preference to 1ESPT schema
|
||||
const schemaUri1ESPT = await get1ESPTSchemaUri(azureDevOpsClient, organizationName, session, context, repoId1espt);
|
||||
const schemaUri1ESPT = await get1ESPTSchemaUri(azureDevOpsClient, organizationName, azureDevOpsSession, context, repoId1espt);
|
||||
if (schemaUri1ESPT) {
|
||||
lastUpdated1ESPTSchema.set(organizationName, new Date());
|
||||
return schemaUri1ESPT;
|
||||
|
@ -337,7 +321,7 @@ async function autoDetectSchema(
|
|||
// hit the network to request an updated schema for an organization once per session.
|
||||
const schemaUri = Utils.joinPath(context.globalStorageUri, `${organizationName}-schema.json`);
|
||||
if (seenOrganizations.has(organizationName)) {
|
||||
logger.log(`Returning cached schema for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
logger.log(`Returning cached ${organizationName} schema for ${workspaceFolder.name}`, 'SchemaDetection');
|
||||
return schemaUri;
|
||||
}
|
||||
|
||||
|
@ -350,6 +334,81 @@ async function autoDetectSchema(
|
|||
return schemaUri;
|
||||
}
|
||||
|
||||
export async function getAzureDevOpsSessions(context: vscode.ExtensionContext, options?: vscode.AuthenticationGetSessionOptions): Promise<vscode.AuthenticationSession[] | undefined> {
|
||||
// First, request an ARM token.
|
||||
const managementSession = await vscode.authentication.getSession('microsoft', AZURE_MANAGEMENT_SCOPES, options);
|
||||
if (managementSession === undefined) {
|
||||
const doNotAskAgainSignIn = context.globalState.get<boolean>(DO_NOT_ASK_SIGN_IN_KEY);
|
||||
if (doNotAskAgainSignIn) {
|
||||
logger.log(`Not prompting for login - do not ask again was set`, 'SchemaDetection');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
logger.log(`Waiting for login`, 'SchemaDetection');
|
||||
|
||||
try {
|
||||
await delete1ESPTSchemaFileIfPresent(context);
|
||||
logger.log("1ESPTSchema folder deleted as user is not signed in", 'SchemaDetection')
|
||||
}
|
||||
catch (error) {
|
||||
logger.log(`Error ${String(error)} while trying to delete 1ESPTSchema folder. Either the folder does not exist or there is an actual error.`, 'SchemaDetection')
|
||||
}
|
||||
|
||||
// Don't await this message so that we can return the fallback schema instead of blocking.
|
||||
// We'll detect the login in extension.ts and then re-request the schema.
|
||||
void vscode.window.showInformationMessage(Messages.signInForEnhancedIntelliSense, Messages.signInLabel, Messages.doNotAskAgain)
|
||||
.then(async action => {
|
||||
if (action === Messages.signInLabel) {
|
||||
await vscode.authentication.getSession('microsoft', AZURE_MANAGEMENT_SCOPES, { createIfNone: true });
|
||||
} else if (action === Messages.doNotAskAgain) {
|
||||
await context.globalState.update(DO_NOT_ASK_SIGN_IN_KEY, true);
|
||||
}
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const azureDevOpsSessions: vscode.AuthenticationSession[] = [];
|
||||
|
||||
// The ARM token allows us to get a list of tenants, which we then request ADO tokens for.
|
||||
let nextLink: string | undefined = 'https://management.azure.com/tenants?api-version=2022-01-01';
|
||||
while (nextLink !== undefined) {
|
||||
const response = await fetch(nextLink, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${managementSession.accessToken}`,
|
||||
'User-Agent': `azure-pipelines-vscode ${extensionVersion}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json() as { value: { tenantId: string }[], nextLink?: string };
|
||||
nextLink = data.nextLink;
|
||||
|
||||
for (const tenant of data.value) {
|
||||
const session = await vscode.authentication.getSession('microsoft', [...AZURE_DEVOPS_SCOPES, `VSCODE_TENANT:${tenant.tenantId}`], { silent: true });
|
||||
if (session !== undefined) {
|
||||
azureDevOpsSessions.push(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation detail (yuck): The microsoft provider sets this to MSAL's homeAccountId,
|
||||
// which is further defined as <objectId>.<tenantId>.
|
||||
// Also included is a live.com check for the non-MSAL case, which can be removed once MSAL is the only option.
|
||||
// We can use this to determine if the session is for an MSA or not.
|
||||
if (managementSession.account.id.includes('live.com') ||
|
||||
(managementSession.account.id.includes('.')
|
||||
// MSA tenant & first-party tenant which MSAs can request tokens for.
|
||||
&& ['9188040d-6c67-4c5b-b112-36a304b66dad', 'f8cdef31-a31e-4b4a-93e4-5f571e91255a']
|
||||
.includes(managementSession.account.id.split('.')[1]))) {
|
||||
// MSAs have their own organizations that aren't associated with a tenant.
|
||||
const msaSession = await vscode.authentication.getSession('microsoft', AZURE_DEVOPS_SCOPES, { silent: true });
|
||||
if (msaSession !== undefined) {
|
||||
azureDevOpsSessions.push(msaSession);
|
||||
}
|
||||
}
|
||||
|
||||
return azureDevOpsSessions;
|
||||
}
|
||||
|
||||
// Mapping of glob pattern -> schemas
|
||||
interface ISchemaAssociations {
|
||||
[pattern: string]: string[];
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import * as path from 'path';
|
||||
import Mocha from 'mocha';
|
||||
import glob from 'glob';
|
||||
|
||||
export function run(): Promise<void> {
|
||||
// Create the mocha test
|
||||
const mocha = new Mocha({
|
||||
ui: 'tdd',
|
||||
color: true,
|
||||
});
|
||||
mocha.timeout(100000);
|
||||
|
||||
const testsRoot = __dirname;
|
||||
|
||||
return new Promise((c, e) => {
|
||||
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
|
||||
if (err) {
|
||||
e(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add files to the test suite
|
||||
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
|
||||
|
||||
try {
|
||||
// Run the mocha test
|
||||
mocha.run(failures => {
|
||||
if (failures > 0) {
|
||||
e(new Error(`${failures} tests failed.`));
|
||||
} else {
|
||||
c();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
e(err as Error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import * as cp from 'child_process';
|
||||
import * as path from 'path';
|
||||
|
||||
import {
|
||||
downloadAndUnzipVSCode,
|
||||
resolveCliPathFromVSCodeExecutablePath,
|
||||
runTests
|
||||
} from '@vscode/test-electron';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// The folder containing the Extension Manifest package.json
|
||||
// Passed to `--extensionDevelopmentPath`
|
||||
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
|
||||
|
||||
// The path to the extension test runner script
|
||||
// Passed to --extensionTestsPath
|
||||
const extensionTestsPath = path.resolve(__dirname, './index');
|
||||
|
||||
// If the first argument is a path to a file/folder/workspace,
|
||||
// the launched VS Code instance will open it.
|
||||
// workspace isn't copied to out because it's all YAML files.
|
||||
const launchArgs = [path.resolve(__dirname, '../../src/test/workspace')];
|
||||
|
||||
const vscodeExecutablePath = await downloadAndUnzipVSCode();
|
||||
const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath);
|
||||
|
||||
// 0.11.0 has a bug where it blocks extension loading on first launch:
|
||||
// https://github.com/microsoft/vscode-azure-account/pull/603.
|
||||
// Since we always launch for the first time in CI, that turns out
|
||||
// to be problematic.
|
||||
cp.spawnSync(cliPath, ['--install-extension', 'ms-vscode.azure-account@0.10.1'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
// Download VS Code, unzip it and run the integration test
|
||||
await runTests({
|
||||
vscodeExecutablePath,
|
||||
extensionDevelopmentPath,
|
||||
extensionTestsPath,
|
||||
launchArgs
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error('Failed to run tests');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void main();
|
|
@ -1,158 +0,0 @@
|
|||
import * as assert from 'assert';
|
||||
import { isAzureReposUrl, getFormattedRemoteUrl, getRepositoryDetailsFromRemoteUrl, getOldFormatBuildDefinitionUrl, getOldFormatBuildUrl } from '../../../configure/helper/devOps/azureDevOpsHelper';
|
||||
|
||||
suite('Azure DevOps Helper', () => {
|
||||
suite('isAzureReposUrl', () => {
|
||||
test('Returns true for HTTPS ADO URLs', () => {
|
||||
assert.ok(isAzureReposUrl('https://dev.azure.com/ms/example/_git/repo'));
|
||||
});
|
||||
|
||||
test('Returns true for HTTPS ADO URLs with leading organization', () => {
|
||||
assert.ok(isAzureReposUrl('https://ms@dev.azure.com/ms/example/_git/repo'));
|
||||
});
|
||||
|
||||
test('Returns true for SSH ADO URLs', () => {
|
||||
assert.ok(isAzureReposUrl('git@ssh.dev.azure.com:v3/ms/example/repo'));
|
||||
});
|
||||
|
||||
test('Returns true for legacy HTTPS VSTS URLs', () => {
|
||||
assert.ok(isAzureReposUrl('https://ms.visualstudio.com/example/_git/repo'));
|
||||
});
|
||||
|
||||
test('Returns true for legacy HTTPS VSTS URLs with DefaultCollection', () => {
|
||||
assert.ok(isAzureReposUrl('https://ms.visualstudio.com/DefaultCollection/example/_git/repo'));
|
||||
});
|
||||
|
||||
test('Returns true for legacy SSH VSTS URLs', () => {
|
||||
assert.ok(isAzureReposUrl('ms@vs-ssh.visualstudio.com:v3/ms/example/repo'));
|
||||
});
|
||||
|
||||
test('Returns false for non-ADO HTTPS URLs', () => {
|
||||
assert.strictEqual(
|
||||
isAzureReposUrl('https://dev.azure.coms/ms/example/_git/repo'),
|
||||
false);
|
||||
});
|
||||
|
||||
test('Returns false for non-ADO SSH URLs', () => {
|
||||
assert.strictEqual(
|
||||
isAzureReposUrl('git@dev.azure.com:v3/ms/example/repo'),
|
||||
false);
|
||||
});
|
||||
|
||||
test('Returns false for non-VSTS HTTPS URLs', () => {
|
||||
assert.strictEqual(
|
||||
isAzureReposUrl('https://ms.visualstudio.coms/example/_git/repo'),
|
||||
false);
|
||||
});
|
||||
|
||||
test('Returns false for non-VSTS SSH URLs', () => {
|
||||
assert.strictEqual(
|
||||
isAzureReposUrl('ms@ssh.visualstudio.com:v3/ms/example/repo'),
|
||||
false);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getRepositoryIdFromUrl', () => {
|
||||
test('Returns details from an HTTPS ADO URL', () => {
|
||||
assert.deepStrictEqual(
|
||||
getRepositoryDetailsFromRemoteUrl('https://dev.azure.com/ms/example/_git/repo'),
|
||||
{
|
||||
organizationName: 'ms',
|
||||
projectName: 'example',
|
||||
repositoryName: 'repo',
|
||||
});
|
||||
});
|
||||
|
||||
test('Returns details from an HTTPS ADO URL with leading organization', () => {
|
||||
assert.deepStrictEqual(
|
||||
getRepositoryDetailsFromRemoteUrl('https://ms@dev.azure.com/ms/example/_git/repo'),
|
||||
{
|
||||
organizationName: 'ms',
|
||||
projectName: 'example',
|
||||
repositoryName: 'repo',
|
||||
});
|
||||
});
|
||||
|
||||
test('Returns details from a SSH ADO URL', () => {
|
||||
assert.deepStrictEqual(
|
||||
getRepositoryDetailsFromRemoteUrl('git@ssh.dev.azure.com:v3/ms/example/repo'),
|
||||
{
|
||||
organizationName: 'ms',
|
||||
projectName: 'example',
|
||||
repositoryName: 'repo',
|
||||
});
|
||||
});
|
||||
|
||||
test('Returns details from a legacy HTTPS VSTS URL', () => {
|
||||
assert.deepStrictEqual(
|
||||
getRepositoryDetailsFromRemoteUrl('https://ms.visualstudio.com/example/_git/repo'),
|
||||
{
|
||||
organizationName: 'ms',
|
||||
projectName: 'example',
|
||||
repositoryName: 'repo',
|
||||
});
|
||||
});
|
||||
|
||||
test('Returns details from a legacy HTTPS VSTS URL with DefaultCollection', () => {
|
||||
assert.deepStrictEqual(
|
||||
getRepositoryDetailsFromRemoteUrl('https://ms.visualstudio.com/DefaultCollection/example/_git/repo'),
|
||||
{
|
||||
organizationName: 'ms',
|
||||
projectName: 'example',
|
||||
repositoryName: 'repo',
|
||||
});
|
||||
});
|
||||
|
||||
test('Returns details from a legacy SSH VSTS URL', () => {
|
||||
assert.deepStrictEqual(
|
||||
getRepositoryDetailsFromRemoteUrl('ms@vs-ssh.visualstudio.com:v3/ms/example/repo'),
|
||||
{
|
||||
organizationName: 'ms',
|
||||
projectName: 'example',
|
||||
repositoryName: 'repo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('getFormattedRemoteUrl', () => {
|
||||
test('Returns HTTPS ADO URLs as-is', () => {
|
||||
assert.strictEqual(
|
||||
getFormattedRemoteUrl('https://dev.azure.com/ms/example/_git/repo'),
|
||||
'https://dev.azure.com/ms/example/_git/repo');
|
||||
});
|
||||
|
||||
test('Returns HTTPS ADO URLs with leading organization as-is', () => {
|
||||
assert.strictEqual(
|
||||
getFormattedRemoteUrl('https://ms@dev.azure.com/ms/example/_git/repo'),
|
||||
'https://ms@dev.azure.com/ms/example/_git/repo');
|
||||
});
|
||||
|
||||
test('Returns an HTTPS VSTS URL from a SSH ADO URL', () => {
|
||||
assert.strictEqual(
|
||||
getFormattedRemoteUrl('git@ssh.dev.azure.com:v3/ms/example/repo'),
|
||||
'https://ms.visualstudio.com/example/_git/repo');
|
||||
});
|
||||
|
||||
test('Returns an HTTPS VSTS URL from a SSH VSTS URL', () => {
|
||||
assert.strictEqual(
|
||||
getFormattedRemoteUrl('ms@vs-ssh.visualstudio.com:v3/ms/example/repo'),
|
||||
'https://ms.visualstudio.com/example/_git/repo');
|
||||
});
|
||||
});
|
||||
|
||||
suite('getOldFormatBuildDefinitionUrl', () => {
|
||||
test('Returns a legacy HTTPS VSTS build definition URL', () => {
|
||||
assert.strictEqual(
|
||||
getOldFormatBuildDefinitionUrl('ms', 'example', 42),
|
||||
'https://ms.visualstudio.com/example/_build?definitionId=42&_a=summary');
|
||||
});
|
||||
});
|
||||
|
||||
suite('getOldFormatBuildUrl', () => {
|
||||
test('Returns a legacy HTTPS VSTS build URL', () => {
|
||||
assert.strictEqual(
|
||||
getOldFormatBuildUrl('ms', 'example', 42),
|
||||
'https://ms.visualstudio.com/example/_build/results?buildId=42&view=results');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,64 +0,0 @@
|
|||
import * as assert from 'assert';
|
||||
import { isGitHubUrl, getFormattedRemoteUrl, getRepositoryDetailsFromRemoteUrl } from '../../../configure/helper/gitHubHelper';
|
||||
|
||||
suite('GitHub Helper', () => {
|
||||
suite('isGitHubUrl', () => {
|
||||
test('Returns true for HTTPS GitHub URLs', () => {
|
||||
assert.ok(isGitHubUrl('https://github.com/microsoft/azure-pipelines-vscode'));
|
||||
});
|
||||
|
||||
test('Returns true for HTTPS GitHub URLs with trailing .git', () => {
|
||||
assert.ok(isGitHubUrl('https://github.com/microsoft/azure-pipelines-vscode.git'));
|
||||
});
|
||||
|
||||
test('Returns true for SSH GitHub URLs', () => {
|
||||
assert.ok(isGitHubUrl('git@github.com:microsoft/azure-pipelines-vscode.git'));
|
||||
});
|
||||
|
||||
test('Returns false for non-GitHub HTTPS URLs', () => {
|
||||
assert.strictEqual(
|
||||
isGitHubUrl('https://github.coms/microsoft/azure-pipelines-vscode'),
|
||||
false);
|
||||
});
|
||||
|
||||
test('Returns false for non-GitHub SSH URLs', () => {
|
||||
assert.strictEqual(
|
||||
isGitHubUrl('sgit@github.com:microsoft/azure-pipelines-vscode.git'),
|
||||
false);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getRepositoryDetailsFromRemoteUrl', () => {
|
||||
test('Returns owner and repo from an HTTPS URL', () => {
|
||||
assert.deepStrictEqual(
|
||||
getRepositoryDetailsFromRemoteUrl('https://github.com/microsoft/azure-pipelines-vscode'),
|
||||
{ ownerName: 'microsoft', repositoryName: 'azure-pipelines-vscode' });
|
||||
});
|
||||
|
||||
test('Returns owner from an HTTPS URL with trailing .git', () => {
|
||||
assert.deepStrictEqual(
|
||||
getRepositoryDetailsFromRemoteUrl('https://github.com/microsoft/azure-pipelines-vscode.git'),
|
||||
{ ownerName: 'microsoft', repositoryName: 'azure-pipelines-vscode' });
|
||||
});
|
||||
|
||||
test('Returns owner from a SSH URL', () => {
|
||||
assert.deepStrictEqual(
|
||||
getRepositoryDetailsFromRemoteUrl('git@github.com:microsoft/azure-pipelines-vscode.git'),
|
||||
{ ownerName: 'microsoft', repositoryName: 'azure-pipelines-vscode' });
|
||||
});
|
||||
});
|
||||
|
||||
suite('getFormattedRemoteUrl', () => {
|
||||
test('Returns HTTPS URLs as-is', () => {
|
||||
assert.strictEqual(
|
||||
getFormattedRemoteUrl('https://github.com/microsoft/azure-pipelines-vscode.git'),
|
||||
'https://github.com/microsoft/azure-pipelines-vscode.git');
|
||||
});
|
||||
|
||||
test('Returns an HTTPS URL from a SSH URL', () => {
|
||||
assert.strictEqual(
|
||||
getFormattedRemoteUrl('git@github.com:microsoft/azure-pipelines-vscode.git'),
|
||||
'https://github.com/microsoft/azure-pipelines-vscode.git');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,71 +0,0 @@
|
|||
// https://github.com/microsoft/vscode-azure-account/blob/v0.9.11/src/azure-account.api.d.ts
|
||||
// This is the version right before they started versioning their API.
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Terminal, Progress, CancellationToken } from 'vscode';
|
||||
import { ServiceClientCredentials } from 'ms-rest';
|
||||
import { ReadStream } from 'fs';
|
||||
import { TokenCredentialsBase } from '@azure/ms-rest-nodeauth';
|
||||
import { Environment } from '@azure/ms-rest-azure-env';
|
||||
import { SubscriptionModels } from '@azure/arm-subscriptions';
|
||||
|
||||
export type AzureLoginStatus = 'Initializing' | 'LoggingIn' | 'LoggedIn' | 'LoggedOut';
|
||||
|
||||
export interface AzureAccount {
|
||||
readonly status: AzureLoginStatus;
|
||||
readonly onStatusChanged: Event<AzureLoginStatus>;
|
||||
readonly waitForLogin: () => Promise<boolean>;
|
||||
readonly sessions: AzureSession[];
|
||||
readonly onSessionsChanged: Event<void>;
|
||||
readonly subscriptions: AzureSubscription[];
|
||||
readonly onSubscriptionsChanged: Event<void>;
|
||||
readonly waitForSubscriptions: () => Promise<boolean>;
|
||||
readonly filters: AzureResourceFilter[];
|
||||
readonly onFiltersChanged: Event<void>;
|
||||
readonly waitForFilters: () => Promise<boolean>;
|
||||
createCloudShell(os: 'Linux' | 'Windows'): CloudShell;
|
||||
}
|
||||
|
||||
export interface AzureSession {
|
||||
readonly environment: Environment;
|
||||
readonly userId: string;
|
||||
readonly tenantId: string;
|
||||
|
||||
/**
|
||||
* The credentials object for azure-sdk-for-node modules https://github.com/azure/azure-sdk-for-node
|
||||
*/
|
||||
readonly credentials: ServiceClientCredentials;
|
||||
|
||||
/**
|
||||
* The credentials object for azure-sdk-for-js modules https://github.com/azure/azure-sdk-for-js
|
||||
*/
|
||||
readonly credentials2: TokenCredentialsBase;
|
||||
}
|
||||
|
||||
export interface AzureSubscription {
|
||||
readonly session: AzureSession;
|
||||
readonly subscription: SubscriptionModels.Subscription;
|
||||
}
|
||||
|
||||
export type AzureResourceFilter = AzureSubscription;
|
||||
|
||||
export type CloudShellStatus = 'Connecting' | 'Connected' | 'Disconnected';
|
||||
|
||||
export interface UploadOptions {
|
||||
contentLength?: number;
|
||||
progress?: Progress<{ message?: string; increment?: number }>;
|
||||
token?: CancellationToken;
|
||||
}
|
||||
|
||||
export interface CloudShell {
|
||||
readonly status: CloudShellStatus;
|
||||
readonly onStatusChanged: Event<CloudShellStatus>;
|
||||
readonly waitForConnection: () => Promise<boolean>;
|
||||
readonly terminal: Promise<Terminal>;
|
||||
readonly session: Promise<AzureSession>;
|
||||
readonly uploadFile: (filename: string, stream: ReadStream, options?: UploadOptions) => Promise<void>;
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* List of library files to be included in the compilation. */
|
||||
"lib": [ "es2019" ],
|
||||
|
||||
/* Specify module code generation. */
|
||||
"module": "ES2022",
|
||||
|
||||
|
@ -42,9 +39,7 @@
|
|||
"strict": true,
|
||||
|
||||
/* Specify ECMAScript target version. */
|
||||
"target": "es2019",
|
||||
|
||||
"skipLibCheck": true // https://github.com/Azure/ms-rest-js/issues/367
|
||||
"target": "ES2022",
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
/** @type {import('webpack').Configuration} */
|
||||
const config = {
|
||||
|
@ -40,18 +39,5 @@ const config = {
|
|||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: 'src/configure/templates', to: 'configure/templates' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
// Disable optimization until vscode-azure-account supports @azure/core-auth
|
||||
// and we move off of @azure/ms-rest-nodeauth.
|
||||
// https://github.com/Azure/ms-rest-nodeauth/issues/83
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
};
|
||||
module.exports = config;
|
||||
|
|
Загрузка…
Ссылка в новой задаче