diff --git a/docs/development.md b/docs/development.md index 98077ffc..fb549b23 100644 --- a/docs/development.md +++ b/docs/development.md @@ -66,6 +66,20 @@ Generate a `.vsix` package for distribution: npm run package ``` +## Update the Topo CLI Version + +To update the Topo CLI version bundled with the extension, pass the new version +as a positional argument: + +```bash +npm run bump-topo -- 9.1.0 +``` + +The `--` forwards the version argument from npm to the script. The command +checks that the version has artifacts for every supported platform, then updates +the Topo version and platform checksums in `package.json`. If any artifact is +missing, `package.json` is left unchanged. + ## Access the `topo` Binary When the extension is loaded, the `topo` binary path is added to `PATH` and can be accessed directly from the VS Code terminal: diff --git a/package.json b/package.json index 4cd590b2..c9245d13 100644 --- a/package.json +++ b/package.json @@ -478,6 +478,7 @@ "build": "vite build --mode extension", "watch": "vite build --mode extension --watch --minify=false", "download": "node --no-warnings --experimental-strip-types scripts/download.ts", + "bump-topo": "node --no-warnings --experimental-strip-types scripts/bump-topo.ts", "calculate-new-version": "node --no-warnings --experimental-strip-types scripts/calculate-new-version.ts", "prettier-check": "prettier -c .", "eslint-check": "eslint .", diff --git a/scripts/bump-topo.ts b/scripts/bump-topo.ts new file mode 100644 index 00000000..c349aeb8 --- /dev/null +++ b/scripts/bump-topo.ts @@ -0,0 +1,99 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import yargs from 'yargs'; +import * as manifest from '../src/manifest.ts'; +import { + DOWNLOAD_TARGETS, + type DownloadTarget, + getArtifactUrl, +} from './topoArtifacts.ts'; + +const VERSION_PATTERN = /^\d+\.\d+\.\d+$/; + +const getChecksum = async ( + version: string, + target: DownloadTarget, +): Promise<[DownloadTarget, string]> => { + const url = getArtifactUrl(version, target); + const response = await fetch(url, { method: 'HEAD' }); + if (!response.ok) { + throw new Error( + `Topo ${version} does not exist for ${target}: ${response.status} ${response.statusText}`, + ); + } + + const checksum = response.headers.get('x-checksum-sha256'); + if (checksum === null) { + throw new Error(`Artifactory returned no SHA-256 for ${target}`); + } + + return [target, checksum.toLowerCase()]; +}; + +const main = async (): Promise => { + const parsedArgs = yargs(process.argv.slice(2)) + .usage('$0 ') + .command( + '$0 ', + 'Updates the bundled Topo CLI version and checksums', + (command) => + command.positional('version', { + description: 'Topo CLI version in major.minor.patch format', + type: 'string', + demandOption: true, + }), + ) + .check((args) => { + if ( + typeof args.version !== 'string' || + !VERSION_PATTERN.test(args.version) + ) { + throw new Error('Version must use major.minor.patch format'); + } + return true; + }) + .help() + .alias('h', 'help') + .version(false) + .strict() + .parseSync(); + const version = parsedArgs.version as string; + + const pkgPath = path.resolve(process.cwd(), 'package.json'); + if (!fs.existsSync(pkgPath)) { + throw new Error(`Couldn't find package.json at ${pkgPath}`); + } + + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as Record< + string, + unknown + >; + const section = pkg[manifest.TOPO_CLI]; + if ( + typeof section !== 'object' || + section === null || + Array.isArray(section) + ) { + throw new Error( + `package.json must have a top-level "${manifest.TOPO_CLI}" object`, + ); + } + + console.log(`→ Checking Topo ${version}`); + const checksumEntries: [DownloadTarget, string][] = []; + for (const target of DOWNLOAD_TARGETS) { + checksumEntries.push(await getChecksum(version, target)); + } + + const topo = section as Record; + topo.version = version; + topo.sha256 = Object.fromEntries(checksumEntries); + fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 4)}\n`); + console.log(`✓ Updated Topo to ${version} in package.json`); +}; + +void main().catch((err: unknown) => { + const errorMsg = err instanceof Error ? err.message : err; + console.error('✖ Error bumping Topo:', errorMsg); + process.exit(1); +}); diff --git a/scripts/download.ts b/scripts/download.ts index 7eda3c66..e6aa25a0 100755 --- a/scripts/download.ts +++ b/scripts/download.ts @@ -6,21 +6,12 @@ import yargs from 'yargs'; import extract from 'extract-zip'; import * as tar from 'tar'; import * as manifest from '../src/manifest.ts'; - -const DOWNLOAD_TARGETS = [ - 'win32-x64', - 'win32-arm64', - 'linux-x64', - 'linux-arm64', - 'darwin-x64', - 'darwin-arm64', -] as const; -type DownloadTarget = (typeof DOWNLOAD_TARGETS)[number]; - -const SHA256_PATTERN = /^[a-f0-9]{64}$/i; - -const isSha256 = (value: unknown): value is string => - typeof value === 'string' && SHA256_PATTERN.test(value); +import { + DOWNLOAD_TARGETS, + type DownloadTarget, + getArtifactUrl, + isSha256, +} from './topoArtifacts.ts'; const readFromUrl = async (url: string): Promise => { const response = await fetch(url); @@ -178,22 +169,12 @@ if (!isSha256(expectedSha256)) { } // --- Determine asset name --------------------------------------------------- -const assetMapping: Record = { - 'linux-x64': 'linux/topo_linux_amd64.tar.gz', - 'linux-arm64': 'linux/topo_linux_arm64.tar.gz', - 'darwin-x64': 'macos/topo_darwin_amd64.tar.gz', - 'darwin-arm64': 'macos/topo_darwin_arm64.tar.gz', - 'win32-x64': 'windows/topo_windows_amd64.zip', - 'win32-arm64': 'windows/topo_windows_arm64.zip', -}; const isWin = target.startsWith('win32'); const topoFilename = isWin ? `${manifest.TOPO_CLI_WINDOWS}` : manifest.TOPO_CLI; const destFilename = `resources/${topoFilename}`; -const assetPath = assetMapping[target]; // --- Compose download URL --------------------------------------------------- -const tag = `v${version}`; -const downloadUrl = `https://artifacts.tools.arm.com/topo/${tag}/${assetPath}`; +const downloadUrl = getArtifactUrl(version, target); console.log(`→ Downloading ${downloadUrl}`); // --- Perform download -------------------------------------------------------- diff --git a/scripts/topoArtifacts.ts b/scripts/topoArtifacts.ts new file mode 100644 index 00000000..b4e153ef --- /dev/null +++ b/scripts/topoArtifacts.ts @@ -0,0 +1,31 @@ +export const DOWNLOAD_TARGETS = [ + 'win32-x64', + 'win32-arm64', + 'linux-x64', + 'linux-arm64', + 'darwin-x64', + 'darwin-arm64', +] as const; + +export type DownloadTarget = (typeof DOWNLOAD_TARGETS)[number]; + +const ARTIFACT_BASE_URL = 'https://artifacts.tools.arm.com/topo'; + +const ASSET_MAPPING: Record = { + 'linux-x64': 'linux/topo_linux_amd64.tar.gz', + 'linux-arm64': 'linux/topo_linux_arm64.tar.gz', + 'darwin-x64': 'macos/topo_darwin_amd64.tar.gz', + 'darwin-arm64': 'macos/topo_darwin_arm64.tar.gz', + 'win32-x64': 'windows/topo_windows_amd64.zip', + 'win32-arm64': 'windows/topo_windows_arm64.zip', +}; + +export const getArtifactUrl = ( + version: string, + target: DownloadTarget, +): string => `${ARTIFACT_BASE_URL}/v${version}/${ASSET_MAPPING[target]}`; + +const SHA256_PATTERN = /^[a-f0-9]{64}$/i; + +export const isSha256 = (value: unknown): value is string => + typeof value === 'string' && SHA256_PATTERN.test(value);