Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/azure-pipelines/common/downloadCopilotVsix.ts
13383 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import fs from 'fs';
7
import path from 'path';
8
import { Readable } from 'stream';
9
import type { ReadableStream } from 'stream/web';
10
import { pipeline } from 'node:stream/promises';
11
import yauzl from 'yauzl';
12
import { type Artifact, e, requestAZDOAPI } from './publish.ts';
13
import { retry } from './retry.ts';
14
15
const ARTIFACT_NAME = 'copilot_vsix';
16
const COPILOT_JOB_NAME = 'Copilot';
17
18
interface Timeline {
19
readonly records: {
20
readonly name: string;
21
readonly type: string;
22
readonly state: string;
23
readonly result: string;
24
}[];
25
}
26
27
function getAzdoFetchOptions() {
28
return {
29
headers: {
30
'Accept': 'application/json;api-version=5.0-preview.1',
31
'Accept-Encoding': 'gzip, deflate, br',
32
'Accept-Language': 'en-US,en;q=0.9',
33
'Referer': 'https://dev.azure.com',
34
Authorization: `Bearer ${e('SYSTEM_ACCESSTOKEN')}`
35
}
36
};
37
}
38
39
async function getPipelineArtifacts(): Promise<Artifact[]> {
40
const result = await requestAZDOAPI<{ readonly value: Artifact[] }>('artifacts');
41
return result.value.filter(a => !/sbom$/.test(a.name));
42
}
43
44
async function getPipelineTimeline(): Promise<Timeline> {
45
return await requestAZDOAPI<Timeline>('timeline');
46
}
47
48
async function checkCopilotJobFailed(): Promise<boolean> {
49
try {
50
const timeline = await retry(() => getPipelineTimeline());
51
const copilotJob = timeline.records.find(
52
r => r.type === 'Job' && r.name === COPILOT_JOB_NAME
53
);
54
55
if (copilotJob && copilotJob.state === 'completed' && copilotJob.result !== 'succeeded' && copilotJob.result !== 'succeededWithIssues') {
56
return true;
57
}
58
} catch (err) {
59
console.error(`WARNING: Failed to check Copilot job status: ${err}`);
60
}
61
62
return false;
63
}
64
65
async function downloadArtifact(artifact: Artifact, downloadPath: string): Promise<void> {
66
const abortController = new AbortController();
67
const timeout = setTimeout(() => abortController.abort(), 4 * 60 * 1000);
68
69
try {
70
const res = await fetch(artifact.resource.downloadUrl, { ...getAzdoFetchOptions(), signal: abortController.signal });
71
72
if (!res.ok) {
73
throw new Error(`Unexpected status code: ${res.status}`);
74
}
75
76
await pipeline(Readable.fromWeb(res.body as ReadableStream), fs.createWriteStream(downloadPath));
77
} finally {
78
clearTimeout(timeout);
79
}
80
}
81
82
async function unzip(zipPath: string, outputPath: string): Promise<string[]> {
83
return new Promise((resolve, reject) => {
84
yauzl.open(zipPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
85
if (err) {
86
return reject(err);
87
}
88
89
const result: string[] = [];
90
zipfile!.on('entry', entry => {
91
if (/\/$/.test(entry.fileName)) {
92
zipfile!.readEntry();
93
} else {
94
zipfile!.openReadStream(entry, (err, istream) => {
95
if (err) {
96
return reject(err);
97
}
98
99
const filePath = path.join(outputPath, entry.fileName);
100
fs.mkdirSync(path.dirname(filePath), { recursive: true });
101
102
const ostream = fs.createWriteStream(filePath);
103
ostream.on('finish', () => {
104
result.push(filePath);
105
zipfile!.readEntry();
106
});
107
istream?.on('error', err => reject(err));
108
istream!.pipe(ostream);
109
});
110
}
111
});
112
113
zipfile!.on('close', () => resolve(result));
114
zipfile!.readEntry();
115
});
116
});
117
}
118
119
async function waitForArtifact(): Promise<Artifact> {
120
for (let index = 0; index < 60; index++) {
121
try {
122
console.log(`Waiting for Copilot VSIX artifact to be uploaded (${index + 1}/60)...`);
123
124
// Check if the Copilot job failed
125
const failed = await checkCopilotJobFailed();
126
if (failed) {
127
throw new Error('Copilot job failed. Aborting.');
128
}
129
130
const allArtifacts = await retry(() => getPipelineArtifacts());
131
const artifact = allArtifacts.find(a => a.name === ARTIFACT_NAME);
132
133
if (artifact) {
134
console.log(' * Copilot VSIX artifact found');
135
return artifact;
136
}
137
138
console.log(' * Not found yet, waiting...');
139
} catch (err) {
140
if (err instanceof Error && err.message.includes('Copilot job failed')) {
141
throw err;
142
}
143
console.error(`WARNING: Failed to check for artifact: ${err}`);
144
}
145
146
await new Promise(c => setTimeout(c, 30_000));
147
}
148
149
throw new Error('Copilot VSIX artifact was not uploaded within 30 minutes.');
150
}
151
152
async function main(): Promise<void> {
153
const outputDir = path.resolve('.build/extensions/copilot');
154
155
console.log('Waiting for Copilot VSIX artifact...');
156
const artifact = await waitForArtifact();
157
158
// Download the artifact (a zip containing the VSIX)
159
const tmpDir = path.resolve('.build/tmp-copilot');
160
fs.mkdirSync(tmpDir, { recursive: true });
161
const artifactZipPath = path.join(tmpDir, 'artifact.zip');
162
163
console.log('Downloading Copilot VSIX artifact...');
164
await retry(() => downloadArtifact(artifact, artifactZipPath));
165
166
// Extract the artifact zip to get the VSIX file
167
console.log('Extracting artifact zip...');
168
const artifactFiles = await unzip(artifactZipPath, tmpDir);
169
const vsixFile = artifactFiles.find(f => f.endsWith('.vsix'));
170
171
if (!vsixFile) {
172
throw new Error('No .vsix file found in the Copilot artifact');
173
}
174
175
console.log(`Found VSIX: ${vsixFile}`);
176
177
// Extract the VSIX (which is also a zip) to the output directory
178
// VSIX files contain an 'extension/' folder with the actual extension files
179
console.log(`Extracting VSIX to ${outputDir}...`);
180
fs.rmSync(outputDir, { recursive: true, force: true });
181
fs.mkdirSync(outputDir, { recursive: true });
182
183
const vsixTmpDir = path.join(tmpDir, 'vsix-contents');
184
fs.mkdirSync(vsixTmpDir, { recursive: true });
185
186
await unzip(vsixFile, vsixTmpDir);
187
188
// Move extension/ contents to the output directory
189
const extensionDir = path.join(vsixTmpDir, 'extension');
190
if (!fs.existsSync(extensionDir)) {
191
throw new Error('VSIX does not contain an extension/ directory');
192
}
193
194
// Copy all files from extension/ to outputDir
195
copyDirSync(extensionDir, outputDir);
196
197
// Cleanup
198
fs.rmSync(tmpDir, { recursive: true, force: true });
199
200
console.log('Copilot VSIX successfully extracted to .build/extensions/copilot/');
201
}
202
203
function copyDirSync(src: string, dest: string): void {
204
fs.mkdirSync(dest, { recursive: true });
205
const entries = fs.readdirSync(src, { withFileTypes: true });
206
for (const entry of entries) {
207
const srcPath = path.join(src, entry.name);
208
const destPath = path.join(dest, entry.name);
209
if (entry.isDirectory()) {
210
copyDirSync(srcPath, destPath);
211
} else {
212
fs.copyFileSync(srcPath, destPath);
213
}
214
}
215
}
216
217
main().then(() => {
218
process.exit(0);
219
}, err => {
220
console.error(err);
221
process.exit(1);
222
});
223
224