Path: blob/main/build/azure-pipelines/common/downloadCopilotVsix.ts
13383 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import fs from 'fs';6import path from 'path';7import { Readable } from 'stream';8import type { ReadableStream } from 'stream/web';9import { pipeline } from 'node:stream/promises';10import yauzl from 'yauzl';11import { type Artifact, e, requestAZDOAPI } from './publish.ts';12import { retry } from './retry.ts';1314const ARTIFACT_NAME = 'copilot_vsix';15const COPILOT_JOB_NAME = 'Copilot';1617interface Timeline {18readonly records: {19readonly name: string;20readonly type: string;21readonly state: string;22readonly result: string;23}[];24}2526function getAzdoFetchOptions() {27return {28headers: {29'Accept': 'application/json;api-version=5.0-preview.1',30'Accept-Encoding': 'gzip, deflate, br',31'Accept-Language': 'en-US,en;q=0.9',32'Referer': 'https://dev.azure.com',33Authorization: `Bearer ${e('SYSTEM_ACCESSTOKEN')}`34}35};36}3738async function getPipelineArtifacts(): Promise<Artifact[]> {39const result = await requestAZDOAPI<{ readonly value: Artifact[] }>('artifacts');40return result.value.filter(a => !/sbom$/.test(a.name));41}4243async function getPipelineTimeline(): Promise<Timeline> {44return await requestAZDOAPI<Timeline>('timeline');45}4647async function checkCopilotJobFailed(): Promise<boolean> {48try {49const timeline = await retry(() => getPipelineTimeline());50const copilotJob = timeline.records.find(51r => r.type === 'Job' && r.name === COPILOT_JOB_NAME52);5354if (copilotJob && copilotJob.state === 'completed' && copilotJob.result !== 'succeeded' && copilotJob.result !== 'succeededWithIssues') {55return true;56}57} catch (err) {58console.error(`WARNING: Failed to check Copilot job status: ${err}`);59}6061return false;62}6364async function downloadArtifact(artifact: Artifact, downloadPath: string): Promise<void> {65const abortController = new AbortController();66const timeout = setTimeout(() => abortController.abort(), 4 * 60 * 1000);6768try {69const res = await fetch(artifact.resource.downloadUrl, { ...getAzdoFetchOptions(), signal: abortController.signal });7071if (!res.ok) {72throw new Error(`Unexpected status code: ${res.status}`);73}7475await pipeline(Readable.fromWeb(res.body as ReadableStream), fs.createWriteStream(downloadPath));76} finally {77clearTimeout(timeout);78}79}8081async function unzip(zipPath: string, outputPath: string): Promise<string[]> {82return new Promise((resolve, reject) => {83yauzl.open(zipPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {84if (err) {85return reject(err);86}8788const result: string[] = [];89zipfile!.on('entry', entry => {90if (/\/$/.test(entry.fileName)) {91zipfile!.readEntry();92} else {93zipfile!.openReadStream(entry, (err, istream) => {94if (err) {95return reject(err);96}9798const filePath = path.join(outputPath, entry.fileName);99fs.mkdirSync(path.dirname(filePath), { recursive: true });100101const ostream = fs.createWriteStream(filePath);102ostream.on('finish', () => {103result.push(filePath);104zipfile!.readEntry();105});106istream?.on('error', err => reject(err));107istream!.pipe(ostream);108});109}110});111112zipfile!.on('close', () => resolve(result));113zipfile!.readEntry();114});115});116}117118async function waitForArtifact(): Promise<Artifact> {119for (let index = 0; index < 60; index++) {120try {121console.log(`Waiting for Copilot VSIX artifact to be uploaded (${index + 1}/60)...`);122123// Check if the Copilot job failed124const failed = await checkCopilotJobFailed();125if (failed) {126throw new Error('Copilot job failed. Aborting.');127}128129const allArtifacts = await retry(() => getPipelineArtifacts());130const artifact = allArtifacts.find(a => a.name === ARTIFACT_NAME);131132if (artifact) {133console.log(' * Copilot VSIX artifact found');134return artifact;135}136137console.log(' * Not found yet, waiting...');138} catch (err) {139if (err instanceof Error && err.message.includes('Copilot job failed')) {140throw err;141}142console.error(`WARNING: Failed to check for artifact: ${err}`);143}144145await new Promise(c => setTimeout(c, 30_000));146}147148throw new Error('Copilot VSIX artifact was not uploaded within 30 minutes.');149}150151async function main(): Promise<void> {152const outputDir = path.resolve('.build/extensions/copilot');153154console.log('Waiting for Copilot VSIX artifact...');155const artifact = await waitForArtifact();156157// Download the artifact (a zip containing the VSIX)158const tmpDir = path.resolve('.build/tmp-copilot');159fs.mkdirSync(tmpDir, { recursive: true });160const artifactZipPath = path.join(tmpDir, 'artifact.zip');161162console.log('Downloading Copilot VSIX artifact...');163await retry(() => downloadArtifact(artifact, artifactZipPath));164165// Extract the artifact zip to get the VSIX file166console.log('Extracting artifact zip...');167const artifactFiles = await unzip(artifactZipPath, tmpDir);168const vsixFile = artifactFiles.find(f => f.endsWith('.vsix'));169170if (!vsixFile) {171throw new Error('No .vsix file found in the Copilot artifact');172}173174console.log(`Found VSIX: ${vsixFile}`);175176// Extract the VSIX (which is also a zip) to the output directory177// VSIX files contain an 'extension/' folder with the actual extension files178console.log(`Extracting VSIX to ${outputDir}...`);179fs.rmSync(outputDir, { recursive: true, force: true });180fs.mkdirSync(outputDir, { recursive: true });181182const vsixTmpDir = path.join(tmpDir, 'vsix-contents');183fs.mkdirSync(vsixTmpDir, { recursive: true });184185await unzip(vsixFile, vsixTmpDir);186187// Move extension/ contents to the output directory188const extensionDir = path.join(vsixTmpDir, 'extension');189if (!fs.existsSync(extensionDir)) {190throw new Error('VSIX does not contain an extension/ directory');191}192193// Copy all files from extension/ to outputDir194copyDirSync(extensionDir, outputDir);195196// Cleanup197fs.rmSync(tmpDir, { recursive: true, force: true });198199console.log('Copilot VSIX successfully extracted to .build/extensions/copilot/');200}201202function copyDirSync(src: string, dest: string): void {203fs.mkdirSync(dest, { recursive: true });204const entries = fs.readdirSync(src, { withFileTypes: true });205for (const entry of entries) {206const srcPath = path.join(src, entry.name);207const destPath = path.join(dest, entry.name);208if (entry.isDirectory()) {209copyDirSync(srcPath, destPath);210} else {211fs.copyFileSync(srcPath, destPath);212}213}214}215216main().then(() => {217process.exit(0);218}, err => {219console.error(err);220process.exit(1);221});222223224