Merge branch 'main' into user/winstonliu/upstream-language-server

This commit is contained in:
Winston Liu 2024-10-04 16:16:00 -07:00
Родитель d89868e046 6c788dbfff
Коммит 82e7f68cee
60 изменённых файлов: 6163 добавлений и 9395 удалений

Просмотреть файл

@ -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
.gitignore поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
coverage
dist
out
node_modules

16
.vscode-test.mjs Normal file
Просмотреть файл

@ -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',
}
});

3
.vscode/extensions.json поставляемый
Просмотреть файл

@ -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"
]
}

2
.vscode/launch.json поставляемый
Просмотреть файл

@ -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.

Просмотреть файл

@ -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 dont 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):

3906
package-lock.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

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 586 KiB

Двоичные данные
resources/gitHubPatScope.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 83 KiB

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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');
});
});
});

71
src/typings/azure-account.api.d.ts поставляемый
Просмотреть файл

@ -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;