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..c8ae85a878fcf --- /dev/null +++ b/.github/workflows/tag-vscode-typescript.yml @@ -0,0 +1,95 @@ +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 + 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 }} + 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..a3f3bc0d9b6be --- /dev/null +++ b/tools/pipelines/vscode-typescript-build.yml @@ -0,0 +1,175 @@ +trigger: + tags: + include: + - vscode-typescript/v* + +pr: none + +name: vscode-typescript-$(Date:yyyyMMdd)$(Rev:.r) +appendCommitMessageToRunName: false + +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: real + azureSubscription: MicroBuild Signing Task (DevDiv) + 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: 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)