Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
84 changes: 33 additions & 51 deletions src/features/UpdatePowerShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -106,22 +112,22 @@ export class UpdatePowerShell {
return true;
}

private async getRemoteVersion(url: string): Promise<string | undefined> {
const response = await fetch(url);
private async getRemoteVersion(
channel: UpdateChannel,
): Promise<string | undefined> {
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<string | undefined> {
Expand All @@ -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>("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.");
Expand Down
80 changes: 49 additions & 31 deletions test/features/UpdatePowerShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
);
}
});
}
});
});
Loading