-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathfetch-schemas.mjs
More file actions
127 lines (114 loc) · 4.52 KB
/
Copy pathfetch-schemas.mjs
File metadata and controls
127 lines (114 loc) · 4.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/usr/bin/env node
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '../../..');
const packagePath = path.join(repoRoot, 'nodejs', 'package.json');
const outputDir = path.resolve(
process.env.COPILOT_CLI_SCHEMA_OUTPUT ?? path.join(scriptDir, 'target', 'schemas'),
);
// Schemas are platform-independent; use one asset consistently on every codegen host.
const platform = process.env.COPILOT_CLI_SCHEMA_PLATFORM ?? 'linux-x64';
const version =
process.env.COPILOT_CLI_VERSION ??
JSON.parse(fs.readFileSync(packagePath, 'utf8')).copilotCliVersion;
if (!version) {
throw new Error(`Could not find copilotCliVersion in ${packagePath}`);
}
const assetName = `github-copilot-${version}-${platform}.tgz`;
const releaseBase = (
process.env.COPILOT_CLI_DOWNLOAD_BASE_URL ??
'https://github.com/github/copilot-cli/releases/download'
).replace(/\/+$/, '');
let archive;
let expectedHash;
if (process.env.COPILOT_CLI_RELEASE_TARBALL) {
archive = fs.readFileSync(process.env.COPILOT_CLI_RELEASE_TARBALL);
expectedHash = process.env.COPILOT_CLI_RELEASE_SHA256;
} else {
const releaseUrl = `${releaseBase}/v${version}`;
const checksums = (await download(`${releaseUrl}/SHA256SUMS.txt`)).toString('utf8');
expectedHash = findChecksum(checksums, assetName);
archive = await download(`${releaseUrl}/${assetName}`);
}
if (!expectedHash || !/^[a-fA-F0-9]{64}$/.test(expectedHash)) {
throw new Error(`Missing or invalid SHA-256 for ${assetName}`);
}
const actualHash = createHash('sha256').update(archive).digest('hex');
if (actualHash !== expectedHash.toLowerCase()) {
throw new Error(
`Integrity verification failed for ${assetName}: expected ${expectedHash}, got ${actualHash}`,
);
}
const schemaNames = ['api.schema.json', 'session-events.schema.json'];
const members = execFileSync('tar', ['-tzf', '-'], {
encoding: 'utf8',
input: archive,
maxBuffer: 512 * 1024 * 1024,
})
.split(/\r?\n/)
.filter(Boolean);
const outputParent = path.dirname(outputDir);
fs.mkdirSync(outputParent, { recursive: true });
const stagingDir = fs.mkdtempSync(path.join(outputParent, '.schemas-'));
try {
for (const schemaName of schemaNames) {
const member = `package/schemas/${schemaName}`;
if (members.filter((candidate) => candidate === member).length !== 1) {
throw new Error(`${assetName} must contain exactly one ${member}`);
}
const contents = execFileSync('tar', ['-xOzf', '-', member], {
encoding: null,
input: archive,
maxBuffer: 512 * 1024 * 1024,
});
JSON.parse(contents.toString('utf8'));
fs.writeFileSync(path.join(stagingDir, schemaName), contents);
}
fs.rmSync(outputDir, { recursive: true, force: true });
fs.renameSync(stagingDir, outputDir);
} finally {
fs.rmSync(stagingDir, { recursive: true, force: true });
}
console.log(`Staged Copilot CLI ${version} schemas at ${outputDir}`);
async function download(url) {
let lastError;
for (let attempt = 0; attempt < 3; attempt++) {
try {
// lgtm[js/file-access-to-http] The repository-pinned CLI version selects the release asset.
const response = await fetch(url, { signal: AbortSignal.timeout(600_000) });
if (response.ok) {
return Buffer.from(await response.arrayBuffer());
}
await response.body?.cancel();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429) {
break;
}
} catch (error) {
lastError = error;
}
if (attempt < 2) {
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
}
}
throw new Error(`Failed to download ${url}: ${lastError}`);
}
function findChecksum(checksums, expectedAssetName) {
for (const line of checksums.split(/\r?\n/)) {
const [hash, name] = line.trim().split(/\s+/, 2);
if (
name?.replace(/^\*/, '') === expectedAssetName &&
/^[a-fA-F0-9]{64}$/.test(hash)
) {
return hash.toLowerCase();
}
}
throw new Error(`SHA256SUMS.txt does not contain ${expectedAssetName}`);
}