Path: blob/main/build/azure-pipelines/common/publish.js
3520 views
"use strict";1var __importDefault = (this && this.__importDefault) || function (mod) {2return (mod && mod.__esModule) ? mod : { "default": mod };3};4Object.defineProperty(exports, "__esModule", { value: true });5exports.e = e;6exports.requestAZDOAPI = requestAZDOAPI;7/*---------------------------------------------------------------------------------------------8* Copyright (c) Microsoft Corporation. All rights reserved.9* Licensed under the MIT License. See License.txt in the project root for license information.10*--------------------------------------------------------------------------------------------*/11const fs_1 = __importDefault(require("fs"));12const path_1 = __importDefault(require("path"));13const stream_1 = require("stream");14const promises_1 = require("node:stream/promises");15const yauzl_1 = __importDefault(require("yauzl"));16const crypto_1 = __importDefault(require("crypto"));17const retry_1 = require("./retry");18const cosmos_1 = require("@azure/cosmos");19const child_process_1 = __importDefault(require("child_process"));20const os_1 = __importDefault(require("os"));21const node_worker_threads_1 = require("node:worker_threads");22const msal_node_1 = require("@azure/msal-node");23const storage_blob_1 = require("@azure/storage-blob");24const jws_1 = __importDefault(require("jws"));25const node_timers_1 = require("node:timers");26function e(name) {27const result = process.env[name];28if (typeof result !== 'string') {29throw new Error(`Missing env: ${name}`);30}31return result;32}33function hashStream(hashName, stream) {34return new Promise((c, e) => {35const shasum = crypto_1.default.createHash(hashName);36stream37.on('data', shasum.update.bind(shasum))38.on('error', e)39.on('close', () => c(shasum.digest()));40});41}42var StatusCode;43(function (StatusCode) {44StatusCode["Pass"] = "pass";45StatusCode["Aborted"] = "aborted";46StatusCode["Inprogress"] = "inprogress";47StatusCode["FailCanRetry"] = "failCanRetry";48StatusCode["FailDoNotRetry"] = "failDoNotRetry";49StatusCode["PendingAnalysis"] = "pendingAnalysis";50StatusCode["Cancelled"] = "cancelled";51})(StatusCode || (StatusCode = {}));52function getCertificateBuffer(input) {53return Buffer.from(input.replace(/-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----|\n/g, ''), 'base64');54}55function getThumbprint(input, algorithm) {56const buffer = getCertificateBuffer(input);57return crypto_1.default.createHash(algorithm).update(buffer).digest();58}59function getKeyFromPFX(pfx) {60const pfxCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pfx');61const pemKeyPath = path_1.default.join(os_1.default.tmpdir(), 'key.pem');62try {63const pfxCertificate = Buffer.from(pfx, 'base64');64fs_1.default.writeFileSync(pfxCertificatePath, pfxCertificate);65child_process_1.default.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nocerts -nodes -out "${pemKeyPath}" -passin pass:`);66const raw = fs_1.default.readFileSync(pemKeyPath, 'utf-8');67const result = raw.match(/-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g)[0];68return result;69}70finally {71fs_1.default.rmSync(pfxCertificatePath, { force: true });72fs_1.default.rmSync(pemKeyPath, { force: true });73}74}75function getCertificatesFromPFX(pfx) {76const pfxCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pfx');77const pemCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pem');78try {79const pfxCertificate = Buffer.from(pfx, 'base64');80fs_1.default.writeFileSync(pfxCertificatePath, pfxCertificate);81child_process_1.default.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nokeys -out "${pemCertificatePath}" -passin pass:`);82const raw = fs_1.default.readFileSync(pemCertificatePath, 'utf-8');83const matches = raw.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);84return matches ? matches.reverse() : [];85}86finally {87fs_1.default.rmSync(pfxCertificatePath, { force: true });88fs_1.default.rmSync(pemCertificatePath, { force: true });89}90}91class ESRPReleaseService {92log;93clientId;94accessToken;95requestSigningCertificates;96requestSigningKey;97containerClient;98stagingSasToken;99static async create(log, tenantId, clientId, authCertificatePfx, requestSigningCertificatePfx, containerClient, stagingSasToken) {100const authKey = getKeyFromPFX(authCertificatePfx);101const authCertificate = getCertificatesFromPFX(authCertificatePfx)[0];102const requestSigningKey = getKeyFromPFX(requestSigningCertificatePfx);103const requestSigningCertificates = getCertificatesFromPFX(requestSigningCertificatePfx);104const app = new msal_node_1.ConfidentialClientApplication({105auth: {106clientId,107authority: `https://login.microsoftonline.com/${tenantId}`,108clientCertificate: {109thumbprintSha256: getThumbprint(authCertificate, 'sha256').toString('hex'),110privateKey: authKey,111x5c: authCertificate112}113}114});115const response = await app.acquireTokenByClientCredential({116scopes: ['https://api.esrp.microsoft.com/.default']117});118return new ESRPReleaseService(log, clientId, response.accessToken, requestSigningCertificates, requestSigningKey, containerClient, stagingSasToken);119}120static API_URL = 'https://api.esrp.microsoft.com/api/v3/releaseservices/clients/';121constructor(log, clientId, accessToken, requestSigningCertificates, requestSigningKey, containerClient, stagingSasToken) {122this.log = log;123this.clientId = clientId;124this.accessToken = accessToken;125this.requestSigningCertificates = requestSigningCertificates;126this.requestSigningKey = requestSigningKey;127this.containerClient = containerClient;128this.stagingSasToken = stagingSasToken;129}130async createRelease(version, filePath, friendlyFileName) {131const correlationId = crypto_1.default.randomUUID();132const blobClient = this.containerClient.getBlockBlobClient(correlationId);133this.log(`Uploading ${filePath} to ${blobClient.url}`);134await blobClient.uploadFile(filePath);135this.log('Uploaded blob successfully');136try {137this.log(`Submitting release for ${version}: ${filePath}`);138const submitReleaseResult = await this.submitRelease(version, filePath, friendlyFileName, correlationId, blobClient);139this.log(`Successfully submitted release ${submitReleaseResult.operationId}. Polling for completion...`);140// Poll every 5 seconds, wait 60 minutes max -> poll 60/5*60=720 times141for (let i = 0; i < 720; i++) {142await new Promise(c => setTimeout(c, 5000));143const releaseStatus = await this.getReleaseStatus(submitReleaseResult.operationId);144if (releaseStatus.status === 'pass') {145break;146}147else if (releaseStatus.status === 'aborted') {148this.log(JSON.stringify(releaseStatus));149throw new Error(`Release was aborted`);150}151else if (releaseStatus.status !== 'inprogress') {152this.log(JSON.stringify(releaseStatus));153throw new Error(`Unknown error when polling for release`);154}155}156const releaseDetails = await this.getReleaseDetails(submitReleaseResult.operationId);157if (releaseDetails.status !== 'pass') {158throw new Error(`Timed out waiting for release: ${JSON.stringify(releaseDetails)}`);159}160this.log('Successfully created release:', releaseDetails.files[0].fileDownloadDetails[0].downloadUrl);161return releaseDetails.files[0].fileDownloadDetails[0].downloadUrl;162}163finally {164this.log(`Deleting blob ${blobClient.url}`);165await blobClient.delete();166this.log('Deleted blob successfully');167}168}169async submitRelease(version, filePath, friendlyFileName, correlationId, blobClient) {170const size = fs_1.default.statSync(filePath).size;171const hash = await hashStream('sha256', fs_1.default.createReadStream(filePath));172const blobUrl = `${blobClient.url}?${this.stagingSasToken}`;173const message = {174customerCorrelationId: correlationId,175esrpCorrelationId: correlationId,176driEmail: ['[email protected]'],177createdBy: { userPrincipalName: '[email protected]' },178owners: [{ owner: { userPrincipalName: '[email protected]' } }],179approvers: [{ approver: { userPrincipalName: '[email protected]' }, isAutoApproved: true, isMandatory: false }],180releaseInfo: {181title: 'VS Code',182properties: {183'ReleaseContentType': 'InstallPackage'184},185minimumNumberOfApprovers: 1186},187productInfo: {188name: 'VS Code',189version,190description: 'VS Code'191},192accessPermissionsInfo: {193mainPublisher: 'VSCode',194channelDownloadEntityDetails: {195AllDownloadEntities: ['VSCode']196}197},198routingInfo: {199intent: 'filedownloadlinkgeneration'200},201files: [{202name: path_1.default.basename(filePath),203friendlyFileName,204tenantFileLocation: blobUrl,205tenantFileLocationType: 'AzureBlob',206sourceLocation: {207type: 'azureBlob',208blobUrl209},210hashType: 'sha256',211hash: Array.from(hash),212sizeInBytes: size213}]214};215message.jwsToken = await this.generateJwsToken(message);216const res = await fetch(`${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations`, {217method: 'POST',218headers: {219'Content-Type': 'application/json',220'Authorization': `Bearer ${this.accessToken}`221},222body: JSON.stringify(message)223});224if (!res.ok) {225const text = await res.text();226throw new Error(`Failed to submit release: ${res.statusText}\n${text}`);227}228return await res.json();229}230async getReleaseStatus(releaseId) {231const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grs/${releaseId}`;232const res = await (0, retry_1.retry)(() => fetch(url, {233headers: {234'Authorization': `Bearer ${this.accessToken}`235}236}));237if (!res.ok) {238const text = await res.text();239throw new Error(`Failed to get release status: ${res.statusText}\n${text}`);240}241return await res.json();242}243async getReleaseDetails(releaseId) {244const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grd/${releaseId}`;245const res = await (0, retry_1.retry)(() => fetch(url, {246headers: {247'Authorization': `Bearer ${this.accessToken}`248}249}));250if (!res.ok) {251const text = await res.text();252throw new Error(`Failed to get release status: ${res.statusText}\n${text}`);253}254return await res.json();255}256async generateJwsToken(message) {257return jws_1.default.sign({258header: {259alg: 'RS256',260crit: ['exp', 'x5t'],261// Release service uses ticks, not seconds :roll_eyes: (https://stackoverflow.com/a/7968483)262exp: ((Date.now() + (6 * 60 * 1000)) * 10000) + 621355968000000000,263// Release service uses hex format, not base64url :roll_eyes:264x5t: getThumbprint(this.requestSigningCertificates[0], 'sha1').toString('hex'),265// Release service uses a '.' separated string, not an array of strings :roll_eyes:266x5c: this.requestSigningCertificates.map(c => getCertificateBuffer(c).toString('base64url')).join('.'),267},268payload: message,269privateKey: this.requestSigningKey,270});271}272}273class State {274statePath;275set = new Set();276constructor() {277const pipelineWorkspacePath = e('PIPELINE_WORKSPACE');278const previousState = fs_1.default.readdirSync(pipelineWorkspacePath)279.map(name => /^artifacts_processed_(\d+)$/.exec(name))280.filter((match) => !!match)281.map(match => ({ name: match[0], attempt: Number(match[1]) }))282.sort((a, b) => b.attempt - a.attempt)[0];283if (previousState) {284const previousStatePath = path_1.default.join(pipelineWorkspacePath, previousState.name, previousState.name + '.txt');285fs_1.default.readFileSync(previousStatePath, 'utf8').split(/\n/).filter(name => !!name).forEach(name => this.set.add(name));286}287const stageAttempt = e('SYSTEM_STAGEATTEMPT');288this.statePath = path_1.default.join(pipelineWorkspacePath, `artifacts_processed_${stageAttempt}`, `artifacts_processed_${stageAttempt}.txt`);289fs_1.default.mkdirSync(path_1.default.dirname(this.statePath), { recursive: true });290fs_1.default.writeFileSync(this.statePath, [...this.set.values()].map(name => `${name}\n`).join(''));291}292get size() {293return this.set.size;294}295has(name) {296return this.set.has(name);297}298add(name) {299this.set.add(name);300fs_1.default.appendFileSync(this.statePath, `${name}\n`);301}302[Symbol.iterator]() {303return this.set[Symbol.iterator]();304}305}306const azdoFetchOptions = {307headers: {308// Pretend we're a web browser to avoid download rate limits309'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0',310'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',311'Accept-Encoding': 'gzip, deflate, br',312'Accept-Language': 'en-US,en;q=0.9',313'Referer': 'https://dev.azure.com',314Authorization: `Bearer ${e('SYSTEM_ACCESSTOKEN')}`315}316};317async function requestAZDOAPI(path) {318const abortController = new AbortController();319const timeout = setTimeout(() => abortController.abort(), 2 * 60 * 1000);320try {321const res = await (0, retry_1.retry)(() => fetch(`${e('BUILDS_API_URL')}${path}?api-version=6.0`, { ...azdoFetchOptions, signal: abortController.signal }));322if (!res.ok) {323throw new Error(`Unexpected status code: ${res.status}`);324}325return await res.json();326}327finally {328clearTimeout(timeout);329}330}331async function getPipelineArtifacts() {332const result = await requestAZDOAPI('artifacts');333return result.value.filter(a => /^vscode_/.test(a.name) && !/sbom$/.test(a.name));334}335async function getPipelineTimeline() {336return await requestAZDOAPI('timeline');337}338async function downloadArtifact(artifact, downloadPath) {339const abortController = new AbortController();340const timeout = setTimeout(() => abortController.abort(), 4 * 60 * 1000);341try {342const res = await fetch(artifact.resource.downloadUrl, { ...azdoFetchOptions, signal: abortController.signal });343if (!res.ok) {344throw new Error(`Unexpected status code: ${res.status}`);345}346await (0, promises_1.pipeline)(stream_1.Readable.fromWeb(res.body), fs_1.default.createWriteStream(downloadPath));347}348finally {349clearTimeout(timeout);350}351}352async function unzip(packagePath, outputPath) {353return new Promise((resolve, reject) => {354yauzl_1.default.open(packagePath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {355if (err) {356return reject(err);357}358const result = [];359zipfile.on('entry', entry => {360if (/\/$/.test(entry.fileName)) {361zipfile.readEntry();362}363else {364zipfile.openReadStream(entry, (err, istream) => {365if (err) {366return reject(err);367}368const filePath = path_1.default.join(outputPath, entry.fileName);369fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });370const ostream = fs_1.default.createWriteStream(filePath);371ostream.on('finish', () => {372result.push(filePath);373zipfile.readEntry();374});375istream?.on('error', err => reject(err));376istream.pipe(ostream);377});378}379});380zipfile.on('close', () => resolve(result));381zipfile.readEntry();382});383});384}385// Contains all of the logic for mapping details to our actual product names in CosmosDB386function getPlatform(product, os, arch, type) {387switch (os) {388case 'win32':389switch (product) {390case 'client': {391switch (type) {392case 'archive':393return `win32-${arch}-archive`;394case 'setup':395return `win32-${arch}`;396case 'user-setup':397return `win32-${arch}-user`;398default:399throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`);400}401}402case 'server':403return `server-win32-${arch}`;404case 'web':405return `server-win32-${arch}-web`;406case 'cli':407return `cli-win32-${arch}`;408default:409throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`);410}411case 'alpine':412switch (product) {413case 'server':414return `server-alpine-${arch}`;415case 'web':416return `server-alpine-${arch}-web`;417case 'cli':418return `cli-alpine-${arch}`;419default:420throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`);421}422case 'linux':423switch (type) {424case 'snap':425return `linux-snap-${arch}`;426case 'archive-unsigned':427switch (product) {428case 'client':429return `linux-${arch}`;430case 'server':431return `server-linux-${arch}`;432case 'web':433if (arch === 'standalone') {434return 'web-standalone';435}436return `server-linux-${arch}-web`;437default:438throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`);439}440case 'deb-package':441return `linux-deb-${arch}`;442case 'rpm-package':443return `linux-rpm-${arch}`;444case 'cli':445return `cli-linux-${arch}`;446default:447throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`);448}449case 'darwin':450switch (product) {451case 'client':452if (arch === 'x64') {453return 'darwin';454}455return `darwin-${arch}`;456case 'server':457if (arch === 'x64') {458return 'server-darwin';459}460return `server-darwin-${arch}`;461case 'web':462if (arch === 'x64') {463return 'server-darwin-web';464}465return `server-darwin-${arch}-web`;466case 'cli':467return `cli-darwin-${arch}`;468default:469throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`);470}471default:472throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`);473}474}475// Contains all of the logic for mapping types to our actual types in CosmosDB476function getRealType(type) {477switch (type) {478case 'user-setup':479return 'setup';480case 'deb-package':481case 'rpm-package':482return 'package';483default:484return type;485}486}487async function withLease(client, fn) {488const lease = client.getBlobLeaseClient();489for (let i = 0; i < 360; i++) { // Try to get lease for 30 minutes490try {491await client.uploadData(new ArrayBuffer()); // blob needs to exist for lease to be acquired492await lease.acquireLease(60);493try {494const abortController = new AbortController();495const refresher = new Promise((c, e) => {496abortController.signal.onabort = () => {497(0, node_timers_1.clearInterval)(interval);498c();499};500const interval = (0, node_timers_1.setInterval)(() => {501lease.renewLease().catch(err => {502(0, node_timers_1.clearInterval)(interval);503e(new Error('Failed to renew lease ' + err));504});505}, 30_000);506});507const result = await Promise.race([fn(), refresher]);508abortController.abort();509return result;510}511finally {512await lease.releaseLease();513}514}515catch (err) {516if (err.statusCode !== 409 && err.statusCode !== 412) {517throw err;518}519await new Promise(c => setTimeout(c, 5000));520}521}522throw new Error('Failed to acquire lease on blob after 30 minutes');523}524async function processArtifact(artifact, filePath) {525const log = (...args) => console.log(`[${artifact.name}]`, ...args);526const match = /^vscode_(?<product>[^_]+)_(?<os>[^_]+)(?:_legacy)?_(?<arch>[^_]+)_(?<unprocessedType>[^_]+)$/.exec(artifact.name);527if (!match) {528throw new Error(`Invalid artifact name: ${artifact.name}`);529}530const { cosmosDBAccessToken, blobServiceAccessToken } = JSON.parse(e('PUBLISH_AUTH_TOKENS'));531const quality = e('VSCODE_QUALITY');532const version = e('BUILD_SOURCEVERSION');533const friendlyFileName = `${quality}/${version}/${path_1.default.basename(filePath)}`;534const blobServiceClient = new storage_blob_1.BlobServiceClient(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, { getToken: async () => blobServiceAccessToken });535const leasesContainerClient = blobServiceClient.getContainerClient('leases');536await leasesContainerClient.createIfNotExists();537const leaseBlobClient = leasesContainerClient.getBlockBlobClient(friendlyFileName);538log(`Acquiring lease for: ${friendlyFileName}`);539await withLease(leaseBlobClient, async () => {540log(`Successfully acquired lease for: ${friendlyFileName}`);541const url = `${e('PRSS_CDN_URL')}/${friendlyFileName}`;542const res = await (0, retry_1.retry)(() => fetch(url));543if (res.status === 200) {544log(`Already released and provisioned: ${url}`);545}546else {547const stagingContainerClient = blobServiceClient.getContainerClient('staging');548await stagingContainerClient.createIfNotExists();549const now = new Date().valueOf();550const oneHour = 60 * 60 * 1000;551const oneHourAgo = new Date(now - oneHour);552const oneHourFromNow = new Date(now + oneHour);553const userDelegationKey = await blobServiceClient.getUserDelegationKey(oneHourAgo, oneHourFromNow);554const sasOptions = { containerName: 'staging', permissions: storage_blob_1.ContainerSASPermissions.from({ read: true }), startsOn: oneHourAgo, expiresOn: oneHourFromNow };555const stagingSasToken = (0, storage_blob_1.generateBlobSASQueryParameters)(sasOptions, userDelegationKey, e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')).toString();556const releaseService = await ESRPReleaseService.create(log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), e('RELEASE_AUTH_CERT'), e('RELEASE_REQUEST_SIGNING_CERT'), stagingContainerClient, stagingSasToken);557await releaseService.createRelease(version, filePath, friendlyFileName);558}559const { product, os, arch, unprocessedType } = match.groups;560const platform = getPlatform(product, os, arch, unprocessedType);561const type = getRealType(unprocessedType);562const size = fs_1.default.statSync(filePath).size;563const stream = fs_1.default.createReadStream(filePath);564const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256565const asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true };566log('Creating asset...');567const result = await (0, retry_1.retry)(async (attempt) => {568log(`Creating asset in Cosmos DB (attempt ${attempt})...`);569const client = new cosmos_1.CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) });570const scripts = client.database('builds').container(quality).scripts;571const { resource: result } = await scripts.storedProcedure('createAsset').execute('', [version, asset, true]);572return result;573});574if (result === 'already exists') {575log('Asset already exists!');576}577else {578log('Asset successfully created: ', JSON.stringify(asset, undefined, 2));579}580});581log(`Successfully released lease for: ${friendlyFileName}`);582}583// It is VERY important that we don't download artifacts too much too fast from AZDO.584// AZDO throttles us SEVERELY if we do. Not just that, but they also close open585// sockets, so the whole things turns to a grinding halt. So, downloading and extracting586// happens serially in the main thread, making the downloads are spaced out587// properly. For each extracted artifact, we spawn a worker thread to upload it to588// the CDN and finally update the build in Cosmos DB.589async function main() {590if (!node_worker_threads_1.isMainThread) {591const { artifact, artifactFilePath } = node_worker_threads_1.workerData;592await processArtifact(artifact, artifactFilePath);593return;594}595const done = new State();596const processing = new Set();597for (const name of done) {598console.log(`\u2705 ${name}`);599}600const stages = new Set(['Compile']);601if (e('VSCODE_BUILD_STAGE_LINUX') === 'True' ||602e('VSCODE_BUILD_STAGE_ALPINE') === 'True' ||603e('VSCODE_BUILD_STAGE_MACOS') === 'True' ||604e('VSCODE_BUILD_STAGE_WINDOWS') === 'True') {605stages.add('CompileCLI');606}607if (e('VSCODE_BUILD_STAGE_WINDOWS') === 'True') {608stages.add('Windows');609}610if (e('VSCODE_BUILD_STAGE_LINUX') === 'True') {611stages.add('Linux');612}613if (e('VSCODE_BUILD_STAGE_ALPINE') === 'True') {614stages.add('Alpine');615}616if (e('VSCODE_BUILD_STAGE_MACOS') === 'True') {617stages.add('macOS');618}619if (e('VSCODE_BUILD_STAGE_WEB') === 'True') {620stages.add('Web');621}622let timeline;623let artifacts;624let resultPromise = Promise.resolve([]);625const operations = [];626while (true) {627[timeline, artifacts] = await Promise.all([(0, retry_1.retry)(() => getPipelineTimeline()), (0, retry_1.retry)(() => getPipelineArtifacts())]);628const stagesCompleted = new Set(timeline.records.filter(r => r.type === 'Stage' && r.state === 'completed' && stages.has(r.name)).map(r => r.name));629const stagesInProgress = [...stages].filter(s => !stagesCompleted.has(s));630const artifactsInProgress = artifacts.filter(a => processing.has(a.name));631if (stagesInProgress.length === 0 && artifacts.length === done.size + processing.size) {632break;633}634else if (stagesInProgress.length > 0) {635console.log('Stages in progress:', stagesInProgress.join(', '));636}637else if (artifactsInProgress.length > 0) {638console.log('Artifacts in progress:', artifactsInProgress.map(a => a.name).join(', '));639}640else {641console.log(`Waiting for a total of ${artifacts.length}, ${done.size} done, ${processing.size} in progress...`);642}643for (const artifact of artifacts) {644if (done.has(artifact.name) || processing.has(artifact.name)) {645continue;646}647console.log(`[${artifact.name}] Found new artifact`);648const artifactZipPath = path_1.default.join(e('AGENT_TEMPDIRECTORY'), `${artifact.name}.zip`);649await (0, retry_1.retry)(async (attempt) => {650const start = Date.now();651console.log(`[${artifact.name}] Downloading (attempt ${attempt})...`);652await downloadArtifact(artifact, artifactZipPath);653const archiveSize = fs_1.default.statSync(artifactZipPath).size;654const downloadDurationS = (Date.now() - start) / 1000;655const downloadSpeedKBS = Math.round((archiveSize / 1024) / downloadDurationS);656console.log(`[${artifact.name}] Successfully downloaded after ${Math.floor(downloadDurationS)} seconds(${downloadSpeedKBS} KB/s).`);657});658const artifactFilePaths = await unzip(artifactZipPath, e('AGENT_TEMPDIRECTORY'));659const artifactFilePath = artifactFilePaths.filter(p => !/_manifest/.test(p))[0];660processing.add(artifact.name);661const promise = new Promise((resolve, reject) => {662const worker = new node_worker_threads_1.Worker(__filename, { workerData: { artifact, artifactFilePath } });663worker.on('error', reject);664worker.on('exit', code => {665if (code === 0) {666resolve();667}668else {669reject(new Error(`[${artifact.name}] Worker stopped with exit code ${code}`));670}671});672});673const operation = promise.then(() => {674processing.delete(artifact.name);675done.add(artifact.name);676console.log(`\u2705 ${artifact.name} `);677});678operations.push({ name: artifact.name, operation });679resultPromise = Promise.allSettled(operations.map(o => o.operation));680}681await new Promise(c => setTimeout(c, 10_000));682}683console.log(`Found all ${done.size + processing.size} artifacts, waiting for ${processing.size} artifacts to finish publishing...`);684const artifactsInProgress = operations.filter(o => processing.has(o.name));685if (artifactsInProgress.length > 0) {686console.log('Artifacts in progress:', artifactsInProgress.map(a => a.name).join(', '));687}688const results = await resultPromise;689for (let i = 0; i < operations.length; i++) {690const result = results[i];691if (result.status === 'rejected') {692console.error(`[${operations[i].name}]`, result.reason);693}694}695// Fail the job if any of the artifacts failed to publish696if (results.some(r => r.status === 'rejected')) {697throw new Error('Some artifacts failed to publish');698}699// Also fail the job if any of the stages did not succeed700let shouldFail = false;701for (const stage of stages) {702const record = timeline.records.find(r => r.name === stage && r.type === 'Stage');703if (record.result !== 'succeeded' && record.result !== 'succeededWithIssues') {704shouldFail = true;705console.error(`Stage ${stage} did not succeed: ${record.result}`);706}707}708if (shouldFail) {709throw new Error('Some stages did not succeed');710}711console.log(`All ${done.size} artifacts published!`);712}713if (require.main === module) {714main().then(() => {715process.exit(0);716}, err => {717console.error(err);718process.exit(1);719});720}721//# sourceMappingURL=publish.js.map722723