From f0e99fe07e252e4388dbc84793cf3b4600808f3e Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:08:36 -0700 Subject: [PATCH 01/24] Add independent VS Code extension releases --- .github/workflows/bump-vscode-typescript.yml | 138 ++++++++++ .github/workflows/tag-vscode-typescript.yml | 90 +++++++ Herebyfile.mjs | 111 ++++++-- tools/pipelines/vscode-typescript-build.yml | 186 ++++++++++++++ tools/pipelines/vscode-typescript-publish.yml | 240 ++++++++++++++++++ 5 files changed, 743 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/bump-vscode-typescript.yml create mode 100644 .github/workflows/tag-vscode-typescript.yml create mode 100644 tools/pipelines/vscode-typescript-build.yml create mode 100644 tools/pipelines/vscode-typescript-publish.yml diff --git a/.github/workflows/bump-vscode-typescript.yml b/.github/workflows/bump-vscode-typescript.yml new file mode 100644 index 0000000000000..fcbda1452f082 --- /dev/null +++ b/.github/workflows/bump-vscode-typescript.yml @@ -0,0 +1,138 @@ +name: Bump vscode-typescript + +on: + workflow_dispatch: + inputs: + extension_version: + description: Extension version, without the vscode-typescript/v prefix + required: true + type: string + bundled_typescript_version: + description: Exact published TypeScript version to bundle + required: true + type: string + +run-name: Bump vscode-typescript to ${{ inputs.extension_version }} + +permissions: + contents: read + id-token: write + +defaults: + run: + shell: bash + +jobs: + bump: + if: github.repository == 'microsoft/TypeScript' + runs-on: ubuntu-latest + environment: + name: azure + deployment: false + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + filter: blob:none + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 'lts/*' + + - name: Validate versions + env: + EXTENSION_VERSION: ${{ inputs.extension_version }} + TYPESCRIPT_VERSION: ${{ inputs.bundled_typescript_version }} + run: | + set -euo pipefail + versionPattern='^[0-9]+\.[0-9]+\.[0-9]+$' + if ! [[ "$EXTENSION_VERSION" =~ $versionPattern ]] || [ "$EXTENSION_VERSION" = "0.0.0" ]; then + echo "Extension version must be a non-placeholder three-component numeric version." >&2 + exit 1 + fi + if ! [[ "$TYPESCRIPT_VERSION" =~ $versionPattern ]]; then + echo "Bundled TypeScript version must be a three-component numeric version." >&2 + exit 1 + fi + npm view "typescript@$TYPESCRIPT_VERSION" version + + - run: npm ci + + - name: Check Marketplace version + env: + EXTENSION_VERSION: ${{ inputs.extension_version }} + run: | + set -euo pipefail + publishedVersion="$(npx vsce show TypeScriptTeam.native-preview --json | jq -r '.versions[0].version')" + PUBLISHED_VERSION="$publishedVersion" node <<'NODE' + const extensionVersion = process.env.EXTENSION_VERSION.split(".").map(Number); + const publishedVersion = process.env.PUBLISHED_VERSION.split(".").map(Number); + const comparison = extensionVersion.findIndex((part, index) => part !== publishedVersion[index]); + if (comparison === -1 || extensionVersion[comparison] < publishedVersion[comparison]) { + throw new Error(`Extension version ${process.env.EXTENSION_VERSION} must be greater than published version ${process.env.PUBLISHED_VERSION}.`); + } + NODE + + - name: Update versions + env: + EXTENSION_VERSION: ${{ inputs.extension_version }} + TYPESCRIPT_VERSION: ${{ inputs.bundled_typescript_version }} + run: | + set -euo pipefail + npm version "$EXTENSION_VERSION" \ + --workspace native-preview \ + --no-git-tag-version \ + --allow-same-version + npm install \ + --workspace native-preview \ + --save-dev \ + --save-exact \ + "@typescript/bundled-typescript@npm:typescript@$TYPESCRIPT_VERSION" + + packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" + lockVersion="$(jq -r '.packages["packages/vscode-typescript"].version' package-lock.json)" + if [ "$packageVersion" != "$lockVersion" ]; then + echo "package.json version $packageVersion does not match package-lock.json version $lockVersion." >&2 + exit 1 + fi + + - run: npm test -w native-preview + + - name: Package extension + run: npx hereby vscode-typescript:pack --forRelease --vscodeTypescriptRelease + + - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Create GitHub App token + id: app-token + uses: microsoft/create-github-app-token-via-key-vault@5ba0d436e9c3cac52feff4d1f2f66f9698ce4a2d # v1 + with: + client-id: ${{ vars.TYPESCRIPT_AUTOMATION_GITHUB_APP_CLIENT_ID }} + key-id: ${{ vars.TYPESCRIPT_AUTOMATION_GITHUB_APP_KEY_ID }} + owner: microsoft + repositories: TypeScript + permission-contents: write + + - name: Commit and push release branch + env: + EXTENSION_VERSION: ${{ inputs.extension_version }} + GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -euo pipefail + branch="vscode-typescript-release/v$EXTENSION_VERSION" + git switch -c "$branch" + git add packages/vscode-typescript/package.json package-lock.json + git config user.email "290192711+typescript-automation[bot]@users.noreply.github.com" + git config user.name "typescript-automation[bot]" + git commit -m "Bump vscode-typescript to $EXTENSION_VERSION" + + basic_auth="$(node -e 'process.stdout.write(Buffer.from("x-access-token:" + process.env.GITHUB_APP_TOKEN).toString("base64"))')" + echo "::add-mask::$basic_auth" + git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}" + git push --set-upstream origin "$branch" diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml new file mode 100644 index 0000000000000..60900c4fef0ec --- /dev/null +++ b/.github/workflows/tag-vscode-typescript.yml @@ -0,0 +1,90 @@ +name: Tag vscode-typescript release + +on: + workflow_dispatch: + inputs: + extension_version: + description: Reviewed extension version to release + required: true + type: string + +run-name: Tag vscode-typescript/v${{ inputs.extension_version }} + +permissions: + contents: read + id-token: write + +defaults: + run: + shell: bash + +jobs: + tag: + if: github.repository == 'microsoft/TypeScript' + runs-on: ubuntu-latest + environment: + name: azure + deployment: false + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + filter: blob:none + fetch-depth: 0 + persist-credentials: false + + - name: Validate release version + env: + EXTENSION_VERSION: ${{ inputs.extension_version }} + run: | + set -euo pipefail + versionPattern='^[0-9]+\.[0-9]+\.[0-9]+$' + if ! [[ "$EXTENSION_VERSION" =~ $versionPattern ]] || [ "$EXTENSION_VERSION" = "0.0.0" ]; then + echo "Extension version must be a non-placeholder three-component numeric version." >&2 + exit 1 + fi + + packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" + if [ "$EXTENSION_VERSION" != "$packageVersion" ]; then + echo "Requested version $EXTENSION_VERSION does not match main's package version $packageVersion." >&2 + exit 1 + fi + + tag="vscode-typescript/v$EXTENSION_VERSION" + if git rev-parse --verify --quiet "refs/tags/$tag"; then + echo "Tag $tag already exists." >&2 + exit 1 + fi + + - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Create GitHub App token + id: app-token + uses: microsoft/create-github-app-token-via-key-vault@5ba0d436e9c3cac52feff4d1f2f66f9698ce4a2d # v1 + with: + client-id: ${{ vars.TYPESCRIPT_AUTOMATION_GITHUB_APP_CLIENT_ID }} + key-id: ${{ vars.TYPESCRIPT_AUTOMATION_GITHUB_APP_KEY_ID }} + owner: microsoft + repositories: TypeScript + permission-contents: write + + - name: Create release tag + env: + EXTENSION_VERSION: ${{ inputs.extension_version }} + GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -euo pipefail + tag="vscode-typescript/v$EXTENSION_VERSION" + git config user.email "290192711+typescript-automation[bot]@users.noreply.github.com" + git config user.name "typescript-automation[bot]" + git tag --annotate "$tag" --message "vscode-typescript $EXTENSION_VERSION" + + basic_auth="$(node -e 'process.stdout.write(Buffer.from("x-access-token:" + process.env.GITHUB_APP_TOKEN).toString("base64"))')" + echo "::add-mask::$basic_auth" + git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}" + git push origin "refs/tags/$tag" diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 381b13808b5e4..9072879d3db32 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -109,6 +109,7 @@ const { values: rawOptions } = parseArgs({ setPrerelease: { type: "string" }, forRelease: { type: "boolean" }, + vscodeTypescriptRelease: { type: "boolean" }, race: { type: "boolean", default: parseEnvBoolean("RACE") }, noembed: { type: "boolean", default: parseEnvBoolean("NOEMBED") }, @@ -130,17 +131,21 @@ const options = /** @type {Options} */ (rawOptions); // Main publishes prerelease builds of the TypeScript package. const nativePreviewReleaseProfile = /** @type {"native-preview" | "typescript"} */ ("typescript"); const nativePreviewReleaseVersion = /** @type {string | undefined} */ (undefined); -const produceNativePreviewVsix = /** @type {boolean} */ (false); -const produceTypeScriptNightlyVsix = /** @type {boolean} */ (true); -const usePublishedPlatformPackagesForVsix = /** @type {boolean} */ (false); +const releaseVscodeTypescript = !!options.vscodeTypescriptRelease; +const produceNativePreviewVsix = releaseVscodeTypescript; +const produceTypeScriptNightlyVsix = !releaseVscodeTypescript; +const usePublishedPlatformPackagesForVsix = releaseVscodeTypescript; const produceAnyVsix = produceNativePreviewVsix || produceTypeScriptNightlyVsix; const publishAsTypescript = nativePreviewReleaseProfile === "typescript"; -if (options.forRelease && !options.setPrerelease && (!nativePreviewReleaseVersion || produceAnyVsix)) { +if (releaseVscodeTypescript && options.setPrerelease) { + throw new Error("vscode-typescript releases use the extension's package.json version and do not accept setPrerelease"); +} +if (!releaseVscodeTypescript && options.forRelease && !options.setPrerelease && (!nativePreviewReleaseVersion || produceAnyVsix)) { throw new Error("forRelease requires setPrerelease unless nativePreviewReleaseVersion is hardcoded and VSIX production is disabled"); } -if (usePublishedPlatformPackagesForVsix && !publishAsTypescript) { - throw new Error("usePublishedPlatformPackagesForVsix requires nativePreviewReleaseProfile to be 'typescript'"); +if (releaseVscodeTypescript && !publishAsTypescript) { + throw new Error("vscode-typescript releases require nativePreviewReleaseProfile to be 'typescript'"); } const defaultGoBuildTags = [ @@ -1700,6 +1705,19 @@ const builtSignTmp = path.resolve("./built/sign-tmp"); const publishedTypeScriptAliasPackageName = "@typescript/bundled-typescript"; const releasePackageEnv = { COREPACK_ENABLE_STRICT: "0" }; +const getVscodeTypeScriptExtensionPackageJson = memoize(() => JSON.parse(fs.readFileSync(path.join(extensionDir, "package.json"), "utf8"))); + +function getVscodeTypeScriptExtensionVersion() { + const version = getVscodeTypeScriptExtensionPackageJson().version; + if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version)) { + throw new Error(`packages/vscode-typescript/package.json must contain a three-component numeric version, got ${JSON.stringify(version)}.`); + } + if (version === "0.0.0") { + throw new Error("Refusing to release vscode-typescript with placeholder version 0.0.0."); + } + return version; +} + const getSignTempDir = memoize(async () => { const dir = path.resolve(builtSignTmp); await rimraf(dir); @@ -2072,8 +2090,8 @@ function nodeToGOARCH(arch, os) { } const getPlatforms = memoize(() => { - const publishTag = getPublishTag(); - let supportedPlatforms = publishAsTypescript && publishTag !== "next" + const publishTag = releaseVscodeTypescript ? undefined : getPublishTag(); + let supportedPlatforms = !releaseVscodeTypescript && publishAsTypescript && publishTag !== "next" ? platforms : platforms.filter(({ vsix }) => vsix); @@ -2649,9 +2667,8 @@ const getPublishedTypeScriptPackageJson = memoize(() => { function getPublishedTypeScriptVersion() { const version = getPublishedTypeScriptPackageJson().version; - const expectedVersion = getVersion(); - if (usePublishedPlatformPackagesForVsix && version !== expectedVersion) { - throw new Error(`usePublishedPlatformPackagesForVsix requires ${publishedTypeScriptAliasPackageName}'s installed version (${version}) to match release version ${expectedVersion}.`); + if (releaseVscodeTypescript && !/^\d+\.\d+\.\d+$/.test(version)) { + throw new Error(`vscode-typescript releases require a stable three-component TypeScript version, got ${version}.`); } return version; } @@ -2747,18 +2764,23 @@ async function runPackVsixExtensions() { let version = "0.0.0"; if (options.forRelease) { - // No real semver prerelease versioning. - // https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prerelease-extensions - assert(options.setPrerelease, "forRelease is true but setPrerelease is not set"); - const prerelease = options.setPrerelease; - assert(typeof prerelease === "string", "setPrerelease is not a string"); - // parse `dev..`. - const match = prerelease.match(/dev\.(\d+)\.(\d+)/); - if (!match) { - throw new Error(`Prerelease version should be in the form of dev.., but got ${prerelease}`); + if (releaseVscodeTypescript) { + version = getVscodeTypeScriptExtensionVersion(); + } + else { + // No real semver prerelease versioning. + // https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prerelease-extensions + assert(options.setPrerelease, "forRelease is true but setPrerelease is not set"); + const prerelease = options.setPrerelease; + assert(typeof prerelease === "string", "setPrerelease is not a string"); + // parse `dev..`. + const match = prerelease.match(/dev\.(\d+)\.(\d+)/); + if (!match) { + throw new Error(`Prerelease version should be in the form of dev.., but got ${prerelease}`); + } + // Set version to `0..`. + version = `0.${match[1]}.${match[2]}`; } - // Set version to `0..`. - version = `0.${match[1]}.${match[2]}`; } console.log("Version:", version); @@ -2826,10 +2848,55 @@ async function runSignVsixExtensions() { }); } +async function runWriteVscodeTypeScriptReleaseManifest() { + const platforms = getPlatforms(); + const extensions = platforms.flatMap(({ extensions }) => extensions); + /** @type {Record} */ + const artifacts = {}; + for (const extension of extensions) { + for (const artifactPath of [extension.vsixPath, extension.vsixManifestPath, extension.vsixSignaturePath]) { + const filename = path.basename(artifactPath); + artifacts[filename] = { + sha256: crypto.createHash("sha256").update(await fs.promises.readFile(artifactPath)).digest("hex"), + }; + } + } + + const packageJson = getVscodeTypeScriptExtensionPackageJson(); + const manifest = { + extension: `${packageJson.publisher}.${packageJson.name}`, + extensionVersion: getVscodeTypeScriptExtensionVersion(), + bundledTypeScriptVersion: getPublishedTypeScriptVersion(), + signType: process.env.VSCODE_TYPESCRIPT_SIGN_TYPE, + sourceRef: process.env.BUILD_SOURCEBRANCH || process.env.GITHUB_REF || undefined, + sourceCommit: process.env.BUILD_SOURCEVERSION || process.env.GITHUB_SHA || undefined, + targets: extensions.map(({ vscodeTarget }) => vscodeTarget), + artifacts, + }; + await fs.promises.writeFile(path.join(builtVsix, "release-manifest.json"), JSON.stringify(manifest, undefined, 4) + "\n"); +} + +export const vscodeTypescriptRelease = task({ + name: "vscode-typescript:release", + hiddenFromTaskList: true, + run: async () => { + if (!options.forRelease || !releaseVscodeTypescript) { + throw new Error("vscode-typescript:release requires --forRelease and --vscodeTypescriptRelease"); + } + await runPackVsixExtensions(); + await runSignVsixExtensions(); + await runWriteVscodeTypeScriptReleaseManifest(); + await runCleanSignTempDirectory(); + }, +}); + export const nativePreviewRelease = task({ name: "typescript:release", hiddenFromTaskList: true, run: async () => { + if (releaseVscodeTypescript) { + throw new Error("typescript:release cannot be used with --vscodeTypescriptRelease; use vscode-typescript:release"); + } if (!options.forRelease || !options.setPrerelease && (!nativePreviewReleaseVersion || produceAnyVsix)) { throw new Error("typescript:release requires --forRelease and --setPrerelease flags, unless nativePreviewReleaseVersion is hardcoded and VSIX production is disabled. Example: npx hereby typescript:release --forRelease --setPrerelease=dev.1.0"); } diff --git a/tools/pipelines/vscode-typescript-build.yml b/tools/pipelines/vscode-typescript-build.yml new file mode 100644 index 0000000000000..fd2e43bc9a4d9 --- /dev/null +++ b/tools/pipelines/vscode-typescript-build.yml @@ -0,0 +1,186 @@ +trigger: + tags: + include: + - vscode-typescript/v* + +pr: none + +name: vscode-typescript-$(Date:yyyyMMdd)$(Rev:.r) +appendCommitMessageToRunName: false + +parameters: + - name: signType + displayName: Sign type + type: string + default: real + values: + - real + - test + +variables: + - name: TeamName + value: TypeScript + +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release + +extends: + template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate + + parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean + sdl: + git: + submodules: false + fetchDepth: 0 + fetchTags: false + retryCount: 3 + sourceAnalysisPool: VSEngSS-MicroBuild2022-1ES + pool: + name: AzurePipelines-EO + image: 1ESPT-Ubuntu22.04 + os: linux + + stages: + - stage: Build + displayName: Build and sign vscode-typescript + jobs: + - job: Build + displayName: Build and sign vscode-typescript + timeoutInMinutes: 90 + + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)/vsix + artifactName: vsix + + steps: + - checkout: self + clean: true + submodules: false + fetchDepth: 0 + fetchFilter: blob:none + fetchTags: false + + - bash: | + set -euo pipefail + git fetch origin main --no-tags + if ! git merge-base --is-ancestor "$BUILD_SOURCEVERSION" refs/remotes/origin/main; then + echo "Release tags must point to commits already merged into main." >&2 + exit 1 + fi + + tag="${BUILD_SOURCEBRANCH#refs/tags/}" + case "$tag" in + vscode-typescript/v*) ;; + *) + echo "Expected a vscode-typescript/v* tag, got $BUILD_SOURCEBRANCH." >&2 + exit 1 + ;; + esac + + tagVersion="${tag#vscode-typescript/v}" + packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" + lockVersion="$(jq -r '.packages["packages/vscode-typescript"].version' package-lock.json)" + if [ "$tagVersion" != "$packageVersion" ]; then + echo "Tag version $tagVersion does not match package version $packageVersion." >&2 + exit 1 + fi + if [ "$packageVersion" != "$lockVersion" ]; then + echo "package.json version $packageVersion does not match package-lock.json version $lockVersion." >&2 + exit 1 + fi + if ! [[ "$packageVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || [ "$packageVersion" = "0.0.0" ]; then + echo "Extension version must be a non-placeholder three-component numeric version." >&2 + exit 1 + fi + + echo "##vso[build.updatebuildnumber]vscode-typescript-$packageVersion" + displayName: Validate release tag + + - task: NuGetAuthenticate@1 + displayName: '🔩 NuGet Authenticate' + - task: UsePythonVersion@0 + displayName: Use Python 3.11 + inputs: + versionSpec: 3.11 + - task: UseDotNet@2 + displayName: Use .NET Core 3.1.x + inputs: + packageType: sdk + version: 3.1.x + - task: UseDotNet@2 + displayName: Use .NET Core SDK 8.0.x + inputs: + version: 8.0.x + - task: MicroBuildSigningPlugin@4 + displayName: '🔩 Install Signing Plugin' + inputs: + signType: ${{ parameters.signType }} + azureSubscription: MicroBuild Signing Task (DevDiv) + ${{ if eq(parameters.signType, 'real') }}: + ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 + zipSources: false + env: + MicroBuildOutputFolderOverride: $(Agent.TempDirectory) + + - template: /tools/pipelines/steps/setup-node-npm-ci.yml@self + + - bash: | + set -euo pipefail + packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" + publishedVersion="$(npx vsce show TypeScriptTeam.native-preview --json | jq -r '.versions[0].version')" + EXTENSION_VERSION="$packageVersion" PUBLISHED_VERSION="$publishedVersion" node <<'NODE' + const extensionVersion = process.env.EXTENSION_VERSION.split(".").map(Number); + const publishedVersion = process.env.PUBLISHED_VERSION.split(".").map(Number); + const comparison = extensionVersion.findIndex((part, index) => part !== publishedVersion[index]); + if (comparison === -1 || extensionVersion[comparison] < publishedVersion[comparison]) { + throw new Error(`Extension version ${process.env.EXTENSION_VERSION} must be greater than published version ${process.env.PUBLISHED_VERSION}.`); + } + NODE + displayName: Check Marketplace version + + - bash: npm test -w native-preview + displayName: Test extension + + - bash: npx hereby vscode-typescript:release --forRelease --vscodeTypescriptRelease + displayName: Build and sign extensions + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + VSCODE_TYPESCRIPT_SIGN_TYPE: ${{ parameters.signType }} + + - ${{ if eq(parameters.signType, 'real') }}: + - bash: | + set -euo pipefail + shopt -s nullglob + signatureFiles=(built/vsix/*.signature.p7s) + if (( ${#signatureFiles[@]} == 0 )); then + echo "No VSIX signatures were produced." >&2 + exit 1 + fi + for signatureFile in "${signatureFiles[@]}"; do + manifestFile="${signatureFile%.signature.p7s}.manifest" + if cmp -s "$manifestFile" "$signatureFile"; then + echo "$signatureFile was not replaced with a real signature." >&2 + exit 1 + fi + openssl pkcs7 -inform DER -in "$signatureFile" -noout + done + echo "##vso[build.addbuildtag]vscode-typescript-release" + displayName: Validate release signatures + + - bash: | + set -euo pipefail + artifactDirectory="$(Build.ArtifactStagingDirectory)/vsix" + mkdir -p "$artifactDirectory" + cp -v built/vsix/*.vsix "$artifactDirectory" + cp -v built/vsix/*.manifest "$artifactDirectory" + cp -v built/vsix/*.signature.p7s "$artifactDirectory" + cp -v built/vsix/release-manifest.json "$artifactDirectory" + displayName: Stage release artifacts diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml new file mode 100644 index 0000000000000..dc57fe93569ec --- /dev/null +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -0,0 +1,240 @@ +trigger: none + +name: vscode-typescript-publish-$(Date:yyyyMMdd)$(Rev:.r) +appendCommitMessageToRunName: false + +parameters: + - name: dryRun + displayName: Dry run + type: boolean + default: false + +variables: + - name: TeamName + value: TypeScript + +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release + pipelines: + - pipeline: VSCode_TypeScript_Release_Build + project: DevDiv + source: VSCode TypeScript Release Build + trigger: + tags: + - vscode-typescript-release + +extends: + template: azure-pipelines/MicroBuild.1ES.Official.Publish.yml@MicroBuildTemplate + + parameters: + sdl: + sourceAnalysisPool: VSEngSS-MicroBuild2022-1ES + pool: + name: AzurePipelines-EO + image: 1ESPT-Ubuntu22.04 + os: linux + settings: + networkIsolationPolicy: Permissive,CFSClean + + stages: + - stage: Publish + displayName: Publish vscode-typescript + jobs: + - job: Publish + displayName: Publish vscode-typescript + + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: VSCode_TypeScript_Release_Build + artifactName: vsix + targetPath: $(Pipeline.Workspace)/vsix + + steps: + - checkout: none + + - bash: | + set -euo pipefail + artifactDirectory="$(Pipeline.Workspace)/vsix" + manifest="$artifactDirectory/release-manifest.json" + test -f "$manifest" + + extension="$(jq -r '.extension' "$manifest")" + version="$(jq -r '.extensionVersion' "$manifest")" + bundledTypeScriptVersion="$(jq -r '.bundledTypeScriptVersion' "$manifest")" + signType="$(jq -r '.signType' "$manifest")" + sourceRef="$(jq -r '.sourceRef' "$manifest")" + sourceCommit="$(jq -r '.sourceCommit' "$manifest")" + if [ "$extension" != "TypeScriptTeam.native-preview" ]; then + echo "Unexpected extension identity: $extension" >&2 + exit 1 + fi + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || [ "$version" = "0.0.0" ]; then + echo "Invalid extension version: $version" >&2 + exit 1 + fi + if ! [[ "$bundledTypeScriptVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid bundled TypeScript version: $bundledTypeScriptVersion" >&2 + exit 1 + fi + if [ "$signType" != "real" ]; then + echo "Refusing to publish artifacts signed with signType=$signType." >&2 + exit 1 + fi + if [ "$sourceRef" != "$RELEASE_SOURCE_BRANCH" ]; then + echo "Artifact source ref $sourceRef does not match triggering build ref $RELEASE_SOURCE_BRANCH." >&2 + exit 1 + fi + if [ "$sourceCommit" != "$RELEASE_SOURCE_COMMIT" ]; then + echo "Artifact source commit $sourceCommit does not match triggering build commit $RELEASE_SOURCE_COMMIT." >&2 + exit 1 + fi + case "$sourceRef" in + refs/tags/vscode-typescript/v"$version") ;; + *) + echo "Source ref $sourceRef does not match extension version $version." >&2 + exit 1 + ;; + esac + + while IFS=$'\t' read -r filename expectedHash; do + artifact="$artifactDirectory/$filename" + test -f "$artifact" + actualHash="$(sha256sum "$artifact" | cut -d' ' -f1)" + if [ "$actualHash" != "$expectedHash" ]; then + echo "Hash mismatch for $filename." >&2 + exit 1 + fi + done < <(jq -r '.artifacts | to_entries[] | [.key, .value.sha256] | @tsv' "$manifest") + + shopt -s nullglob + vsixFiles=("$artifactDirectory"/*.vsix) + if (( ${#vsixFiles[@]} == 0 )); then + echo "No VSIX artifacts found." >&2 + exit 1 + fi + for vsixFilePath in "${vsixFiles[@]}"; do + manifestFile="${vsixFilePath%.vsix}.manifest" + signatureFile="${vsixFilePath%.vsix}.signature.p7s" + for artifact in "$vsixFilePath" "$manifestFile" "$signatureFile"; do + filename="$(basename "$artifact")" + jq -e --arg filename "$filename" '.artifacts | has($filename)' "$manifest" >/dev/null + done + if cmp -s "$manifestFile" "$signatureFile"; then + echo "$signatureFile does not contain a real signature." >&2 + exit 1 + fi + openssl pkcs7 -inform DER -in "$signatureFile" -noout + + packageVersion="$(unzip -p "$vsixFilePath" extension/package.json | jq -r '.version')" + packageBundledTypeScriptVersion="$(unzip -p "$vsixFilePath" extension/package.json | jq -r '.bundledTypeScriptVersion')" + if [ "$packageVersion" != "$version" ] || [ "$packageBundledTypeScriptVersion" != "$bundledTypeScriptVersion" ]; then + echo "Version metadata mismatch in $(basename "$vsixFilePath")." >&2 + exit 1 + fi + done + targetCount="$(jq '.targets | length' "$manifest")" + if [ "${#vsixFiles[@]}" -ne "$targetCount" ]; then + echo "Expected $targetCount VSIXs, found ${#vsixFiles[@]}." >&2 + exit 1 + fi + displayName: Validate signed release artifacts + env: + RELEASE_SOURCE_BRANCH: $(resources.pipeline.VSCode_TypeScript_Release_Build.sourceBranch) + RELEASE_SOURCE_COMMIT: $(resources.pipeline.VSCode_TypeScript_Release_Build.sourceCommit) + + - task: NodeTool@0 + inputs: + versionSpec: 24.x + displayName: Install Node + + - bash: | + cat > .npmrc << 'EOF' + registry=https://pkgs.dev.azure.com/devdiv/devdiv/_packaging/devdiv_PublicPackages/npm/registry/ + EOF + npm init -y + displayName: Set up npm + + - task: npmAuthenticate@0 + inputs: + workingFile: .npmrc + displayName: Authenticate npm + + - bash: npm install @vscode/vsce@3.9.2 + displayName: Install vsce + + - task: AzureCLI@2 + displayName: Check Marketplace authentication + inputs: + azureSubscription: TypeScript-VSMarketplacePublishAuth + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + set -euo pipefail + az rest -u https://app.vssps.visualstudio.com/_apis/profile/profiles/me --resource 499b84ac-1321-427f-aa17-267ca6975798 + npx vsce verify-pat TypeScriptTeam --azure-credential + + - ${{ if eq(parameters.dryRun, false) }}: + - task: AzureCLI@2 + displayName: Publish VSIXs to Marketplace + retryCountOnTaskFailure: 3 + inputs: + azureSubscription: TypeScript-VSMarketplacePublishAuth + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + set -euo pipefail + shopt -s nullglob + for vsixFilePath in "$(Pipeline.Workspace)"/vsix/*.vsix; do + manifestFilePath="${vsixFilePath%.vsix}.manifest" + signatureFilePath="${vsixFilePath%.vsix}.signature.p7s" + npx vsce publish \ + --packagePath "$vsixFilePath" \ + --manifestPath "$manifestFilePath" \ + --signaturePath "$signatureFilePath" \ + --azure-credential \ + --skip-duplicate \ + --allow-all-proposed-apis + done + + - template: /tools/pipelines/steps/create-github-app-token.yml@self + parameters: + repositories: TypeScript + permissions: contents:write + insertSteps: + - bash: | + set -euo pipefail + artifactDirectory="$(Pipeline.Workspace)/vsix" + manifest="$artifactDirectory/release-manifest.json" + version="$(jq -r '.extensionVersion' "$manifest")" + bundledTypeScriptVersion="$(jq -r '.bundledTypeScriptVersion' "$manifest")" + sourceCommit="$(jq -r '.sourceCommit' "$manifest")" + tag="vscode-typescript/v$version" + + shopt -s nullglob + vsixFiles=("$artifactDirectory"/*.vsix) + if (( ${#vsixFiles[@]} == 0 )); then + echo "No VSIX artifacts found." >&2 + exit 1 + fi + + if gh release view "$tag" --repo microsoft/TypeScript >/dev/null 2>&1; then + gh release upload "$tag" "${vsixFiles[@]}" \ + --repo microsoft/TypeScript \ + --clobber + else + gh release create "$tag" "${vsixFiles[@]}" \ + --repo microsoft/TypeScript \ + --title "$tag" \ + --latest=false \ + --notes "Bundles TypeScript $bundledTypeScriptVersion from commit $sourceCommit." + fi + displayName: Create GitHub Release + env: + GH_TOKEN: $(GH_TOKEN) From c600717b3543ae71b180a685d45a7046e0fdeb41 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:14:03 -0700 Subject: [PATCH 02/24] Require real signing for extension releases Validate the package-lock version before creating a release tag and remove the unsupported test-signing path from the VSIX build pipeline. --- .github/workflows/tag-vscode-typescript.yml | 5 ++ tools/pipelines/vscode-typescript-build.yml | 55 +++++++++------------ 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml index 60900c4fef0ec..c8ae85a878fcf 100644 --- a/.github/workflows/tag-vscode-typescript.yml +++ b/.github/workflows/tag-vscode-typescript.yml @@ -50,6 +50,11 @@ jobs: echo "Requested version $EXTENSION_VERSION does not match main's package version $packageVersion." >&2 exit 1 fi + lockVersion="$(jq -r '.packages["packages/vscode-typescript"].version' package-lock.json)" + if [ "$packageVersion" != "$lockVersion" ]; then + echo "package.json version $packageVersion does not match package-lock.json version $lockVersion." >&2 + exit 1 + fi tag="vscode-typescript/v$EXTENSION_VERSION" if git rev-parse --verify --quiet "refs/tags/$tag"; then diff --git a/tools/pipelines/vscode-typescript-build.yml b/tools/pipelines/vscode-typescript-build.yml index fd2e43bc9a4d9..a3f3bc0d9b6be 100644 --- a/tools/pipelines/vscode-typescript-build.yml +++ b/tools/pipelines/vscode-typescript-build.yml @@ -8,15 +8,6 @@ pr: none name: vscode-typescript-$(Date:yyyyMMdd)$(Rev:.r) appendCommitMessageToRunName: false -parameters: - - name: signType - displayName: Sign type - type: string - default: real - values: - - real - - test - variables: - name: TeamName value: TypeScript @@ -122,10 +113,9 @@ extends: - task: MicroBuildSigningPlugin@4 displayName: '🔩 Install Signing Plugin' inputs: - signType: ${{ parameters.signType }} + signType: real azureSubscription: MicroBuild Signing Task (DevDiv) - ${{ if eq(parameters.signType, 'real') }}: - ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 + ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 zipSources: false env: MicroBuildOutputFolderOverride: $(Agent.TempDirectory) @@ -153,27 +143,26 @@ extends: displayName: Build and sign extensions env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) - VSCODE_TYPESCRIPT_SIGN_TYPE: ${{ parameters.signType }} - - - ${{ if eq(parameters.signType, 'real') }}: - - bash: | - set -euo pipefail - shopt -s nullglob - signatureFiles=(built/vsix/*.signature.p7s) - if (( ${#signatureFiles[@]} == 0 )); then - echo "No VSIX signatures were produced." >&2 - exit 1 - fi - for signatureFile in "${signatureFiles[@]}"; do - manifestFile="${signatureFile%.signature.p7s}.manifest" - if cmp -s "$manifestFile" "$signatureFile"; then - echo "$signatureFile was not replaced with a real signature." >&2 - exit 1 - fi - openssl pkcs7 -inform DER -in "$signatureFile" -noout - done - echo "##vso[build.addbuildtag]vscode-typescript-release" - displayName: Validate release signatures + VSCODE_TYPESCRIPT_SIGN_TYPE: real + + - bash: | + set -euo pipefail + shopt -s nullglob + signatureFiles=(built/vsix/*.signature.p7s) + if (( ${#signatureFiles[@]} == 0 )); then + echo "No VSIX signatures were produced." >&2 + exit 1 + fi + for signatureFile in "${signatureFiles[@]}"; do + manifestFile="${signatureFile%.signature.p7s}.manifest" + if cmp -s "$manifestFile" "$signatureFile"; then + echo "$signatureFile was not replaced with a real signature." >&2 + exit 1 + fi + openssl pkcs7 -inform DER -in "$signatureFile" -noout + done + echo "##vso[build.addbuildtag]vscode-typescript-release" + displayName: Validate release signatures - bash: | set -euo pipefail From b99e410d3b49e406ee5d7e1e2c02db4d92d22aa3 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:43:39 -0700 Subject: [PATCH 03/24] Open a PR for extension version bumps --- .github/workflows/bump-vscode-typescript.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bump-vscode-typescript.yml b/.github/workflows/bump-vscode-typescript.yml index fcbda1452f082..16262f707eafc 100644 --- a/.github/workflows/bump-vscode-typescript.yml +++ b/.github/workflows/bump-vscode-typescript.yml @@ -33,6 +33,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: main filter: blob:none fetch-depth: 0 persist-credentials: false @@ -118,11 +119,14 @@ jobs: owner: microsoft repositories: TypeScript permission-contents: write + permission-pull-requests: write - - name: Commit and push release branch + - name: Commit, push, and open pull request env: EXTENSION_VERSION: ${{ inputs.extension_version }} + TYPESCRIPT_VERSION: ${{ inputs.bundled_typescript_version }} GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -euo pipefail branch="vscode-typescript-release/v$EXTENSION_VERSION" @@ -136,3 +140,10 @@ jobs: echo "::add-mask::$basic_auth" git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}" git push --set-upstream origin "$branch" + + gh pr create \ + --repo microsoft/TypeScript \ + --base main \ + --head "$branch" \ + --title "Bump vscode-typescript to $EXTENSION_VERSION" \ + --body "Updates the vscode-typescript extension to $EXTENSION_VERSION and bundles TypeScript $TYPESCRIPT_VERSION." From 9e5a0e231ca5416b5fc28255d7c757ef44f6ce27 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:48:43 -0700 Subject: [PATCH 04/24] Isolate extension bump credentials --- .github/workflows/bump-vscode-typescript.yml | 53 +++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bump-vscode-typescript.yml b/.github/workflows/bump-vscode-typescript.yml index 16262f707eafc..1333f3f523ba8 100644 --- a/.github/workflows/bump-vscode-typescript.yml +++ b/.github/workflows/bump-vscode-typescript.yml @@ -16,19 +16,15 @@ run-name: Bump vscode-typescript to ${{ inputs.extension_version }} permissions: contents: read - id-token: write defaults: run: shell: bash jobs: - bump: + prepare: if: github.repository == 'microsoft/TypeScript' runs-on: ubuntu-latest - environment: - name: azure - deployment: false steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -104,6 +100,51 @@ jobs: - name: Package extension run: npx hereby vscode-typescript:pack --forRelease --vscodeTypescriptRelease + - name: Create version bump patch + run: git diff --binary -- packages/vscode-typescript/package.json package-lock.json > vscode-typescript-bump.patch + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: vscode-typescript-bump + path: vscode-typescript-bump.patch + if-no-files-found: error + + create-pr: + needs: prepare + if: github.repository == 'microsoft/TypeScript' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + environment: + name: azure + deployment: false + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + filter: blob:none + fetch-depth: 0 + persist-credentials: false + + - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: vscode-typescript-bump + path: ${{ runner.temp }} + + - name: Apply version bump + run: | + set -euo pipefail + git apply --index "$RUNNER_TEMP/vscode-typescript-bump.patch" + + mapfile -t changedFiles < <(git diff --cached --name-only) + expectedFiles=("package-lock.json" "packages/vscode-typescript/package.json") + if [ "${changedFiles[*]}" != "${expectedFiles[*]}" ]; then + echo "Unexpected files in version bump: ${changedFiles[*]}" >&2 + exit 1 + fi + - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: ${{ vars.AZURE_CLIENT_ID }} @@ -131,9 +172,9 @@ jobs: set -euo pipefail branch="vscode-typescript-release/v$EXTENSION_VERSION" git switch -c "$branch" - git add packages/vscode-typescript/package.json package-lock.json git config user.email "290192711+typescript-automation[bot]@users.noreply.github.com" git config user.name "typescript-automation[bot]" + git config core.hooksPath /dev/null git commit -m "Bump vscode-typescript to $EXTENSION_VERSION" basic_auth="$(node -e 'process.stdout.write(Buffer.from("x-access-token:" + process.env.GITHUB_APP_TOKEN).toString("base64"))')" From 980acb4ec769e152b6ed27111c5c837c3a87db2f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:52:58 -0700 Subject: [PATCH 05/24] Make extension bump workflow resumable --- .github/workflows/bump-vscode-typescript.yml | 39 +++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/.github/workflows/bump-vscode-typescript.yml b/.github/workflows/bump-vscode-typescript.yml index 1333f3f523ba8..fa4cca271f6ab 100644 --- a/.github/workflows/bump-vscode-typescript.yml +++ b/.github/workflows/bump-vscode-typescript.yml @@ -25,6 +25,8 @@ jobs: prepare: if: github.repository == 'microsoft/TypeScript' runs-on: ubuntu-latest + outputs: + source-sha: ${{ steps.source.outputs.sha }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -34,6 +36,10 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Record source commit + id: source + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 'lts/*' @@ -123,7 +129,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: main + ref: ${{ needs.prepare.outputs.source-sha }} filter: blob:none fetch-depth: 0 persist-credentials: false @@ -166,6 +172,7 @@ jobs: env: EXTENSION_VERSION: ${{ inputs.extension_version }} TYPESCRIPT_VERSION: ${{ inputs.bundled_typescript_version }} + SOURCE_SHA: ${{ needs.prepare.outputs.source-sha }} GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | @@ -180,11 +187,33 @@ jobs: basic_auth="$(node -e 'process.stdout.write(Buffer.from("x-access-token:" + process.env.GITHUB_APP_TOKEN).toString("base64"))')" echo "::add-mask::$basic_auth" git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}" - git push --set-upstream origin "$branch" - gh pr create \ + if git ls-remote --exit-code --heads origin "$branch" >/dev/null; then + git fetch origin "refs/heads/$branch:refs/remotes/origin/$branch" + existingCommit="$(git rev-parse "origin/$branch")" + existingParent="$(git rev-parse "origin/$branch^")" + if [ "$existingParent" != "$SOURCE_SHA" ] || ! git diff --quiet HEAD "$existingCommit"; then + echo "Existing branch $branch does not match this release bump." >&2 + exit 1 + fi + else + git push --set-upstream origin "$branch" + fi + + existingPr="$(gh pr list \ --repo microsoft/TypeScript \ --base main \ --head "$branch" \ - --title "Bump vscode-typescript to $EXTENSION_VERSION" \ - --body "Updates the vscode-typescript extension to $EXTENSION_VERSION and bundles TypeScript $TYPESCRIPT_VERSION." + --state open \ + --json url \ + --jq '.[0].url // empty')" + if [ -n "$existingPr" ]; then + echo "Pull request already exists: $existingPr" + else + gh pr create \ + --repo microsoft/TypeScript \ + --base main \ + --head "$branch" \ + --title "Bump vscode-typescript to $EXTENSION_VERSION" \ + --body "Updates the vscode-typescript extension to $EXTENSION_VERSION and bundles TypeScript $TYPESCRIPT_VERSION." + fi From 7225576168405d284c916c4107efa547047b34e3 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:17:29 -0700 Subject: [PATCH 06/24] Refine extension release workflow inputs --- .github/workflows/bump-vscode-typescript.yml | 45 ++++++++----------- .github/workflows/tag-vscode-typescript.yml | 20 ++++++--- tools/pipelines/vscode-typescript-build.yml | 2 +- tools/pipelines/vscode-typescript-publish.yml | 12 ++--- 4 files changed, 39 insertions(+), 40 deletions(-) diff --git a/.github/workflows/bump-vscode-typescript.yml b/.github/workflows/bump-vscode-typescript.yml index fa4cca271f6ab..1fdcc7df04fcc 100644 --- a/.github/workflows/bump-vscode-typescript.yml +++ b/.github/workflows/bump-vscode-typescript.yml @@ -3,16 +3,20 @@ name: Bump vscode-typescript on: workflow_dispatch: inputs: - extension_version: - description: Extension version, without the vscode-typescript/v prefix + major: + description: Extension major version required: true - type: string - bundled_typescript_version: - description: Exact published TypeScript version to bundle + type: number + minor: + description: Extension minor version required: true - type: string + type: number + patch: + description: Extension patch version + required: true + type: number -run-name: Bump vscode-typescript to ${{ inputs.extension_version }} +run-name: Bump vscode-typescript to ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} permissions: contents: read @@ -44,10 +48,9 @@ jobs: with: node-version: 'lts/*' - - name: Validate versions + - name: Validate extension version env: - EXTENSION_VERSION: ${{ inputs.extension_version }} - TYPESCRIPT_VERSION: ${{ inputs.bundled_typescript_version }} + EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} run: | set -euo pipefail versionPattern='^[0-9]+\.[0-9]+\.[0-9]+$' @@ -55,17 +58,12 @@ jobs: echo "Extension version must be a non-placeholder three-component numeric version." >&2 exit 1 fi - if ! [[ "$TYPESCRIPT_VERSION" =~ $versionPattern ]]; then - echo "Bundled TypeScript version must be a three-component numeric version." >&2 - exit 1 - fi - npm view "typescript@$TYPESCRIPT_VERSION" version - run: npm ci - name: Check Marketplace version env: - EXTENSION_VERSION: ${{ inputs.extension_version }} + EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} run: | set -euo pipefail publishedVersion="$(npx vsce show TypeScriptTeam.native-preview --json | jq -r '.versions[0].version')" @@ -78,21 +76,15 @@ jobs: } NODE - - name: Update versions + - name: Update extension version env: - EXTENSION_VERSION: ${{ inputs.extension_version }} - TYPESCRIPT_VERSION: ${{ inputs.bundled_typescript_version }} + EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} run: | set -euo pipefail npm version "$EXTENSION_VERSION" \ --workspace native-preview \ --no-git-tag-version \ --allow-same-version - npm install \ - --workspace native-preview \ - --save-dev \ - --save-exact \ - "@typescript/bundled-typescript@npm:typescript@$TYPESCRIPT_VERSION" packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" lockVersion="$(jq -r '.packages["packages/vscode-typescript"].version' package-lock.json)" @@ -170,8 +162,7 @@ jobs: - name: Commit, push, and open pull request env: - EXTENSION_VERSION: ${{ inputs.extension_version }} - TYPESCRIPT_VERSION: ${{ inputs.bundled_typescript_version }} + EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} SOURCE_SHA: ${{ needs.prepare.outputs.source-sha }} GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} GH_TOKEN: ${{ steps.app-token.outputs.token }} @@ -215,5 +206,5 @@ jobs: --base main \ --head "$branch" \ --title "Bump vscode-typescript to $EXTENSION_VERSION" \ - --body "Updates the vscode-typescript extension to $EXTENSION_VERSION and bundles TypeScript $TYPESCRIPT_VERSION." + --body "Updates the vscode-typescript extension to $EXTENSION_VERSION." fi diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml index c8ae85a878fcf..628797189d947 100644 --- a/.github/workflows/tag-vscode-typescript.yml +++ b/.github/workflows/tag-vscode-typescript.yml @@ -3,12 +3,20 @@ name: Tag vscode-typescript release on: workflow_dispatch: inputs: - extension_version: - description: Reviewed extension version to release + major: + description: Extension major version required: true - type: string + type: number + minor: + description: Extension minor version + required: true + type: number + patch: + description: Extension patch version + required: true + type: number -run-name: Tag vscode-typescript/v${{ inputs.extension_version }} +run-name: Tag vscode-typescript/v${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} permissions: contents: read @@ -36,7 +44,7 @@ jobs: - name: Validate release version env: - EXTENSION_VERSION: ${{ inputs.extension_version }} + EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} run: | set -euo pipefail versionPattern='^[0-9]+\.[0-9]+\.[0-9]+$' @@ -80,7 +88,7 @@ jobs: - name: Create release tag env: - EXTENSION_VERSION: ${{ inputs.extension_version }} + EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -euo pipefail diff --git a/tools/pipelines/vscode-typescript-build.yml b/tools/pipelines/vscode-typescript-build.yml index a3f3bc0d9b6be..07d3127cef358 100644 --- a/tools/pipelines/vscode-typescript-build.yml +++ b/tools/pipelines/vscode-typescript-build.yml @@ -5,7 +5,7 @@ trigger: pr: none -name: vscode-typescript-$(Date:yyyyMMdd)$(Rev:.r) +name: TypeScript-VS-Code-Extension-Release-Build-$(Date:yyyyMMdd)$(Rev:.r) appendCommitMessageToRunName: false variables: diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index dc57fe93569ec..b544127d5c223 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -1,6 +1,6 @@ trigger: none -name: vscode-typescript-publish-$(Date:yyyyMMdd)$(Rev:.r) +name: TypeScript-VS-Code-Extension-Release-Publish-$(Date:yyyyMMdd)$(Rev:.r) appendCommitMessageToRunName: false parameters: @@ -20,9 +20,9 @@ resources: name: 1ESPipelineTemplates/MicroBuildTemplate ref: refs/tags/release pipelines: - - pipeline: VSCode_TypeScript_Release_Build + - pipeline: TypeScript_VS_Code_Extension_Release_Build project: DevDiv - source: VSCode TypeScript Release Build + source: TypeScript VS Code Extension Release Build trigger: tags: - vscode-typescript-release @@ -52,7 +52,7 @@ extends: isProduction: true inputs: - input: pipelineArtifact - pipeline: VSCode_TypeScript_Release_Build + pipeline: TypeScript_VS_Code_Extension_Release_Build artifactName: vsix targetPath: $(Pipeline.Workspace)/vsix @@ -146,8 +146,8 @@ extends: fi displayName: Validate signed release artifacts env: - RELEASE_SOURCE_BRANCH: $(resources.pipeline.VSCode_TypeScript_Release_Build.sourceBranch) - RELEASE_SOURCE_COMMIT: $(resources.pipeline.VSCode_TypeScript_Release_Build.sourceCommit) + RELEASE_SOURCE_BRANCH: $(resources.pipeline.TypeScript_VS_Code_Extension_Release_Build.sourceBranch) + RELEASE_SOURCE_COMMIT: $(resources.pipeline.TypeScript_VS_Code_Extension_Release_Build.sourceCommit) - task: NodeTool@0 inputs: From 4844ad5ac10ee1151783b7252181126dcdb1d1e1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:46:01 -0700 Subject: [PATCH 07/24] Automate VS Code extension release tagging --- .github/workflows/bump-vscode-typescript.yml | 63 +++---- .github/workflows/tag-vscode-typescript.yml | 169 ++++++++++++------- package-lock.json | 2 +- packages/vscode-typescript/package.json | 2 +- 4 files changed, 130 insertions(+), 106 deletions(-) diff --git a/.github/workflows/bump-vscode-typescript.yml b/.github/workflows/bump-vscode-typescript.yml index 1fdcc7df04fcc..0bf9bf20bb51c 100644 --- a/.github/workflows/bump-vscode-typescript.yml +++ b/.github/workflows/bump-vscode-typescript.yml @@ -3,20 +3,16 @@ name: Bump vscode-typescript on: workflow_dispatch: inputs: - major: - description: Extension major version + bump: + description: Version component to bump required: true - type: number - minor: - description: Extension minor version - required: true - type: number - patch: - description: Extension patch version - required: true - type: number + type: choice + options: + - patch + - minor + - major -run-name: Bump vscode-typescript to ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} +run-name: Bump vscode-typescript (${{ inputs.bump }}) permissions: contents: read @@ -48,26 +44,30 @@ jobs: with: node-version: 'lts/*' - - name: Validate extension version + - run: npm ci + + - name: Update extension version env: - EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} + BUMP: ${{ inputs.bump }} run: | set -euo pipefail - versionPattern='^[0-9]+\.[0-9]+\.[0-9]+$' - if ! [[ "$EXTENSION_VERSION" =~ $versionPattern ]] || [ "$EXTENSION_VERSION" = "0.0.0" ]; then - echo "Extension version must be a non-placeholder three-component numeric version." >&2 + npm version "$BUMP" \ + --workspace native-preview \ + --no-git-tag-version \ + + packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" + lockVersion="$(jq -r '.packages["packages/vscode-typescript"].version' package-lock.json)" + if [ "$packageVersion" != "$lockVersion" ]; then + echo "package.json version $packageVersion does not match package-lock.json version $lockVersion." >&2 exit 1 fi - - run: npm ci - - name: Check Marketplace version - env: - EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} run: | set -euo pipefail + packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" publishedVersion="$(npx vsce show TypeScriptTeam.native-preview --json | jq -r '.versions[0].version')" - PUBLISHED_VERSION="$publishedVersion" node <<'NODE' + EXTENSION_VERSION="$packageVersion" PUBLISHED_VERSION="$publishedVersion" node <<'NODE' const extensionVersion = process.env.EXTENSION_VERSION.split(".").map(Number); const publishedVersion = process.env.PUBLISHED_VERSION.split(".").map(Number); const comparison = extensionVersion.findIndex((part, index) => part !== publishedVersion[index]); @@ -76,23 +76,6 @@ jobs: } NODE - - name: Update extension version - env: - EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} - run: | - set -euo pipefail - npm version "$EXTENSION_VERSION" \ - --workspace native-preview \ - --no-git-tag-version \ - --allow-same-version - - packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" - lockVersion="$(jq -r '.packages["packages/vscode-typescript"].version' package-lock.json)" - if [ "$packageVersion" != "$lockVersion" ]; then - echo "package.json version $packageVersion does not match package-lock.json version $lockVersion." >&2 - exit 1 - fi - - run: npm test -w native-preview - name: Package extension @@ -162,12 +145,12 @@ jobs: - name: Commit, push, and open pull request env: - EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} SOURCE_SHA: ${{ needs.prepare.outputs.source-sha }} GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -euo pipefail + EXTENSION_VERSION="$(jq -r '.version' packages/vscode-typescript/package.json)" branch="vscode-typescript-release/v$EXTENSION_VERSION" git switch -c "$branch" git config user.email "290192711+typescript-automation[bot]@users.noreply.github.com" diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml index 628797189d947..6bd3d17708f31 100644 --- a/.github/workflows/tag-vscode-typescript.yml +++ b/.github/workflows/tag-vscode-typescript.yml @@ -1,22 +1,14 @@ name: Tag vscode-typescript release on: + pull_request_target: + types: [closed] + branches: [main] + paths: + - packages/vscode-typescript/package.json workflow_dispatch: - inputs: - major: - description: Extension major version - required: true - type: number - minor: - description: Extension minor version - required: true - type: number - patch: - description: Extension patch version - required: true - type: number - -run-name: Tag vscode-typescript/v${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} + +run-name: Tag vscode-typescript release permissions: contents: read @@ -28,48 +20,15 @@ defaults: jobs: tag: - if: github.repository == 'microsoft/TypeScript' + if: >- + github.repository == 'microsoft/TypeScript' && + (github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true) runs-on: ubuntu-latest environment: name: azure deployment: false steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: main - filter: blob:none - fetch-depth: 0 - persist-credentials: false - - - name: Validate release version - env: - EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} - run: | - set -euo pipefail - versionPattern='^[0-9]+\.[0-9]+\.[0-9]+$' - if ! [[ "$EXTENSION_VERSION" =~ $versionPattern ]] || [ "$EXTENSION_VERSION" = "0.0.0" ]; then - echo "Extension version must be a non-placeholder three-component numeric version." >&2 - exit 1 - fi - - packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" - if [ "$EXTENSION_VERSION" != "$packageVersion" ]; then - echo "Requested version $EXTENSION_VERSION does not match main's package version $packageVersion." >&2 - exit 1 - fi - lockVersion="$(jq -r '.packages["packages/vscode-typescript"].version' package-lock.json)" - if [ "$packageVersion" != "$lockVersion" ]; then - echo "package.json version $packageVersion does not match package-lock.json version $lockVersion." >&2 - exit 1 - fi - - tag="vscode-typescript/v$EXTENSION_VERSION" - if git rev-parse --verify --quiet "refs/tags/$tag"; then - echo "Tag $tag already exists." >&2 - exit 1 - fi - - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: ${{ vars.AZURE_CLIENT_ID }} @@ -87,17 +46,99 @@ jobs: permission-contents: write - name: Create release tag + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - EXTENSION_VERSION: ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} - GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - set -euo pipefail - tag="vscode-typescript/v$EXTENSION_VERSION" - git config user.email "290192711+typescript-automation[bot]@users.noreply.github.com" - git config user.name "typescript-automation[bot]" - git tag --annotate "$tag" --message "vscode-typescript $EXTENSION_VERSION" - - basic_auth="$(node -e 'process.stdout.write(Buffer.from("x-access-token:" + process.env.GITHUB_APP_TOKEN).toString("base64"))')" - echo "::add-mask::$basic_auth" - git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}" - git push origin "refs/tags/$tag" + EVENT_NAME: ${{ github.event_name }} + DISPATCH_COMMIT_SHA: ${{ github.sha }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + MERGE_COMMIT_SHA: ${{ github.event.pull_request.merge_commit_sha }} + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + const { + EVENT_NAME, + DISPATCH_COMMIT_SHA, + PR_BASE_SHA, + MERGE_COMMIT_SHA, + } = process.env; + + const owner = context.repo.owner; + const repo = context.repo.repo; + const releaseCommit = EVENT_NAME === "workflow_dispatch" + ? DISPATCH_COMMIT_SHA + : MERGE_COMMIT_SHA; + + if (!releaseCommit) { + throw new Error("Could not determine the release commit."); + } + + const readJson = async (path, ref) => { + const response = await github.rest.repos.getContent({ + owner, + repo, + path, + ref, + }); + if (Array.isArray(response.data) || response.data.type !== "file") { + throw new Error(`${path} is not a file.`); + } + return JSON.parse(Buffer.from(response.data.content, "base64").toString()); + }; + + const packagePath = "packages/vscode-typescript/package.json"; + const packageJson = await readJson(packagePath, releaseCommit); + const version = packageJson.version; + if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version) || version === "0.0.0") { + throw new Error(`Invalid extension version: ${JSON.stringify(version)}.`); + } + + if (EVENT_NAME !== "workflow_dispatch") { + if (!PR_BASE_SHA) { + throw new Error("Could not determine the pull request base commit."); + } + + const previousPackageJson = await readJson(packagePath, PR_BASE_SHA); + if (previousPackageJson.version === version) { + console.log(`${packagePath} changed without changing its version.`); + return; + } + } + + const packageLock = await readJson("package-lock.json", releaseCommit); + const lockVersion = packageLock.packages?.["packages/vscode-typescript"]?.version; + if (lockVersion !== version) { + throw new Error(`package.json version ${version} does not match package-lock.json version ${lockVersion}.`); + } + + const tag = `vscode-typescript/v${version}`; + try { + await github.rest.git.getRef({ + owner, + repo, + ref: `tags/${tag}`, + }); + throw new Error(`Tag ${tag} already exists.`); + } + catch (error) { + if (error.status !== 404) throw error; + } + + const annotatedTag = await github.rest.git.createTag({ + owner, + repo, + tag, + message: `vscode-typescript ${version}`, + object: releaseCommit, + type: "commit", + tagger: { + name: "typescript-automation[bot]", + email: "290192711+typescript-automation[bot]@users.noreply.github.com", + }, + }); + + await github.rest.git.createRef({ + owner, + repo, + ref: `refs/tags/${tag}`, + sha: annotatedTag.data.sha, + }); diff --git a/package-lock.json b/package-lock.json index 6bfed937d972a..f704fa77059e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5505,7 +5505,7 @@ }, "packages/vscode-typescript": { "name": "native-preview", - "version": "0.0.0", + "version": "1.0.0", "dependencies": { "@vscode/extension-telemetry": "^1.5.2", "vscode-languageclient": "^10.1.0", diff --git a/packages/vscode-typescript/package.json b/packages/vscode-typescript/package.json index 3be3c59bd7b72..d4a01f47b2104 100644 --- a/packages/vscode-typescript/package.json +++ b/packages/vscode-typescript/package.json @@ -6,7 +6,7 @@ "description": "%description%", "icon": "logo.png", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "commonjs", "l10n": "./l10n", "repository": { From 986fbb6af15ee447ac824e2ea9c2f4826eb9987f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:46:32 -0700 Subject: [PATCH 08/24] Restrict manual release tags to main --- .github/workflows/tag-vscode-typescript.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml index 6bd3d17708f31..fa5d7d80c1c17 100644 --- a/.github/workflows/tag-vscode-typescript.yml +++ b/.github/workflows/tag-vscode-typescript.yml @@ -22,7 +22,10 @@ jobs: tag: if: >- github.repository == 'microsoft/TypeScript' && - (github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true) + ( + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || + github.event.pull_request.merged == true + ) runs-on: ubuntu-latest environment: name: azure @@ -49,6 +52,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: EVENT_NAME: ${{ github.event_name }} + DISPATCH_REF: ${{ github.ref }} DISPATCH_COMMIT_SHA: ${{ github.sha }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} MERGE_COMMIT_SHA: ${{ github.event.pull_request.merge_commit_sha }} @@ -57,6 +61,7 @@ jobs: script: | const { EVENT_NAME, + DISPATCH_REF, DISPATCH_COMMIT_SHA, PR_BASE_SHA, MERGE_COMMIT_SHA, @@ -64,6 +69,9 @@ jobs: const owner = context.repo.owner; const repo = context.repo.repo; + if (EVENT_NAME === "workflow_dispatch" && DISPATCH_REF !== "refs/heads/main") { + throw new Error(`Manual releases must be dispatched from main, got ${DISPATCH_REF}.`); + } const releaseCommit = EVENT_NAME === "workflow_dispatch" ? DISPATCH_COMMIT_SHA : MERGE_COMMIT_SHA; From 7a8559ffab0a4a8fb866df445dd64cfa572334aa Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:48:49 -0700 Subject: [PATCH 09/24] Simplify automatic extension release tagging --- .github/workflows/tag-vscode-typescript.yml | 158 +++++++------------- 1 file changed, 53 insertions(+), 105 deletions(-) diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml index fa5d7d80c1c17..3a5d8362eb34a 100644 --- a/.github/workflows/tag-vscode-typescript.yml +++ b/.github/workflows/tag-vscode-typescript.yml @@ -6,7 +6,6 @@ on: branches: [main] paths: - packages/vscode-typescript/package.json - workflow_dispatch: run-name: Tag vscode-typescript release @@ -22,23 +21,57 @@ jobs: tag: if: >- github.repository == 'microsoft/TypeScript' && - ( - (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || - github.event.pull_request.merged == true - ) + github.event.pull_request.merged == true runs-on: ubuntu-latest environment: name: azure deployment: false steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + filter: blob:none + fetch-depth: 2 + persist-credentials: false + + - name: Check for extension version bump + id: version + run: | + set -euo pipefail + packagePath="packages/vscode-typescript/package.json" + previousVersion="$(git show "HEAD^:$packagePath" | jq -r '.version')" + version="$(jq -r '.version' "$packagePath")" + + if [ "$previousVersion" = "$version" ]; then + echo "$packagePath changed without changing its version." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || [ "$version" = "0.0.0" ]; then + echo "Invalid extension version: $version" >&2 + exit 1 + fi + + lockVersion="$(jq -r '.packages["packages/vscode-typescript"].version' package-lock.json)" + if [ "$version" != "$lockVersion" ]; then + echo "package.json version $version does not match package-lock.json version $lockVersion." >&2 + exit 1 + fi + + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + if: steps.version.outputs.changed == 'true' with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - name: Create GitHub App token + if: steps.version.outputs.changed == 'true' id: app-token uses: microsoft/create-github-app-token-via-key-vault@5ba0d436e9c3cac52feff4d1f2f66f9698ce4a2d # v1 with: @@ -49,104 +82,19 @@ jobs: permission-contents: write - name: Create release tag - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + if: steps.version.outputs.changed == 'true' env: - EVENT_NAME: ${{ github.event_name }} - DISPATCH_REF: ${{ github.ref }} - DISPATCH_COMMIT_SHA: ${{ github.sha }} - PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} - MERGE_COMMIT_SHA: ${{ github.event.pull_request.merge_commit_sha }} - with: - github-token: ${{ steps.app-token.outputs.token }} - script: | - const { - EVENT_NAME, - DISPATCH_REF, - DISPATCH_COMMIT_SHA, - PR_BASE_SHA, - MERGE_COMMIT_SHA, - } = process.env; - - const owner = context.repo.owner; - const repo = context.repo.repo; - if (EVENT_NAME === "workflow_dispatch" && DISPATCH_REF !== "refs/heads/main") { - throw new Error(`Manual releases must be dispatched from main, got ${DISPATCH_REF}.`); - } - const releaseCommit = EVENT_NAME === "workflow_dispatch" - ? DISPATCH_COMMIT_SHA - : MERGE_COMMIT_SHA; - - if (!releaseCommit) { - throw new Error("Could not determine the release commit."); - } - - const readJson = async (path, ref) => { - const response = await github.rest.repos.getContent({ - owner, - repo, - path, - ref, - }); - if (Array.isArray(response.data) || response.data.type !== "file") { - throw new Error(`${path} is not a file.`); - } - return JSON.parse(Buffer.from(response.data.content, "base64").toString()); - }; - - const packagePath = "packages/vscode-typescript/package.json"; - const packageJson = await readJson(packagePath, releaseCommit); - const version = packageJson.version; - if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version) || version === "0.0.0") { - throw new Error(`Invalid extension version: ${JSON.stringify(version)}.`); - } - - if (EVENT_NAME !== "workflow_dispatch") { - if (!PR_BASE_SHA) { - throw new Error("Could not determine the pull request base commit."); - } - - const previousPackageJson = await readJson(packagePath, PR_BASE_SHA); - if (previousPackageJson.version === version) { - console.log(`${packagePath} changed without changing its version.`); - return; - } - } - - const packageLock = await readJson("package-lock.json", releaseCommit); - const lockVersion = packageLock.packages?.["packages/vscode-typescript"]?.version; - if (lockVersion !== version) { - throw new Error(`package.json version ${version} does not match package-lock.json version ${lockVersion}.`); - } - - const tag = `vscode-typescript/v${version}`; - try { - await github.rest.git.getRef({ - owner, - repo, - ref: `tags/${tag}`, - }); - throw new Error(`Tag ${tag} already exists.`); - } - catch (error) { - if (error.status !== 404) throw error; - } - - const annotatedTag = await github.rest.git.createTag({ - owner, - repo, - tag, - message: `vscode-typescript ${version}`, - object: releaseCommit, - type: "commit", - tagger: { - name: "typescript-automation[bot]", - email: "290192711+typescript-automation[bot]@users.noreply.github.com", - }, - }); - - await github.rest.git.createRef({ - owner, - repo, - ref: `refs/tags/${tag}`, - sha: annotatedTag.data.sha, - }); + EXTENSION_VERSION: ${{ steps.version.outputs.version }} + GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -euo pipefail + tag="vscode-typescript/v$EXTENSION_VERSION" + git config user.email "290192711+typescript-automation[bot]@users.noreply.github.com" + git config user.name "typescript-automation[bot]" + git config core.hooksPath /dev/null + git tag --annotate "$tag" --message "vscode-typescript $EXTENSION_VERSION" + + basic_auth="$(node -e 'process.stdout.write(Buffer.from("x-access-token:" + process.env.GITHUB_APP_TOKEN).toString("base64"))')" + echo "::add-mask::$basic_auth" + git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}" + git push origin "refs/tags/$tag" From 5dfbe43ebf41b66e874f6647fec187ac5c3fa9b5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:51:04 -0700 Subject: [PATCH 10/24] Make automatic release tagging idempotent --- .github/workflows/tag-vscode-typescript.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml index 3a5d8362eb34a..3581e5e200cbc 100644 --- a/.github/workflows/tag-vscode-typescript.yml +++ b/.github/workflows/tag-vscode-typescript.yml @@ -92,9 +92,21 @@ jobs: git config user.email "290192711+typescript-automation[bot]@users.noreply.github.com" git config user.name "typescript-automation[bot]" git config core.hooksPath /dev/null - git tag --annotate "$tag" --message "vscode-typescript $EXTENSION_VERSION" basic_auth="$(node -e 'process.stdout.write(Buffer.from("x-access-token:" + process.env.GITHUB_APP_TOKEN).toString("base64"))')" echo "::add-mask::$basic_auth" git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}" + + if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null; then + git fetch origin "refs/tags/$tag:refs/tags/$tag" + existingCommit="$(git rev-list -n 1 "$tag")" + if [ "$existingCommit" != "$(git rev-parse HEAD)" ]; then + echo "Tag $tag already exists at $existingCommit." >&2 + exit 1 + fi + echo "Tag $tag already exists at the release commit." + exit 0 + fi + + git tag --annotate "$tag" --message "vscode-typescript $EXTENSION_VERSION" git push origin "refs/tags/$tag" From e6a159b14eade22bb9700483ee46f02f6f3a16c6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:52:11 -0700 Subject: [PATCH 11/24] Serialize automatic release tagging --- .github/workflows/tag-vscode-typescript.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml index 3581e5e200cbc..51172b6b08f85 100644 --- a/.github/workflows/tag-vscode-typescript.yml +++ b/.github/workflows/tag-vscode-typescript.yml @@ -9,6 +9,10 @@ on: run-name: Tag vscode-typescript release +concurrency: + group: tag-vscode-typescript-${{ github.event.pull_request.merge_commit_sha }} + cancel-in-progress: false + permissions: contents: read id-token: write From 810aa9b21b02e3ecb865a04bf6d80ec216bf7aca Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:00:42 -0700 Subject: [PATCH 12/24] Remove redundant Marketplace version checks --- .github/workflows/bump-vscode-typescript.yml | 14 -------------- tools/pipelines/vscode-typescript-build.yml | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/.github/workflows/bump-vscode-typescript.yml b/.github/workflows/bump-vscode-typescript.yml index 0bf9bf20bb51c..65bbe96afc2ec 100644 --- a/.github/workflows/bump-vscode-typescript.yml +++ b/.github/workflows/bump-vscode-typescript.yml @@ -62,20 +62,6 @@ jobs: exit 1 fi - - name: Check Marketplace version - run: | - set -euo pipefail - packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" - publishedVersion="$(npx vsce show TypeScriptTeam.native-preview --json | jq -r '.versions[0].version')" - EXTENSION_VERSION="$packageVersion" PUBLISHED_VERSION="$publishedVersion" node <<'NODE' - const extensionVersion = process.env.EXTENSION_VERSION.split(".").map(Number); - const publishedVersion = process.env.PUBLISHED_VERSION.split(".").map(Number); - const comparison = extensionVersion.findIndex((part, index) => part !== publishedVersion[index]); - if (comparison === -1 || extensionVersion[comparison] < publishedVersion[comparison]) { - throw new Error(`Extension version ${process.env.EXTENSION_VERSION} must be greater than published version ${process.env.PUBLISHED_VERSION}.`); - } - NODE - - run: npm test -w native-preview - name: Package extension diff --git a/tools/pipelines/vscode-typescript-build.yml b/tools/pipelines/vscode-typescript-build.yml index 07d3127cef358..a1327699e7bdd 100644 --- a/tools/pipelines/vscode-typescript-build.yml +++ b/tools/pipelines/vscode-typescript-build.yml @@ -122,20 +122,6 @@ extends: - template: /tools/pipelines/steps/setup-node-npm-ci.yml@self - - bash: | - set -euo pipefail - packageVersion="$(jq -r '.version' packages/vscode-typescript/package.json)" - publishedVersion="$(npx vsce show TypeScriptTeam.native-preview --json | jq -r '.versions[0].version')" - EXTENSION_VERSION="$packageVersion" PUBLISHED_VERSION="$publishedVersion" node <<'NODE' - const extensionVersion = process.env.EXTENSION_VERSION.split(".").map(Number); - const publishedVersion = process.env.PUBLISHED_VERSION.split(".").map(Number); - const comparison = extensionVersion.findIndex((part, index) => part !== publishedVersion[index]); - if (comparison === -1 || extensionVersion[comparison] < publishedVersion[comparison]) { - throw new Error(`Extension version ${process.env.EXTENSION_VERSION} must be greater than published version ${process.env.PUBLISHED_VERSION}.`); - } - NODE - displayName: Check Marketplace version - - bash: npm test -w native-preview displayName: Test extension From f4a7b90733ec0e85c3264b0f81986816ab339f53 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:02:07 -0700 Subject: [PATCH 13/24] Remove obsolete placeholder version checks --- .github/workflows/tag-vscode-typescript.yml | 2 +- Herebyfile.mjs | 3 --- tools/pipelines/vscode-typescript-build.yml | 4 ++-- tools/pipelines/vscode-typescript-publish.yml | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml index 51172b6b08f85..b78f542da247f 100644 --- a/.github/workflows/tag-vscode-typescript.yml +++ b/.github/workflows/tag-vscode-typescript.yml @@ -53,7 +53,7 @@ jobs: exit 0 fi - if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || [ "$version" = "0.0.0" ]; then + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Invalid extension version: $version" >&2 exit 1 fi diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 9072879d3db32..39ebd11ff6f53 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -1712,9 +1712,6 @@ function getVscodeTypeScriptExtensionVersion() { if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version)) { throw new Error(`packages/vscode-typescript/package.json must contain a three-component numeric version, got ${JSON.stringify(version)}.`); } - if (version === "0.0.0") { - throw new Error("Refusing to release vscode-typescript with placeholder version 0.0.0."); - } return version; } diff --git a/tools/pipelines/vscode-typescript-build.yml b/tools/pipelines/vscode-typescript-build.yml index a1327699e7bdd..f8b27bb07195c 100644 --- a/tools/pipelines/vscode-typescript-build.yml +++ b/tools/pipelines/vscode-typescript-build.yml @@ -87,8 +87,8 @@ extends: echo "package.json version $packageVersion does not match package-lock.json version $lockVersion." >&2 exit 1 fi - if ! [[ "$packageVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || [ "$packageVersion" = "0.0.0" ]; then - echo "Extension version must be a non-placeholder three-component numeric version." >&2 + if ! [[ "$packageVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Extension version must be a three-component numeric version." >&2 exit 1 fi diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index b544127d5c223..4337cb8404056 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -75,7 +75,7 @@ extends: echo "Unexpected extension identity: $extension" >&2 exit 1 fi - if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || [ "$version" = "0.0.0" ]; then + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Invalid extension version: $version" >&2 exit 1 fi From da4356690a83d37dcb81d860ab37b3040c2a90e6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:10:49 -0700 Subject: [PATCH 14/24] Add manual extension publish approval --- tools/pipelines/vscode-typescript-publish.yml | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index 4337cb8404056..f2f437b3142b7 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -44,12 +44,10 @@ extends: - stage: Publish displayName: Publish vscode-typescript jobs: - - job: Publish - displayName: Publish vscode-typescript + - job: Validate + displayName: Validate vscode-typescript release templateContext: - type: releaseJob - isProduction: true inputs: - input: pipelineArtifact pipeline: TypeScript_VS_Code_Extension_Release_Build @@ -180,7 +178,61 @@ extends: az rest -u https://app.vssps.visualstudio.com/_apis/profile/profiles/me --resource 499b84ac-1321-427f-aa17-267ca6975798 npx vsce verify-pat TypeScriptTeam --azure-credential - - ${{ if eq(parameters.dryRun, false) }}: + - ${{ if eq(parameters.dryRun, false) }}: + - job: Approve + displayName: Approve production publication + dependsOn: Validate + pool: server + timeoutInMinutes: 5760 + steps: + - task: ManualValidation@1 + timeoutInMinutes: 4320 + inputs: + notifyUsers: '[DevDiv]\TypeScript Team' + approvers: '[DevDiv]\TypeScript Team' + allowApproversToApproveTheirOwnRuns: true + instructions: | + Review the validated source tag, commit, extension version, + bundled TypeScript version, target list, and signing results. + Resume to publish the VSIXs to the Marketplace and GitHub Release. + onTimeout: reject + + - job: Publish + displayName: Publish vscode-typescript + dependsOn: Approve + + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: TypeScript_VS_Code_Extension_Release_Build + artifactName: vsix + targetPath: $(Pipeline.Workspace)/vsix + + steps: + - checkout: none + + - task: NodeTool@0 + inputs: + versionSpec: 24.x + displayName: Install Node + + - bash: | + cat > .npmrc << 'EOF' + registry=https://pkgs.dev.azure.com/devdiv/devdiv/_packaging/devdiv_PublicPackages/npm/registry/ + EOF + npm init -y + displayName: Set up npm + + - task: npmAuthenticate@0 + inputs: + workingFile: .npmrc + displayName: Authenticate npm + + - bash: npm install @vscode/vsce@3.9.2 + displayName: Install vsce + - task: AzureCLI@2 displayName: Publish VSIXs to Marketplace retryCountOnTaskFailure: 3 From fd480d6ec05e1be5f0490b98ff7829662e014cc9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:55:12 -0700 Subject: [PATCH 15/24] Use existing TypeScript approval team --- tools/pipelines/vscode-typescript-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index f2f437b3142b7..0571fa148590b 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -188,8 +188,8 @@ extends: - task: ManualValidation@1 timeoutInMinutes: 4320 inputs: - notifyUsers: '[DevDiv]\TypeScript Team' - approvers: '[DevDiv]\TypeScript Team' + notifyUsers: '[DevDiv]\JSTSteam' + approvers: '[DevDiv]\JSTSteam' allowApproversToApproveTheirOwnRuns: true instructions: | Review the validated source tag, commit, extension version, From 4e2c4016be0ea72cbdfd619eeb547b5e44065e4e Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:07:21 -0700 Subject: [PATCH 16/24] Remove stale Marketplace auth precheck --- tools/pipelines/vscode-typescript-publish.yml | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index 0571fa148590b..bfdeb2e96cb50 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -147,37 +147,6 @@ extends: RELEASE_SOURCE_BRANCH: $(resources.pipeline.TypeScript_VS_Code_Extension_Release_Build.sourceBranch) RELEASE_SOURCE_COMMIT: $(resources.pipeline.TypeScript_VS_Code_Extension_Release_Build.sourceCommit) - - task: NodeTool@0 - inputs: - versionSpec: 24.x - displayName: Install Node - - - bash: | - cat > .npmrc << 'EOF' - registry=https://pkgs.dev.azure.com/devdiv/devdiv/_packaging/devdiv_PublicPackages/npm/registry/ - EOF - npm init -y - displayName: Set up npm - - - task: npmAuthenticate@0 - inputs: - workingFile: .npmrc - displayName: Authenticate npm - - - bash: npm install @vscode/vsce@3.9.2 - displayName: Install vsce - - - task: AzureCLI@2 - displayName: Check Marketplace authentication - inputs: - azureSubscription: TypeScript-VSMarketplacePublishAuth - scriptType: bash - scriptLocation: inlineScript - inlineScript: | - set -euo pipefail - az rest -u https://app.vssps.visualstudio.com/_apis/profile/profiles/me --resource 499b84ac-1321-427f-aa17-267ca6975798 - npx vsce verify-pat TypeScriptTeam --azure-credential - - ${{ if eq(parameters.dryRun, false) }}: - job: Approve displayName: Approve production publication From bbdce346dd045add61b0251dfc6b3c2f4b06b327 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:27:02 -0700 Subject: [PATCH 17/24] Use DevDiv Key Vault for GitHub releases --- tools/pipelines/vscode-typescript-publish.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index bfdeb2e96cb50..52f036a719a84 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -12,6 +12,10 @@ parameters: variables: - name: TeamName value: TypeScript + - name: TYPESCRIPT_AUTOMATION_GITHUB_APP_CLIENT_ID + value: Iv23li4GolzJSEp1mzHI + - name: TYPESCRIPT_AUTOMATION_GITHUB_APP_KEY_ID + value: https://jststeam-passwords.vault.azure.net/keys/typescript-automation resources: repositories: @@ -226,6 +230,7 @@ extends: - template: /tools/pipelines/steps/create-github-app-token.yml@self parameters: + azureSubscription: TypeScript-DevDiv-KeyVault repositories: TypeScript permissions: contents:write insertSteps: From 7464a7ae95cca2648e3e1b56c93605223f3741d2 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:55:55 -0700 Subject: [PATCH 18/24] Use central npm package proxy in Azure --- tools/pipelines/steps/setup-node-npm-ci.yml | 7 +------ tools/pipelines/typescript-publish.yml | 8 +------- tools/pipelines/vscode-typescript-publish.yml | 7 +------ 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/tools/pipelines/steps/setup-node-npm-ci.yml b/tools/pipelines/steps/setup-node-npm-ci.yml index c358c9301b8ef..82891ca1db3ee 100755 --- a/tools/pipelines/steps/setup-node-npm-ci.yml +++ b/tools/pipelines/steps/setup-node-npm-ci.yml @@ -6,15 +6,10 @@ steps: - bash: | cat > .npmrc << 'EOF' - registry=https://pkgs.dev.azure.com/devdiv/devdiv/_packaging/devdiv_PublicPackages/npm/registry/ + registry=https://packagefeedproxy.microsoft.io/npm/ EOF displayName: 'Set up .npmrc' - - task: npmAuthenticate@0 - inputs: - workingFile: .npmrc - displayName: 'Authenticate npm' - - pwsh: | npm install -g (Get-Content package.json | ConvertFrom-Json).packageManager npm --version diff --git a/tools/pipelines/typescript-publish.yml b/tools/pipelines/typescript-publish.yml index b85898c865e99..1ef96f2292f70 100755 --- a/tools/pipelines/typescript-publish.yml +++ b/tools/pipelines/typescript-publish.yml @@ -189,18 +189,12 @@ extends: - bash: | cat > .npmrc << 'EOF' - registry=https://pkgs.dev.azure.com/devdiv/devdiv/_packaging/devdiv_PublicPackages/npm/registry/ + registry=https://packagefeedproxy.microsoft.io/npm/ EOF npm init -y displayName: 'Set up .npmrc' condition: and(succeeded(), eq(variables['HasVsix'], 'true')) - - task: npmAuthenticate@0 - condition: and(succeeded(), eq(variables['HasVsix'], 'true')) - inputs: - workingFile: .npmrc - displayName: 'Authenticate npm' - - bash: npm install @vscode/vsce@latest displayName: 'Install vsce' condition: and(succeeded(), eq(variables['HasVsix'], 'true')) diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index 52f036a719a84..592f522ab66a9 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -193,16 +193,11 @@ extends: - bash: | cat > .npmrc << 'EOF' - registry=https://pkgs.dev.azure.com/devdiv/devdiv/_packaging/devdiv_PublicPackages/npm/registry/ + registry=https://packagefeedproxy.microsoft.io/npm/ EOF npm init -y displayName: Set up npm - - task: npmAuthenticate@0 - inputs: - workingFile: .npmrc - displayName: Authenticate npm - - bash: npm install @vscode/vsce@3.9.2 displayName: Install vsce From 73da4b213ecbea66e8abd70e4b46e53f49c5f122 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:07:27 -0700 Subject: [PATCH 19/24] Share pinned vsce release setup --- .github/workflows/ci.yml | 1 + Herebyfile.mjs | 27 +++++++++++++++++++ package-lock.json | 2 +- packages/vscode-typescript/package.json | 2 +- tools/pipelines/steps/setup-vsce.yml | 23 ++++++++++++++++ tools/pipelines/typescript-publish.yml | 20 +++----------- tools/pipelines/vscode-typescript-publish.yml | 15 +---------- 7 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 tools/pipelines/steps/setup-vsce.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a864147929cf..f0a155b13af7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -331,6 +331,7 @@ jobs: - run: npm ci - run: go -C ./tools run ./cmd/checkmodpaths "$PWD" - run: npx hereby check:herebyfile + - run: npx hereby check:vsce-version - run: npx hereby check:scripts - run: npx hereby typescript:check-platforms diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 39ebd11ff6f53..654745f981444 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -1403,6 +1403,33 @@ export const checkHerebyfile = task({ ]), }); +export const checkVsceVersion = task({ + name: "check:vsce-version", + description: "Checks that Azure release jobs use the repository's pinned vsce version.", + run: () => { + const packageJson = JSON.parse(fs.readFileSync("./packages/vscode-typescript/package.json", "utf8")); + const packageLock = JSON.parse(fs.readFileSync("./package-lock.json", "utf8")); + const version = packageJson.devDependencies?.["@vscode/vsce"]; + if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version)) { + throw new Error(`packages/vscode-typescript must pin @vscode/vsce to an exact version, got ${JSON.stringify(version)}.`); + } + + const workspaceVersion = packageLock.packages?.["packages/vscode-typescript"]?.devDependencies?.["@vscode/vsce"]; + const installedVersion = packageLock.packages?.["node_modules/@vscode/vsce"]?.version; + if (workspaceVersion !== version || installedVersion !== version) { + throw new Error( + `@vscode/vsce version mismatch: package.json=${version}, package-lock workspace=${workspaceVersion}, package-lock package=${installedVersion}.`, + ); + } + + const setupVsce = fs.readFileSync("./tools/pipelines/steps/setup-vsce.yml", "utf8"); + const matches = [...setupVsce.matchAll(/npm install --no-save @vscode\/vsce@(\d+\.\d+\.\d+)/g)]; + if (matches.length !== 1 || matches[0][1] !== version) { + throw new Error(`tools/pipelines/steps/setup-vsce.yml must install exactly @vscode/vsce@${version}.`); + } + }, +}); + const scriptTsconfigs = [ "./tools/scripts/tsc/tsconfig.json", "./tsc/internal/lsp/lsproto/_generate/tsconfig.json", diff --git a/package-lock.json b/package-lock.json index f704fa77059e6..a98eff3e2f81a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5514,7 +5514,7 @@ "devDependencies": { "@types/vscode": "~1.125.0", "@typescript/bundled-typescript": "npm:typescript@7.0.2", - "@vscode/vsce": "^3.9.2", + "@vscode/vsce": "3.9.2", "esbuild": "^0.28.2" }, "engines": { diff --git a/packages/vscode-typescript/package.json b/packages/vscode-typescript/package.json index d4a01f47b2104..5fbead2166487 100644 --- a/packages/vscode-typescript/package.json +++ b/packages/vscode-typescript/package.json @@ -385,7 +385,7 @@ "devDependencies": { "@types/vscode": "~1.125.0", "@typescript/bundled-typescript": "npm:typescript@7.0.2", - "@vscode/vsce": "^3.9.2", + "@vscode/vsce": "3.9.2", "esbuild": "^0.28.2" } } diff --git a/tools/pipelines/steps/setup-vsce.yml b/tools/pipelines/steps/setup-vsce.yml new file mode 100644 index 0000000000000..afac1da9d23b5 --- /dev/null +++ b/tools/pipelines/steps/setup-vsce.yml @@ -0,0 +1,23 @@ +parameters: + - name: condition + type: string + default: succeeded() + +steps: + - task: NodeTool@0 + condition: ${{ parameters.condition }} + inputs: + versionSpec: 24.x + displayName: 'Install Node' + + - bash: | + cat > .npmrc << 'EOF' + registry=https://packagefeedproxy.microsoft.io/npm/ + EOF + npm init -y + condition: ${{ parameters.condition }} + displayName: 'Set up npm' + + - bash: npm install --no-save @vscode/vsce@3.9.2 + condition: ${{ parameters.condition }} + displayName: 'Install vsce' diff --git a/tools/pipelines/typescript-publish.yml b/tools/pipelines/typescript-publish.yml index 1ef96f2292f70..07a6bb74fb855 100755 --- a/tools/pipelines/typescript-publish.yml +++ b/tools/pipelines/typescript-publish.yml @@ -181,23 +181,9 @@ extends: fi displayName: 'Check VSIX artifacts' - - task: NodeTool@0 - condition: and(succeeded(), eq(variables['HasVsix'], 'true')) - inputs: - versionSpec: 24.x - displayName: 'Install Node' - - - bash: | - cat > .npmrc << 'EOF' - registry=https://packagefeedproxy.microsoft.io/npm/ - EOF - npm init -y - displayName: 'Set up .npmrc' - condition: and(succeeded(), eq(variables['HasVsix'], 'true')) - - - bash: npm install @vscode/vsce@latest - displayName: 'Install vsce' - condition: and(succeeded(), eq(variables['HasVsix'], 'true')) + - template: /tools/pipelines/steps/setup-vsce.yml@self + parameters: + condition: and(succeeded(), eq(variables['HasVsix'], 'true')) - task: AzureCLI@2 displayName: 'Check Marketplace Auth' diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index 592f522ab66a9..4d7aedbb74811 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -186,20 +186,7 @@ extends: steps: - checkout: none - - task: NodeTool@0 - inputs: - versionSpec: 24.x - displayName: Install Node - - - bash: | - cat > .npmrc << 'EOF' - registry=https://packagefeedproxy.microsoft.io/npm/ - EOF - npm init -y - displayName: Set up npm - - - bash: npm install @vscode/vsce@3.9.2 - displayName: Install vsce + - template: /tools/pipelines/steps/setup-vsce.yml@self - task: AzureCLI@2 displayName: Publish VSIXs to Marketplace From 4aa9f3e57c4ce831fa6226924974b7a995618d3a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:09:23 -0700 Subject: [PATCH 20/24] Check release pipelines use pinned vsce --- Herebyfile.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 654745f981444..76199a8c2642c 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -1427,6 +1427,19 @@ export const checkVsceVersion = task({ if (matches.length !== 1 || matches[0][1] !== version) { throw new Error(`tools/pipelines/steps/setup-vsce.yml must install exactly @vscode/vsce@${version}.`); } + + for ( + const pipeline of [ + "./tools/pipelines/typescript-publish.yml", + "./tools/pipelines/vscode-typescript-publish.yml", + ] + ) { + const contents = fs.readFileSync(pipeline, "utf8"); + const templateReferences = contents.match(/\/tools\/pipelines\/steps\/setup-vsce\.yml@self/g) ?? []; + if (templateReferences.length !== 1 || contents.includes("@vscode/vsce")) { + throw new Error(`${pipeline} must use setup-vsce.yml exactly once and must not install @vscode/vsce directly.`); + } + } }, }); From fa6e27e16b1914958b190ba6e951dc8e42aa8179 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:11:50 -0700 Subject: [PATCH 21/24] Validate active vsce pipeline configuration --- Herebyfile.mjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 76199a8c2642c..a496bc96c1afa 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -1423,8 +1423,9 @@ export const checkVsceVersion = task({ } const setupVsce = fs.readFileSync("./tools/pipelines/steps/setup-vsce.yml", "utf8"); - const matches = [...setupVsce.matchAll(/npm install --no-save @vscode\/vsce@(\d+\.\d+\.\d+)/g)]; - if (matches.length !== 1 || matches[0][1] !== version) { + const activeSetupLines = setupVsce.split(/\r?\n/).filter(line => !line.trimStart().startsWith("#")); + const installCommand = `- bash: npm install --no-save @vscode/vsce@${version}`; + if (activeSetupLines.filter(line => line.trim() === installCommand).length !== 1) { throw new Error(`tools/pipelines/steps/setup-vsce.yml must install exactly @vscode/vsce@${version}.`); } @@ -1435,8 +1436,11 @@ export const checkVsceVersion = task({ ] ) { const contents = fs.readFileSync(pipeline, "utf8"); - const templateReferences = contents.match(/\/tools\/pipelines\/steps\/setup-vsce\.yml@self/g) ?? []; - if (templateReferences.length !== 1 || contents.includes("@vscode/vsce")) { + const activeLines = contents.split(/\r?\n/).filter(line => !line.trimStart().startsWith("#")); + const templateReferences = activeLines.filter( + line => line.trim() === "- template: /tools/pipelines/steps/setup-vsce.yml@self", + ); + if (templateReferences.length !== 1 || activeLines.some(line => line.includes("@vscode/vsce"))) { throw new Error(`${pipeline} must use setup-vsce.yml exactly once and must not install @vscode/vsce directly.`); } } From 744e7e3adf8907912edee0603b99a2e8c0af65de Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:03:49 -0700 Subject: [PATCH 22/24] Harden VS Code extension releases Fetch native package tarballs using the lockfile URLs and verify their\nSHA-512 integrity before packaging. Exercise the extension release path in\nCI and explicitly disable pull request triggers for the publish pipeline. --- .github/workflows/ci.yml | 1 + Herebyfile.mjs | 41 +++++++++++++++---- tools/pipelines/vscode-typescript-publish.yml | 2 + 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0a155b13af7d..851b17462a5d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,7 @@ jobs: - uses: ./.github/actions/setup-go - run: npm ci - run: npx hereby typescript:release --forRelease --setPrerelease dev.0.0 + - run: npx hereby vscode-typescript:release --forRelease --vscodeTypescriptRelease extension: runs-on: ubuntu-latest diff --git a/Herebyfile.mjs b/Herebyfile.mjs index a496bc96c1afa..cfdd27c0546d8 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -1748,6 +1748,10 @@ const builtPublishedPlatformPackages = path.resolve("./built/published-platform- const builtSignTmp = path.resolve("./built/sign-tmp"); const publishedTypeScriptAliasPackageName = "@typescript/bundled-typescript"; const releasePackageEnv = { COREPACK_ENABLE_STRICT: "0" }; +const getReleasePackageRegistry = memoize(async () => { + const { stdout } = await runOutput("npm", ["config", "get", "registry"], { env: releasePackageEnv }); + return stdout.trim(); +}); const getVscodeTypeScriptExtensionPackageJson = memoize(() => JSON.parse(fs.readFileSync(path.join(extensionDir, "package.json"), "utf8"))); @@ -2765,17 +2769,36 @@ async function getPublishedPlatformPackageLibDirWorker(npmPackageName) { if (!lockEntry.resolved || typeof lockEntry.resolved !== "string") { throw new Error(`package-lock.json entry for ${npmPackageName}@${version} does not contain a tarball URL.`); } + if (!lockEntry.integrity || typeof lockEntry.integrity !== "string") { + throw new Error(`package-lock.json entry for ${npmPackageName}@${version} does not contain an integrity hash.`); + } - console.log(`Fetching ${npmPackageName}@${version} with npm.`); - const { stdout } = await runOutput("npm", ["pack", "--json", `${npmPackageName}@${version}`], { - cwd: tarballDestination, - env: releasePackageEnv, - }); - const [packed] = JSON.parse(stdout); - if (!packed.filename || typeof packed.filename !== "string") { - throw new Error(`npm pack ${npmPackageName}@${version} did not return a filename.`); + const resolved = new URL(lockEntry.resolved); + if (resolved.origin === "https://registry.npmjs.org") { + const registry = new URL(await getReleasePackageRegistry()); + resolved.protocol = registry.protocol; + resolved.host = registry.host; + resolved.pathname = path.posix.join(registry.pathname, resolved.pathname); + } + + console.log(`Fetching locked ${npmPackageName}@${version} tarball from ${resolved}.`); + const response = await fetch(resolved); + if (!response.ok) { + throw new Error(`Failed to fetch ${npmPackageName}@${version}: ${response.status} ${response.statusText}.`); + } + const tarball = Buffer.from(await response.arrayBuffer()); + const integrityMatch = /^sha512-(.+)$/.exec(lockEntry.integrity); + if (!integrityMatch) { + throw new Error(`Unsupported integrity hash for ${npmPackageName}@${version}: ${lockEntry.integrity}.`); + } + const expectedIntegrity = Buffer.from(integrityMatch[1], "base64"); + const actualIntegrity = crypto.createHash("sha512").update(tarball).digest(); + if (expectedIntegrity.length !== actualIntegrity.length || !crypto.timingSafeEqual(expectedIntegrity, actualIntegrity)) { + throw new Error(`Integrity check failed for ${npmPackageName}@${version}.`); } - await tar.x({ file: path.join(tarballDestination, packed.filename), cwd: dest, strip: 1 }); + const tarballPath = path.join(tarballDestination, path.basename(resolved.pathname)); + await fs.promises.writeFile(tarballPath, tarball); + await tar.x({ file: tarballPath, cwd: dest, strip: 1 }); if (!fs.existsSync(lib)) { throw new Error(`Published platform package ${npmPackageName}@${version} did not contain a lib directory.`); diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index 4d7aedbb74811..f8db35a2f8e93 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -1,5 +1,7 @@ trigger: none +pr: none + name: TypeScript-VS-Code-Extension-Release-Publish-$(Date:yyyyMMdd)$(Rev:.r) appendCommitMessageToRunName: false From 51383b74f2e536d3ed76816ce3b4ed27246429de Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:43:08 -0700 Subject: [PATCH 23/24] Preserve existing extension release assets --- tools/pipelines/vscode-typescript-publish.yml | 92 ++++++++++++++++++- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index f8db35a2f8e93..d6c199ee64217 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -234,17 +234,99 @@ extends: exit 1 fi - if gh release view "$tag" --repo microsoft/TypeScript >/dev/null 2>&1; then - gh release upload "$tag" "${vsixFiles[@]}" \ + existingAssetsDirectory="$(mktemp -d)" + trap 'rm -rf "$existingAssetsDirectory"' EXIT + + refreshExistingAssetNames() { + local output + if ! output="$( + gh release view "$tag" \ + --repo microsoft/TypeScript \ + --json assets \ + --jq '.assets[].name' + )"; then + echo "Could not list assets for GitHub Release $tag." >&2 + return 1 + fi + mapfile -t existingAssetNames <<< "$output" + } + + assetExists() { + local filename="$1" + local existingAssetName + for existingAssetName in "${existingAssetNames[@]}"; do + if [ "$existingAssetName" = "$filename" ]; then + return 0 + fi + done + return 1 + } + + verifyExistingAsset() { + local filename="$1" + local expectedHash="$2" + local existingHash + + gh release download "$tag" \ --repo microsoft/TypeScript \ + --pattern "$filename" \ + --dir "$existingAssetsDirectory" \ --clobber - else - gh release create "$tag" "${vsixFiles[@]}" \ + existingHash="$(sha256sum "$existingAssetsDirectory/$filename" | cut -d' ' -f1)" + if [ "$existingHash" != "$expectedHash" ]; then + echo "Existing GitHub Release asset $filename does not match the release manifest." >&2 + return 1 + fi + echo "Existing GitHub Release asset $filename matches the release manifest." + } + + syncReleaseAssets() { + local vsixFilePath + local filename + local expectedHash + local -a missingVsixFiles=() + + refreshExistingAssetNames + for vsixFilePath in "${vsixFiles[@]}"; do + filename="$(basename "$vsixFilePath")" + expectedHash="$(jq -r --arg filename "$filename" '.artifacts[$filename].sha256 // empty' "$manifest")" + if [ -z "$expectedHash" ]; then + echo "No manifest hash found for $filename." >&2 + exit 1 + fi + + if assetExists "$filename"; then + verifyExistingAsset "$filename" "$expectedHash" + else + missingVsixFiles+=("$vsixFilePath") + fi + done + + for vsixFilePath in "${missingVsixFiles[@]}"; do + filename="$(basename "$vsixFilePath")" + expectedHash="$(jq -r --arg filename "$filename" '.artifacts[$filename].sha256' "$manifest")" + if ! gh release upload "$tag" "$vsixFilePath" --repo microsoft/TypeScript; then + echo "Upload of $filename failed; checking whether another run uploaded it." + refreshExistingAssetNames + if ! assetExists "$filename"; then + return 1 + fi + verifyExistingAsset "$filename" "$expectedHash" + fi + done + } + + if ! gh release view "$tag" --repo microsoft/TypeScript >/dev/null 2>&1; then + if ! gh release create "$tag" \ --repo microsoft/TypeScript \ --title "$tag" \ --latest=false \ - --notes "Bundles TypeScript $bundledTypeScriptVersion from commit $sourceCommit." + --notes "Bundles TypeScript $bundledTypeScriptVersion from commit $sourceCommit."; then + echo "Creation of $tag failed; checking whether another run created it." + gh release view "$tag" --repo microsoft/TypeScript >/dev/null + fi fi + syncReleaseAssets displayName: Create GitHub Release env: GH_TOKEN: $(GH_TOKEN) From 71e5a2d1c30bef8c503afab31753a8d03f71b20c Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:07:07 -0700 Subject: [PATCH 24/24] Reject noncanonical extension versions --- .github/workflows/tag-vscode-typescript.yml | 2 +- Herebyfile.mjs | 31 ++++++++++++++++--- tools/pipelines/vscode-typescript-build.yml | 4 +-- tools/pipelines/vscode-typescript-publish.yml | 4 +-- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tag-vscode-typescript.yml b/.github/workflows/tag-vscode-typescript.yml index b78f542da247f..cc29864bb62ba 100644 --- a/.github/workflows/tag-vscode-typescript.yml +++ b/.github/workflows/tag-vscode-typescript.yml @@ -53,7 +53,7 @@ jobs: exit 0 fi - if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + if ! [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then echo "Invalid extension version: $version" >&2 exit 1 fi diff --git a/Herebyfile.mjs b/Herebyfile.mjs index cfdd27c0546d8..f2cb45e00c97e 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -27,6 +27,8 @@ const __filename = url.fileURLToPath(new URL(import.meta.url)); const __dirname = path.dirname(__filename); const isCI = !!process.env.CI || !!process.env.TF_BUILD; +const stableThreeComponentVersionPatternSource = String.raw`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`; +const stableThreeComponentVersionPattern = new RegExp(stableThreeComponentVersionPatternSource); /** * @typedef {{ @@ -1407,10 +1409,31 @@ export const checkVsceVersion = task({ name: "check:vsce-version", description: "Checks that Azure release jobs use the repository's pinned vsce version.", run: () => { + for (const validVersion of ["0.0.0", "1.2.3", "10.20.30"]) { + assert(stableThreeComponentVersionPattern.test(validVersion), `${validVersion} should be a valid stable version.`); + } + for (const invalidVersion of ["01.0.0", "1.02.0", "1.2.03", "1.2", "1.2.3-beta"]) { + assert(!stableThreeComponentVersionPattern.test(invalidVersion), `${invalidVersion} should not be a valid stable version.`); + } + + for ( + const [workflow, expectedCount] of [ + ["./.github/workflows/tag-vscode-typescript.yml", 1], + ["./tools/pipelines/vscode-typescript-build.yml", 1], + ["./tools/pipelines/vscode-typescript-publish.yml", 2], + ] + ) { + const activeLines = fs.readFileSync(workflow, "utf8").split(/\r?\n/).filter(line => !line.trimStart().startsWith("#")); + const validators = activeLines.filter(line => line.includes(`=~ ${stableThreeComponentVersionPatternSource} ]]`)); + if (validators.length !== expectedCount) { + throw new Error(`${workflow} must contain exactly ${expectedCount} active stable version validator(s).`); + } + } + const packageJson = JSON.parse(fs.readFileSync("./packages/vscode-typescript/package.json", "utf8")); const packageLock = JSON.parse(fs.readFileSync("./package-lock.json", "utf8")); const version = packageJson.devDependencies?.["@vscode/vsce"]; - if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version)) { + if (typeof version !== "string" || !stableThreeComponentVersionPattern.test(version)) { throw new Error(`packages/vscode-typescript must pin @vscode/vsce to an exact version, got ${JSON.stringify(version)}.`); } @@ -1757,8 +1780,8 @@ const getVscodeTypeScriptExtensionPackageJson = memoize(() => JSON.parse(fs.read function getVscodeTypeScriptExtensionVersion() { const version = getVscodeTypeScriptExtensionPackageJson().version; - if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version)) { - throw new Error(`packages/vscode-typescript/package.json must contain a three-component numeric version, got ${JSON.stringify(version)}.`); + if (typeof version !== "string" || !stableThreeComponentVersionPattern.test(version)) { + throw new Error(`packages/vscode-typescript/package.json must contain a stable three-component version, got ${JSON.stringify(version)}.`); } return version; } @@ -2712,7 +2735,7 @@ const getPublishedTypeScriptPackageJson = memoize(() => { function getPublishedTypeScriptVersion() { const version = getPublishedTypeScriptPackageJson().version; - if (releaseVscodeTypescript && !/^\d+\.\d+\.\d+$/.test(version)) { + if (releaseVscodeTypescript && !stableThreeComponentVersionPattern.test(version)) { throw new Error(`vscode-typescript releases require a stable three-component TypeScript version, got ${version}.`); } return version; diff --git a/tools/pipelines/vscode-typescript-build.yml b/tools/pipelines/vscode-typescript-build.yml index f8b27bb07195c..d373a2d943edf 100644 --- a/tools/pipelines/vscode-typescript-build.yml +++ b/tools/pipelines/vscode-typescript-build.yml @@ -87,8 +87,8 @@ extends: echo "package.json version $packageVersion does not match package-lock.json version $lockVersion." >&2 exit 1 fi - if ! [[ "$packageVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Extension version must be a three-component numeric version." >&2 + if ! [[ "$packageVersion" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "Extension version must be a stable three-component version." >&2 exit 1 fi diff --git a/tools/pipelines/vscode-typescript-publish.yml b/tools/pipelines/vscode-typescript-publish.yml index d6c199ee64217..cf0db339acb82 100644 --- a/tools/pipelines/vscode-typescript-publish.yml +++ b/tools/pipelines/vscode-typescript-publish.yml @@ -79,11 +79,11 @@ extends: echo "Unexpected extension identity: $extension" >&2 exit 1 fi - if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + if ! [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then echo "Invalid extension version: $version" >&2 exit 1 fi - if ! [[ "$bundledTypeScriptVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + if ! [[ "$bundledTypeScriptVersion" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then echo "Invalid bundled TypeScript version: $bundledTypeScriptVersion" >&2 exit 1 fi