#!/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'; 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(/\/+$/, ''); 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 = `${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 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}`); }