-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathfetch-schemas.mjs
More file actions
167 lines (151 loc) · 5.9 KB
/
Copy pathfetch-schemas.mjs
File metadata and controls
167 lines (151 loc) · 5.9 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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/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';
import { findRuntimeRoot } from '../../../scripts/runtime-layout.mjs';
import { runtimeReleaseUrl } from '../../../scripts/runtime-release.mjs';
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 schemaNames = ['api.schema.json', 'session-events.schema.json'];
const schemaContents = new Map();
const runtimeRoot = findRuntimeRoot(repoRoot);
const runtimeSource =
process.env.COPILOT_RUNTIME_SOURCE ?? (runtimeRoot ? 'checkout' : undefined);
const explicitSchemaDirectory =
process.env.COPILOT_CLI_SCHEMA_DIR ??
(runtimeSource === 'checkout' && runtimeRoot
? path.join(runtimeRoot, 'generated')
: undefined);
if (explicitSchemaDirectory) {
for (const schemaName of schemaNames) {
const schemaPath = path.resolve(explicitSchemaDirectory, schemaName);
let contents;
try {
contents = fs.readFileSync(schemaPath);
} catch (error) {
throw new Error(`Selected Copilot schema not found at ${schemaPath}`, { cause: error });
}
try {
JSON.parse(contents.toString('utf8'));
} catch (error) {
throw new Error(`Invalid runtime schema at ${schemaPath}`, { cause: error });
}
schemaContents.set(schemaName, contents);
}
} else {
if (runtimeSource === 'checkout') {
throw new Error(
'COPILOT_CLI_SCHEMA_DIR is required when COPILOT_RUNTIME_SOURCE=checkout; generate the runtime schemas and select their directory explicitly.',
);
}
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 = runtimeReleaseUrl(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 members = execFileSync('tar', ['-tzf', '-'], {
encoding: 'utf8',
input: archive,
maxBuffer: 512 * 1024 * 1024,
})
.split(/\r?\n/)
.filter(Boolean);
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'));
schemaContents.set(schemaName, contents);
}
}
const outputParent = path.dirname(outputDir);
fs.mkdirSync(outputParent, { recursive: true });
const stagingDir = fs.mkdtempSync(path.join(outputParent, '.schemas-'));
try {
for (const schemaName of schemaNames) {
fs.writeFileSync(path.join(stagingDir, schemaName), schemaContents.get(schemaName));
}
fs.rmSync(outputDir, { recursive: true, force: true });
fs.renameSync(stagingDir, outputDir);
} finally {
fs.rmSync(stagingDir, { recursive: true, force: true });
}
console.log(
explicitSchemaDirectory
? `Staged checked-out runtime schemas at ${outputDir}`
: `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}`);
}