From 95234a1d8a41405553cc86b53a6da0633e6d9fd6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:30:25 -0700 Subject: [PATCH 1/5] Build all platforms in parallel in release pipeline --- Herebyfile.mjs | 239 +++++++++++++++++---------- tools/pipelines/typescript-build.yml | 67 +++++++- 2 files changed, 218 insertions(+), 88 deletions(-) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index ca85ddf794695..5d28629637df5 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -87,6 +87,7 @@ const { values: rawOptions } = parseArgs({ forRelease: { type: "boolean" }, respectGoEnv: { type: "boolean" }, vscodeTypescriptRelease: { type: "boolean" }, + platform: { type: "string" }, race: { type: "boolean", default: parseEnvBoolean("RACE") }, noembed: { type: "boolean", default: parseEnvBoolean("NOEMBED") }, @@ -2216,46 +2217,79 @@ async function runBuildNativePreviewPackages() { await rimraf(builtNpm); const platforms = getPlatforms(); + await buildMainNativePreviewPackage(platforms); - const inputDir = "./packages/typescript"; - - const inputPackageJson = JSON.parse(fs.readFileSync(path.join(inputDir, "package.json"), "utf8")); - inputPackageJson.version = getVersion(); - delete inputPackageJson.private; - inputPackageJson.files = [...new Set([...(inputPackageJson.files ?? []), "NOTICE.txt"])]; - if (publishAsTypescript) { - inputPackageJson.bin = { - tsc: "./bin/tsc", - }; - inputPackageJson.description = "TypeScript is a language for application scale JavaScript development"; - inputPackageJson.homepage = "https://www.typescriptlang.org/"; - inputPackageJson.keywords = [ - "TypeScript", - "Microsoft", - "compiler", - "language", - "javascript", - ]; - inputPackageJson.bugs = { - url: "https://github.com/microsoft/TypeScript/issues", - }; - inputPackageJson.repository = { - type: "git", - url: "https://github.com/microsoft/TypeScript.git", - }; - delete inputPackageJson.scripts; - delete inputPackageJson.devDependencies; + if (isCI) { + for (const platform of platforms) { + await buildNativePreviewPlatform(platform); + // Build machines have too little space. + // Clear the Go build cache between platforms. + await $`go clean -cache`; + } } - stripSourceConditions(inputPackageJson); - - if (fs.existsSync(".git")) { - const { stdout: gitHead } = await runOutput("git", ["rev-parse", "HEAD"]); - inputPackageJson.gitHead = gitHead.trim(); + else { + const buildLimit = pLimit(os.availableParallelism()); + await Promise.all(platforms.map(platform => buildLimit(() => buildNativePreviewPlatform(platform)))); } - inputPackageJson.publishConfig = { - access: "public", - tag: getPublishTag(), - }; +} + +export const buildMainNativePreviewPackageTask = task({ + name: "typescript:build-main", + hiddenFromTaskList: true, + run: async () => { + if (usePublishedPlatformPackagesForVsix) { + checkPublishedPlatformPackagesForVsix(); + await rimraf(builtNpm); + console.log("Skipping npm package build; VSIX packaging will use published platform packages."); + return; + } + + await rimraf(builtNpm); + await buildMainNativePreviewPackage(getPlatforms()); + }, +}); + +export const buildNativePreviewPlatformTask = task({ + name: "typescript:build-platform", + hiddenFromTaskList: true, + run: async () => { + if (usePublishedPlatformPackagesForVsix) { + throw new Error("Platform packages are not built when VSIX packaging uses published platform packages."); + } + if (!options.platform) { + throw new Error("typescript:build-platform requires --platform."); + } + + const platform = getPlatforms().find(({ nodeOs, nodeArch }) => `${nodeOs}-${nodeArch}` === options.platform); + if (!platform) { + throw new Error(`Unknown release platform: ${options.platform}`); + } + + await rimraf(platform.npmDir); + await buildNativePreviewPlatform(platform); + }, +}); + +export const buildNativePreviewPlatformMatrix = task({ + name: "typescript:build-platform-matrix", + hiddenFromTaskList: true, + run: () => { + const matrix = Object.fromEntries( + getPlatforms().map(({ nodeOs, nodeArch }) => { + const platform = `${nodeOs}-${nodeArch}`; + return [platform.replaceAll("-", "_"), { platform }]; + }), + ); + console.log(`##vso[task.setvariable variable=matrix;isOutput=true]${JSON.stringify(matrix)}`); + }, +}); + +/** + * @param {ReturnType} platforms + */ +async function buildMainNativePreviewPackage(platforms) { + const inputDir = "./packages/typescript"; + const inputPackageJson = await createNativePreviewInputPackageJson(); const mainPackage = { ...inputPackageJson, @@ -2307,65 +2341,98 @@ async function runBuildNativePreviewPackages() { if (importErrors.length) { throw new Error(`Found external imports in .d.ts files:\n${importErrors.map(e => " " + e).join("\n")}`); } +} - const extraFlags = options.respectGoEnv - ? [] - : getReleaseBuildFlags(options.setPrerelease || nativePreviewReleaseVersion ? getVersion() : undefined); - - const platformBuilders = platforms.map(({ npmDir, npmPackageName, nodeOs, nodeArch, goos, goarch }) => async () => { - const packageJson = { - ...inputPackageJson, - bin: undefined, - files: ["lib", "NOTICE.txt"], - imports: undefined, - dependencies: undefined, - name: npmPackageName, - os: [nodeOs], - cpu: [nodeArch], - exports: { - "./package.json": "./package.json", - }, - }; +/** + * @param {ReturnType[number]} platform + */ +async function buildNativePreviewPlatform({ npmDir, npmPackageName, nodeOs, nodeArch, goos, goarch }) { + const inputPackageJson = await createNativePreviewInputPackageJson(); - const out = path.join(npmDir, "lib"); - await fs.promises.mkdir(out, { recursive: true }); - await fs.promises.writeFile(path.join(npmDir, "package.json"), JSON.stringify(packageJson, undefined, 4)); - await fs.promises.copyFile("LICENSE.txt", path.join(npmDir, "LICENSE")); - await fs.promises.copyFile("NOTICE.txt", path.join(npmDir, "NOTICE.txt")); + const packageJson = { + ...inputPackageJson, + bin: undefined, + files: ["lib", "NOTICE.txt"], + imports: undefined, + dependencies: undefined, + name: npmPackageName, + os: [nodeOs], + cpu: [nodeArch], + exports: { + "./package.json": "./package.json", + }, + }; - const readme = [ - `# \`${npmPackageName}\``, - "", - `This package provides ${nodeOs}-${nodeArch} support for [${mainNativePreviewPackage.npmPackageName}](https://www.npmjs.com/package/${mainNativePreviewPackage.npmPackageName}).`, - ]; + const out = path.join(npmDir, "lib"); + await fs.promises.mkdir(out, { recursive: true }); + await fs.promises.writeFile(path.join(npmDir, "package.json"), JSON.stringify(packageJson, undefined, 4)); + await fs.promises.copyFile("LICENSE.txt", path.join(npmDir, "LICENSE")); + await fs.promises.copyFile("NOTICE.txt", path.join(npmDir, "NOTICE.txt")); + + const readme = [ + `# \`${npmPackageName}\``, + "", + `This package provides ${nodeOs}-${nodeArch} support for [${mainNativePreviewPackage.npmPackageName}](https://www.npmjs.com/package/${mainNativePreviewPackage.npmPackageName}).`, + ]; - await fs.promises.writeFile(path.join(npmDir, "README.md"), readme.join("\n") + "\n"); + await fs.promises.writeFile(path.join(npmDir, "README.md"), readme.join("\n") + "\n"); - await generateLibs(out); + await generateLibs(out); - const exeName = nativePreviewExeName(nodeOs); - await buildTsc({ - out: publishAsTypescript ? path.join(out, exeName) : out, - env: { - GOOS: goos, - GOARCH: goarch, - ...(options.respectGoEnv ? {} : { GOARM: "6", CGO_ENABLED: "0" }), - }, - extraFlags, - }); + const exeName = nativePreviewExeName(nodeOs); + await buildTsc({ + out: publishAsTypescript ? path.join(out, exeName) : out, + env: { + GOOS: goos, + GOARCH: goarch, + ...(options.respectGoEnv ? {} : { GOARM: "6", CGO_ENABLED: "0" }), + }, + extraFlags: options.respectGoEnv + ? [] + : getReleaseBuildFlags(options.setPrerelease || nativePreviewReleaseVersion ? getVersion() : undefined), }); +} - if (isCI) { - for (const build of platformBuilders) { - await build(); - // Build machines have too little space. - // Clear the Go build cache between platforms. - await run("go", ["clean", "-cache"]); - } +async function createNativePreviewInputPackageJson() { + const inputPackageJson = JSON.parse(fs.readFileSync("./packages/typescript/package.json", "utf8")); + inputPackageJson.version = getVersion(); + delete inputPackageJson.private; + inputPackageJson.files = [...new Set([...(inputPackageJson.files ?? []), "NOTICE.txt"])]; + if (publishAsTypescript) { + inputPackageJson.bin = { + tsc: "./bin/tsc", + }; + inputPackageJson.description = "TypeScript is a language for application scale JavaScript development"; + inputPackageJson.homepage = "https://www.typescriptlang.org/"; + inputPackageJson.keywords = [ + "TypeScript", + "Microsoft", + "compiler", + "language", + "javascript", + ]; + inputPackageJson.bugs = { + url: "https://github.com/microsoft/TypeScript/issues", + }; + inputPackageJson.repository = { + type: "git", + url: "https://github.com/microsoft/TypeScript.git", + }; + delete inputPackageJson.scripts; + delete inputPackageJson.devDependencies; } - else { - await runWithConcurrencyLimit(platformBuilders, os.availableParallelism()); + stripSourceConditions(inputPackageJson); + + if (fs.existsSync(".git")) { + const { stdout: gitHead } = await runOutput("git", ["rev-parse", "HEAD"]); + inputPackageJson.gitHead = gitHead.trim(); } + inputPackageJson.publishConfig = { + access: "public", + tag: getPublishTag(), + }; + + return inputPackageJson; } /** diff --git a/tools/pipelines/typescript-build.yml b/tools/pipelines/typescript-build.yml index e46e306268e77..664566993bd99 100755 --- a/tools/pipelines/typescript-build.yml +++ b/tools/pipelines/typescript-build.yml @@ -51,8 +51,51 @@ extends: - stage: Build displayName: Build and Sign jobs: + - job: SetupBuildMatrix + displayName: Set up platform build matrix + + steps: + - checkout: self + clean: true + submodules: false + fetchDepth: 1 + fetchTags: false + - template: /tools/pipelines/steps/setup-node-npm-ci.yml@self + + - bash: npx hereby typescript:build-platform-matrix --forRelease --setPrerelease dev.$(Build.BuildNumber) + displayName: Set build matrix + name: buildMatrix + + - job: BuildPackages + displayName: Build $(platform) + dependsOn: SetupBuildMatrix + + strategy: + matrix: $[ dependencies.SetupBuildMatrix.outputs['buildMatrix.matrix'] ] + + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/built/npm/typescript-$(platform) + artifactName: typescript-$(platform) + isProduction: false + sbomEnabled: false + + steps: + - checkout: self + clean: true + submodules: false + fetchDepth: 1 + fetchTags: false + - template: /tools/pipelines/steps/setup-node-npm-ci.yml@self + - template: /tools/pipelines/steps/setup-go.yml@self + + - bash: npx hereby typescript:build-platform --platform $(platform) --forRelease --setPrerelease dev.$(Build.BuildNumber) + displayName: Build $(platform) + - job: Build displayName: Build and Sign + dependsOn: BuildPackages timeoutInMinutes: 180 pool: name: AzurePipelines-EO @@ -136,8 +179,28 @@ extends: - template: /tools/pipelines/steps/setup-go.yml@self - - bash: npx hereby typescript:build --forRelease --setPrerelease dev.$(initialBuildNumber) - displayName: 'Build packages' + - bash: npx hereby typescript:build-main --forRelease --setPrerelease dev.$(initialBuildNumber) + displayName: 'Build main package' + + - task: DownloadPipelineArtifact@2 + displayName: 'Download platform packages' + inputs: + buildType: current + targetPath: $(Pipeline.Workspace)/platform-packages + patterns: 'typescript-*/**' + + - bash: | + set -euo pipefail + shopt -s nullglob + platformArtifacts=("$(Pipeline.Workspace)"/platform-packages/typescript-*) + if (( ${#platformArtifacts[@]} == 0 )); then + echo "No platform package artifacts were downloaded." >&2 + exit 1 + fi + for artifact in "${platformArtifacts[@]}"; do + cp -a "$artifact" "built/npm/$(basename "$artifact")" + done + displayName: 'Assemble platform packages' - bash: npx hereby typescript:test-package --forRelease --setPrerelease dev.$(initialBuildNumber) displayName: 'Test packages' From 118d3e700383b229dbabe3dde762ec9c741c0e8c Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:05:13 -0700 Subject: [PATCH 2/5] Disable signing setup in non-signing release jobs Matrix setup and platform builds do not sign artifacts, so avoid provisioning signing prerequisites in those jobs. --- tools/pipelines/typescript-build.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/pipelines/typescript-build.yml b/tools/pipelines/typescript-build.yml index 664566993bd99..8d43c3e6f6e7b 100755 --- a/tools/pipelines/typescript-build.yml +++ b/tools/pipelines/typescript-build.yml @@ -54,6 +54,11 @@ extends: - job: SetupBuildMatrix displayName: Set up platform build matrix + templateContext: + mb: + signing: + enabled: false + steps: - checkout: self clean: true @@ -74,6 +79,9 @@ extends: matrix: $[ dependencies.SetupBuildMatrix.outputs['buildMatrix.matrix'] ] templateContext: + mb: + signing: + enabled: false outputs: - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/built/npm/typescript-$(platform) From 404164a734177d507d63abd74bfd9b7ea8b803fb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:25:52 -0700 Subject: [PATCH 3/5] Repair parallel release builds across profiles Rebased build tasks must use the current process helpers, and platform artifacts must retain their profile-specific paths and executable permissions after transfer. --- Herebyfile.mjs | 9 ++++----- tools/pipelines/typescript-build.yml | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 5d28629637df5..9bc6c89bb95e2 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -2224,12 +2224,11 @@ async function runBuildNativePreviewPackages() { await buildNativePreviewPlatform(platform); // Build machines have too little space. // Clear the Go build cache between platforms. - await $`go clean -cache`; + await run("go", ["clean", "-cache"]); } } else { - const buildLimit = pLimit(os.availableParallelism()); - await Promise.all(platforms.map(platform => buildLimit(() => buildNativePreviewPlatform(platform)))); + await runWithConcurrencyLimit(platforms.map(platform => () => buildNativePreviewPlatform(platform)), os.availableParallelism()); } } @@ -2275,9 +2274,9 @@ export const buildNativePreviewPlatformMatrix = task({ hiddenFromTaskList: true, run: () => { const matrix = Object.fromEntries( - getPlatforms().map(({ nodeOs, nodeArch }) => { + getPlatforms().map(({ nodeOs, nodeArch, npmDirName }) => { const platform = `${nodeOs}-${nodeArch}`; - return [platform.replaceAll("-", "_"), { platform }]; + return [platform.replaceAll("-", "_"), { platform, npmDirName }]; }), ); console.log(`##vso[task.setvariable variable=matrix;isOutput=true]${JSON.stringify(matrix)}`); diff --git a/tools/pipelines/typescript-build.yml b/tools/pipelines/typescript-build.yml index 8d43c3e6f6e7b..64ab54540151e 100755 --- a/tools/pipelines/typescript-build.yml +++ b/tools/pipelines/typescript-build.yml @@ -84,8 +84,8 @@ extends: enabled: false outputs: - output: pipelineArtifact - targetPath: $(Build.SourcesDirectory)/built/npm/typescript-$(platform) - artifactName: typescript-$(platform) + targetPath: $(Build.SourcesDirectory)/built/npm/$(npmDirName) + artifactName: $(npmDirName) isProduction: false sbomEnabled: false @@ -195,18 +195,25 @@ extends: inputs: buildType: current targetPath: $(Pipeline.Workspace)/platform-packages - patterns: 'typescript-*/**' + patterns: '*-*/**' - bash: | set -euo pipefail shopt -s nullglob - platformArtifacts=("$(Pipeline.Workspace)"/platform-packages/typescript-*) + platformArtifacts=("$(Pipeline.Workspace)"/platform-packages/*) if (( ${#platformArtifacts[@]} == 0 )); then echo "No platform package artifacts were downloaded." >&2 exit 1 fi for artifact in "${platformArtifacts[@]}"; do - cp -a "$artifact" "built/npm/$(basename "$artifact")" + packageDir="built/npm/$(basename "$artifact")" + cp -a "$artifact" "$packageDir" + executable="$(find "$packageDir/lib" -maxdepth 1 -type f \( -name tsc -o -name tsc.exe -o -name tsgo -o -name tsgo.exe \) -print -quit)" + if [[ -z "$executable" ]]; then + echo "Missing platform executable in $packageDir." >&2 + exit 1 + fi + chmod +x "$executable" done displayName: 'Assemble platform packages' From a960cbc0f6eacc3b7ae6598227cc3579685c9dc3 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:17:58 -0700 Subject: [PATCH 4/5] Avoid unresolved platform macro in release job titles Azure appends the matrix leg name to job titles but leaves platform macros unexpanded, so give each leg a readable static title. --- tools/pipelines/typescript-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pipelines/typescript-build.yml b/tools/pipelines/typescript-build.yml index 64ab54540151e..f7164d86cdd7a 100755 --- a/tools/pipelines/typescript-build.yml +++ b/tools/pipelines/typescript-build.yml @@ -72,7 +72,7 @@ extends: name: buildMatrix - job: BuildPackages - displayName: Build $(platform) + displayName: Build platform package dependsOn: SetupBuildMatrix strategy: From 0a7c823aa32fad3f3e3193116fe09e9476846bf1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 1 Oct 2026 07:42:58 -0700 Subject: [PATCH 5/5] Limit release assembly to platform artifacts Azure downloads non-platform artifacts into the workspace as well; select only the active release profile's packages so assembly does not fail on unrelated directories. --- Herebyfile.mjs | 1 + tools/pipelines/typescript-build.yml | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 9bc6c89bb95e2..801eba31e4a37 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -2280,6 +2280,7 @@ export const buildNativePreviewPlatformMatrix = task({ }), ); console.log(`##vso[task.setvariable variable=matrix;isOutput=true]${JSON.stringify(matrix)}`); + console.log(`##vso[task.setvariable variable=packageBaseName;isOutput=true]${path.basename(mainNativePreviewPackage.npmDir)}`); }, }); diff --git a/tools/pipelines/typescript-build.yml b/tools/pipelines/typescript-build.yml index f7164d86cdd7a..371608eb63cf0 100755 --- a/tools/pipelines/typescript-build.yml +++ b/tools/pipelines/typescript-build.yml @@ -103,12 +103,16 @@ extends: - job: Build displayName: Build and Sign - dependsOn: BuildPackages + dependsOn: + - SetupBuildMatrix + - BuildPackages timeoutInMinutes: 180 pool: name: AzurePipelines-EO image: 1ESPT-Ubuntu22.04 os: linux + variables: + packageBaseName: $[ dependencies.SetupBuildMatrix.outputs['buildMatrix.packageBaseName'] ] templateContext: mb: @@ -195,12 +199,12 @@ extends: inputs: buildType: current targetPath: $(Pipeline.Workspace)/platform-packages - patterns: '*-*/**' + patterns: '$(packageBaseName)-*/**' - bash: | set -euo pipefail shopt -s nullglob - platformArtifacts=("$(Pipeline.Workspace)"/platform-packages/*) + platformArtifacts=("$(Pipeline.Workspace)"/platform-packages/"$PACKAGE_BASE_NAME"-*) if (( ${#platformArtifacts[@]} == 0 )); then echo "No platform package artifacts were downloaded." >&2 exit 1 @@ -216,6 +220,8 @@ extends: chmod +x "$executable" done displayName: 'Assemble platform packages' + env: + PACKAGE_BASE_NAME: $(packageBaseName) - bash: npx hereby typescript:test-package --forRelease --setPrerelease dev.$(initialBuildNumber) displayName: 'Test packages'