Skip to content

Commit

Permalink
Merge pull request #270 from microsoft/connor4312/unreleased-builds
Browse files Browse the repository at this point in the history
feat: allow downloading unreleased versions via -unreleased suffix
  • Loading branch information
connor4312 committed May 24, 2024
2 parents 884a3c8 + 021f593 commit 2d17e1e
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 64 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog

### 2.3.10 | 2024-01-19
### 2.4.0 | 2024-05-24

- Allow installing unreleased builds using an `-unreleased` suffix, such as `insiders-unreleased`.

### 2.3.10 | 2024-05-13

- Add `runVSCodeCommand` method and workaround for Node CVE-2024-27980

Expand Down
2 changes: 1 addition & 1 deletion lib/download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('fetchTargetInferredVersion', () => {
let extensionsDevelopmentPath = join(tmpdir(), 'vscode-test-tmp-workspace');

beforeAll(async () => {
[stable, insiders] = await Promise.all([fetchStableVersions(5000), fetchInsiderVersions(5000)]);
[stable, insiders] = await Promise.all([fetchStableVersions(true, 5000), fetchInsiderVersions(true, 5000)]);
});

afterEach(async () => {
Expand Down
88 changes: 43 additions & 45 deletions lib/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ import {
insidersDownloadDirMetadata,
insidersDownloadDirToExecutablePath,
isDefined,
isInsiderVersionIdentifier,
isStableVersionIdentifier,
isSubdirectory,
onceWithoutRejections,
streamToBuffer,
systemDefaultPlatform,
validateStream,
Version,
} from './util';

const extensionRoot = process.cwd();
Expand All @@ -36,7 +35,7 @@ const vscodeStableReleasesAPI = `https://update.code.visualstudio.com/api/releas
const vscodeInsiderReleasesAPI = `https://update.code.visualstudio.com/api/releases/insider`;

const downloadDirNameFormat = /^vscode-(?<platform>[a-z]+)-(?<version>[0-9.]+)$/;
const makeDownloadDirName = (platform: string, version: string) => `vscode-${platform}-${version}`;
const makeDownloadDirName = (platform: string, version: Version) => `vscode-${platform}-${version.id}`;

const DOWNLOAD_ATTEMPTS = 3;

Expand All @@ -50,28 +49,28 @@ interface IFetchInferredOptions extends IFetchStableOptions {
extensionsDevelopmentPath?: string | string[];
}

export const fetchStableVersions = onceWithoutRejections((timeout: number) =>
request.getJSON<string[]>(vscodeStableReleasesAPI, timeout)
export const fetchStableVersions = onceWithoutRejections((released: boolean, timeout: number) =>
request.getJSON<string[]>(`${vscodeStableReleasesAPI}?released=${released}`, timeout)
);
export const fetchInsiderVersions = onceWithoutRejections((timeout: number) =>
request.getJSON<string[]>(vscodeInsiderReleasesAPI, timeout)
export const fetchInsiderVersions = onceWithoutRejections((released: boolean, timeout: number) =>
request.getJSON<string[]>(`${vscodeInsiderReleasesAPI}?released=${released}`, timeout)
);

/**
* Returns the stable version to run tests against. Attempts to get the latest
* version from the update sverice, but falls back to local installs if
* not available (e.g. if the machine is offline).
*/
async function fetchTargetStableVersion({ timeout, cachePath, platform }: IFetchStableOptions): Promise<string> {
async function fetchTargetStableVersion({ timeout, cachePath, platform }: IFetchStableOptions): Promise<Version> {
try {
const versions = await fetchStableVersions(timeout);
return versions[0];
const versions = await fetchStableVersions(true, timeout);
return new Version(versions[0]);
} catch (e) {
return fallbackToLocalEntries(cachePath, platform, e as Error);
}
}

export async function fetchTargetInferredVersion(options: IFetchInferredOptions) {
export async function fetchTargetInferredVersion(options: IFetchInferredOptions): Promise<Version> {
if (!options.extensionsDevelopmentPath) {
return fetchTargetStableVersion(options);
}
Expand All @@ -87,22 +86,22 @@ export async function fetchTargetInferredVersion(options: IFetchInferredOptions)
const matches = (v: string) => !extVersions.some((range) => !semver.satisfies(v, range, { includePrerelease: true }));

try {
const stable = await fetchStableVersions(options.timeout);
const stable = await fetchStableVersions(true, options.timeout);
const found1 = stable.find(matches);
if (found1) {
return found1;
return new Version(found1);
}

const insiders = await fetchInsiderVersions(options.timeout);
const insiders = await fetchInsiderVersions(true, options.timeout);
const found2 = insiders.find(matches);
if (found2) {
return found2;
return new Version(found2);
}

const v = extVersions.join(', ');
console.warn(`No version of VS Code satisfies all extension engine constraints (${v}). Falling back to stable.`);

return stable[0]; // 🤷
return new Version(stable[0]); // 🤷
} catch (e) {
return fallbackToLocalEntries(options.cachePath, options.platform, e as Error);
}
Expand All @@ -129,35 +128,31 @@ async function fallbackToLocalEntries(cachePath: string, platform: string, fromE

if (fallbackTo) {
console.warn(`Error retrieving VS Code versions, using already-installed version ${fallbackTo}`, fromError);
return fallbackTo;
return new Version(fallbackTo);
}

throw fromError;
}

async function isValidVersion(version: string, platform: string, timeout: number) {
if (version === 'insiders' || version === 'stable') {
async function isValidVersion(version: Version, timeout: number) {
if (version.id === 'insiders' || version.id === 'stable' || version.isCommit) {
return true;
}

if (isStableVersionIdentifier(version)) {
const stableVersionNumbers = await fetchStableVersions(timeout);
if (stableVersionNumbers.includes(version)) {
if (version.isStable) {
const stableVersionNumbers = await fetchStableVersions(version.isReleased, timeout);
if (stableVersionNumbers.includes(version.id)) {
return true;
}
}

if (isInsiderVersionIdentifier(version)) {
const insiderVersionNumbers = await fetchInsiderVersions(timeout);
if (insiderVersionNumbers.includes(version)) {
if (version.isInsiders) {
const insiderVersionNumbers = await fetchInsiderVersions(version.isReleased, timeout);
if (insiderVersionNumbers.includes(version.id)) {
return true;
}
}

if (/^[0-9a-f]{40}$/.test(version)) {
return true;
}

return false;
}

Expand Down Expand Up @@ -267,7 +262,8 @@ async function downloadVSCodeArchive(options: DownloadOptions): Promise<IDownloa
}

const timeout = options.timeout!;
const downloadUrl = getVSCodeDownloadUrl(options.version, options.platform);
const version = Version.parse(options.version);
const downloadUrl = getVSCodeDownloadUrl(version, options.platform);

options.reporter?.report({ stage: ProgressReportStage.ResolvingCDNLocation, url: downloadUrl });
const res = await request.getStream(downloadUrl, timeout);
Expand Down Expand Up @@ -429,25 +425,27 @@ const COMPLETE_FILE_NAME = 'is-complete';
* @returns Promise of `vscodeExecutablePath`.
*/
export async function download(options: Partial<DownloadOptions> = {}): Promise<string> {
let version = options?.version;
const inputVersion = options?.version ? Version.parse(options.version) : undefined;
const {
platform = systemDefaultPlatform,
cachePath = defaultCachePath,
reporter = new ConsoleReporter(process.stdout.isTTY),
timeout = 15_000,
} = options;

if (version === 'stable') {
let version: Version;
if (inputVersion?.id === 'stable') {
version = await fetchTargetStableVersion({ timeout, cachePath, platform });
} else if (version) {
} else if (inputVersion) {
/**
* Only validate version against server when no local download that matches version exists
*/
if (!fs.existsSync(path.resolve(cachePath, `vscode-${platform}-${version}`))) {
if (!(await isValidVersion(version, platform, timeout))) {
throw Error(`Invalid version ${version}`);
if (!fs.existsSync(path.resolve(cachePath, makeDownloadDirName(platform, inputVersion)))) {
if (!(await isValidVersion(inputVersion, timeout))) {
throw Error(`Invalid version ${inputVersion.id}`);
}
}
version = inputVersion;
} else {
version = await fetchTargetInferredVersion({
timeout,
Expand All @@ -457,22 +455,22 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
});
}

if (platform === 'win32-archive' && semver.satisfies(version, '>= 1.85.0', { includePrerelease: true })) {
if (platform === 'win32-archive' && semver.satisfies(version.id, '>= 1.85.0', { includePrerelease: true })) {
throw new Error('Windows 32-bit is no longer supported from v1.85 onwards');
}

reporter.report({ stage: ProgressReportStage.ResolvedVersion, version });
reporter.report({ stage: ProgressReportStage.ResolvedVersion, version: version.id });

const downloadedPath = path.resolve(cachePath, makeDownloadDirName(platform, version));
if (fs.existsSync(path.join(downloadedPath, COMPLETE_FILE_NAME))) {
if (isInsiderVersionIdentifier(version)) {
if (version.isInsiders) {
reporter.report({ stage: ProgressReportStage.FetchingInsidersMetadata });
const { version: currentHash, date: currentDate } = insidersDownloadDirMetadata(downloadedPath, platform);

const { version: latestHash, timestamp: latestTimestamp } =
version === 'insiders'
? await getLatestInsidersMetadata(systemDefaultPlatform)
: await getInsidersVersionMetadata(systemDefaultPlatform, version);
version.id === 'insiders' // not qualified with a date
? await getLatestInsidersMetadata(systemDefaultPlatform, version.isReleased)
: await getInsidersVersionMetadata(systemDefaultPlatform, version.id, version.isReleased);

if (currentHash === latestHash) {
reporter.report({ stage: ProgressReportStage.FoundMatchingInstall, downloadedPath });
Expand All @@ -493,7 +491,7 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
throw Error(`Failed to remove outdated Insiders at ${downloadedPath}.`);
}
}
} else if (isStableVersionIdentifier(version)) {
} else if (version.isStable) {
reporter.report({ stage: ProgressReportStage.FoundMatchingInstall, downloadedPath });
return Promise.resolve(downloadDirToExecutablePath(downloadedPath, platform));
} else {
Expand All @@ -507,7 +505,7 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
await fs.promises.rm(downloadedPath, { recursive: true, force: true });

const download = await downloadVSCodeArchive({
version,
version: version.toString(),
platform,
cachePath,
reporter,
Expand Down Expand Up @@ -536,7 +534,7 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
}
reporter.report({ stage: ProgressReportStage.NewInstallComplete, downloadedPath });

if (isStableVersionIdentifier(version)) {
if (version.isStable) {
return downloadDirToExecutablePath(downloadedPath, platform);
} else {
return insidersDownloadDirToExecutablePath(downloadedPath, platform);
Expand Down
57 changes: 40 additions & 17 deletions lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,47 @@ switch (process.platform) {
process.arch === 'arm64' ? 'linux-arm64' : process.arch === 'arm' ? 'linux-armhf' : 'linux-x64';
}

export function isInsiderVersionIdentifier(version: string): boolean {
return version === 'insiders' || version.endsWith('-insider'); // insider or 1.2.3-insider version string
}
const UNRELEASED_SUFFIX = '-unreleased';

export class Version {
public static parse(version: string): Version {
const unreleased = version.endsWith(UNRELEASED_SUFFIX);
if (unreleased) {
version = version.slice(0, -UNRELEASED_SUFFIX.length);
}

export function isStableVersionIdentifier(version: string): boolean {
return version === 'stable' || /^[0-9]+\.[0-9]+\.[0-9]$/.test(version); // stable or 1.2.3 version string
return new Version(version, !unreleased);
}

constructor(public readonly id: string, public readonly isReleased = true) {}

public get isCommit() {
return /^[0-9a-f]{40}$/.test(this.id);
}

public get isInsiders() {
return this.id === 'insiders' || this.id.endsWith('-insider');
}

public get isStable() {
return this.id === 'stable' || /^[0-9]+\.[0-9]+\.[0-9]$/.test(this.id);
}

public toString() {
return this.id + (this.isReleased ? '' : UNRELEASED_SUFFIX);
}
}

export function getVSCodeDownloadUrl(version: string, platform = systemDefaultPlatform) {
if (version === 'insiders') {
return `https://update.code.visualstudio.com/latest/${platform}/insider`;
} else if (isInsiderVersionIdentifier(version)) {
return `https://update.code.visualstudio.com/${version}/${platform}/insider`;
} else if (isStableVersionIdentifier(version)) {
return `https://update.code.visualstudio.com/${version}/${platform}/stable`;
export function getVSCodeDownloadUrl(version: Version, platform: string) {
if (version.id === 'insiders') {
return `https://update.code.visualstudio.com/latest/${platform}/insider?released=${version.isReleased}`;
} else if (version.isInsiders) {
return `https://update.code.visualstudio.com/${version.id}/${platform}/insider?released=${version.isReleased}`;
} else if (version.isStable) {
return `https://update.code.visualstudio.com/${version.id}/${platform}/stable?released=${version.isReleased}`;
} else {
// insiders commit hash
return `https://update.code.visualstudio.com/commit:${version}/${platform}/insider`;
return `https://update.code.visualstudio.com/commit:${version.id}/${platform}/insider`;
}
}

Expand Down Expand Up @@ -126,13 +149,13 @@ export interface IUpdateMetadata {
supportsFastUpdate: boolean;
}

export async function getInsidersVersionMetadata(platform: string, version: string) {
const remoteUrl = `https://update.code.visualstudio.com/api/versions/${version}/${platform}/insider`;
export async function getInsidersVersionMetadata(platform: string, version: string, released: boolean) {
const remoteUrl = `https://update.code.visualstudio.com/api/versions/${version}/${platform}/insider?released=${released}`;
return await request.getJSON<IUpdateMetadata>(remoteUrl, 30_000);
}

export async function getLatestInsidersMetadata(platform: string) {
const remoteUrl = `https://update.code.visualstudio.com/api/update/${platform}/insider/latest`;
export async function getLatestInsidersMetadata(platform: string, released: boolean) {
const remoteUrl = `https://update.code.visualstudio.com/api/update/${platform}/insider/latest?released=${released}`;
return await request.getJSON<IUpdateMetadata>(remoteUrl, 30_000);
}

Expand Down
10 changes: 10 additions & 0 deletions sample/src/test/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ async function go() {
launchArgs: [testWorkspace],
});

/**
* Use unreleased Insiders (here be dragons!)
*/
await runTests({
version: 'insiders-unreleased',
extensionDevelopmentPath,
extensionTestsPath,
launchArgs: [testWorkspace],
});

/**
* Use a specific Insiders commit for testing
*/
Expand Down

0 comments on commit 2d17e1e

Please sign in to comment.