diff --git a/package.json b/package.json index 75eaf159e8..9dcacfc11a 100644 --- a/package.json +++ b/package.json @@ -857,6 +857,21 @@ "default": true, "markdownDescription": "Specifies whether you may be prompted to update your version of PowerShell." }, + "powershell.updateChannel": { + "type": "string", + "default": "latest", + "enum": [ + "latest", + "preview", + "lts" + ], + "enumDescriptions": [ + "Checks for stable releases.", + "Checks for preview releases.", + "Checks for long-term servicing releases." + ], + "markdownDescription": "Specifies which PowerShell release channel to check for updates." + }, "powershell.promptToUpdatePackageManagement": { "type": "boolean", "default": false, diff --git a/src/features/UpdatePowerShell.ts b/src/features/UpdatePowerShell.ts index 99532e1d13..31c9f48039 100644 --- a/src/features/UpdatePowerShell.ts +++ b/src/features/UpdatePowerShell.ts @@ -12,14 +12,20 @@ interface IUpdateMessageItem extends vscode.MessageItem { id: number; } +type UpdateChannel = "latest" | "preview" | "lts"; + +interface IPowerShellReleaseMetadata { + StableReleaseTag?: string; + PreviewReleaseTag?: string; + LTSReleaseTag?: string; +} + // This attempts to mirror PowerShell's `UpdatesNotification.cs` logic as much as // possibly, documented at: // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_update_notifications export class UpdatePowerShell { - private static LTSBuildInfoURL = "https://aka.ms/pwsh-buildinfo-lts"; - private static StableBuildInfoURL = "https://aka.ms/pwsh-buildinfo-stable"; - private static PreviewBuildInfoURL = - "https://aka.ms/pwsh-buildinfo-preview"; + private static ReleaseMetadataURL = + "https://raw.githubusercontent.com/PowerShell/PowerShell/refs/heads/master/tools/metadata.json"; private static GitHubWebReleaseURL = "https://github.com/PowerShell/PowerShell/releases/tag/"; private static promptOptions: IUpdateMessageItem[] = [ @@ -106,22 +112,22 @@ export class UpdatePowerShell { return true; } - private async getRemoteVersion(url: string): Promise { - const response = await fetch(url); + private async getRemoteVersion( + channel: UpdateChannel, + ): Promise { + const response = await fetch(UpdatePowerShell.ReleaseMetadataURL); if (!response.ok) { return undefined; } - // Looks like: - // { - // "ReleaseDate": "2022-10-20T22:01:38Z", - // "BlobName": "v7-2-7", - // "ReleaseTag": "v7.2.7" - // } - const data = await response.json(); + const data = (await response.json()) as IPowerShellReleaseMetadata; this.logger.writeDebug( - `Received from '${url}':\n${JSON.stringify(data, undefined, 2)}`, + `Received from '${UpdatePowerShell.ReleaseMetadataURL}':\n${JSON.stringify(data, undefined, 2)}`, ); - return data.ReleaseTag; + return { + latest: data.StableReleaseTag, + preview: data.PreviewReleaseTag, + lts: data.LTSReleaseTag, + }[channel]; } private async maybeGetNewRelease(): Promise { @@ -130,42 +136,18 @@ export class UpdatePowerShell { } this.logger.writeDebug("Checking for PowerShell update..."); - const tags: string[] = []; - if (process.env.POWERSHELL_UPDATECHECK?.toLowerCase() === "lts") { - // Only check for update to LTS. - this.logger.writeDebug("Checking for LTS update..."); - const tag = await this.getRemoteVersion( - UpdatePowerShell.LTSBuildInfoURL, - ); - if (tag != undefined) { - tags.push(tag); - } - } else { - // Check for update to stable. - this.logger.writeDebug("Checking for stable update..."); - const tag = await this.getRemoteVersion( - UpdatePowerShell.StableBuildInfoURL, - ); - if (tag != undefined) { - tags.push(tag); - } - - // Also check for a preview update. - if (this.localVersion.prerelease.length > 0) { - this.logger.writeDebug("Checking for preview update..."); - const tag = await this.getRemoteVersion( - UpdatePowerShell.PreviewBuildInfoURL, - ); - if (tag != undefined) { - tags.push(tag); - } - } - } - - for (const tag of tags) { - if (this.localVersion.compare(tag) === -1) { - return tag; - } + const updateChannel = + process.env.POWERSHELL_UPDATECHECK?.toLowerCase() === "lts" + ? "lts" + : vscode.workspace + .getConfiguration("powershell") + .get("updateChannel", "latest"); + this.logger.writeDebug( + `Checking for ${updateChannel} PowerShell update...`, + ); + const tag = await this.getRemoteVersion(updateChannel); + if (tag && this.localVersion.compare(tag) === -1) { + return tag; } this.logger.write("PowerShell is up-to-date."); diff --git a/test/features/UpdatePowerShell.test.ts b/test/features/UpdatePowerShell.test.ts index df146b3081..b3557e2cc9 100644 --- a/test/features/UpdatePowerShell.test.ts +++ b/test/features/UpdatePowerShell.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import assert from "assert"; +import Sinon from "sinon"; import * as vscode from "vscode"; import { UpdatePowerShell } from "../../src/features/UpdatePowerShell"; import type { IPowerShellVersionDetails } from "../../src/session"; @@ -114,36 +115,53 @@ describe("UpdatePowerShell feature", function () { }); describe("Which version it gets", function () { - it("Would update to LTS", async function () { - process.env.POWERSHELL_UPDATECHECK = "LTS"; - const version: IPowerShellVersionDetails = { - version: "7.2.0", - edition: "Core", - commit: "7.2.0", - architecture: "X64", - }; - const updater = new UpdatePowerShell(testLogger, version); - // @ts-expect-error method is private. - const tag: string = (await updater.maybeGetNewRelease()) ?? ""; - // NOTE: This will need to be updated each time an LTS is released. - // Also sometimes the prior LTS is more recently updated than the latest LTS. - assert(tag.startsWith("v7.4")); - }); - - it("Would update to stable", async function () { - const version: IPowerShellVersionDetails = { - version: "7.3.0", - edition: "Core", - commit: "7.3.0", - architecture: "X64", - }; - const updater = new UpdatePowerShell(testLogger, version); - // @ts-expect-error method is private. - const tag: string | undefined = await updater.maybeGetNewRelease(); - // NOTE: This will need to be updated each new major stable. - // TODO: Upstream bug causes LTS releases to update the stable info. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - assert(tag?.startsWith("v7.6") || tag?.startsWith("v7.4")); - }); + for (const [channel, tag] of [ + ["latest", "v7.8.0"], + ["preview", "v7.9.0-preview.1"], + ["lts", "v7.4.0"], + ] as const) { + it(`Would update to ${channel}`, async function () { + await changeSetting( + "updateChannel", + channel, + vscode.ConfigurationTarget.Workspace, + undefined, + ); + const fetchStub = Sinon.stub(globalThis, "fetch").resolves({ + ok: true, + json: async () => ({ + StableReleaseTag: "v7.8.0", + PreviewReleaseTag: "v7.9.0-preview.1", + LTSReleaseTag: "v7.4.0", + }), + } as Response); + try { + const version: IPowerShellVersionDetails = { + version: "7.3.0", + edition: "Core", + commit: "7.3.0", + architecture: "X64", + }; + const updater = new UpdatePowerShell(testLogger, version); + // @ts-expect-error method is private. + const foundTag: string | undefined = + await updater.maybeGetNewRelease(); + assert.strictEqual(foundTag, tag); + assert( + fetchStub.calledOnceWith( + "https://raw.githubusercontent.com/PowerShell/PowerShell/refs/heads/master/tools/metadata.json", + ), + ); + } finally { + fetchStub.restore(); + await changeSetting( + "updateChannel", + undefined, + vscode.ConfigurationTarget.Workspace, + undefined, + ); + } + }); + } }); });