diff --git a/build/azure-devdiv-pipeline.pre-release.yml b/build/azure-devdiv-pipeline.pre-release.yml new file mode 100644 index 00000000..1dd0b895 --- /dev/null +++ b/build/azure-devdiv-pipeline.pre-release.yml @@ -0,0 +1,123 @@ +# Run on a schedule +trigger: none +pr: none + +schedules: + - cron: '0 10 * * 1-5' # 10AM UTC (2AM PDT) MON-FRI (VS Code Pre-release builds at 9PM PDT) + displayName: Nightly Pre-Release Schedule + always: false # only run if there are source code changes + branches: + include: + - main + +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release +variables: + - name: TeamName + value: VSCode-black-formatter + - name: VsixName + value: black-formatter.vsix +parameters: + - name: publishExtension + displayName: 🚀 Publish Extension + type: boolean + default: false + +extends: + template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate + parameters: + sdl: + sourceAnalysisPool: VSEngSS-MicroBuild2022-1ES + codeSignValidation: + enabled: true + sbom: + enabled: false # Disable global SBOM generation; we'll enable selectively per artifact output + pool: + name: AzurePipelines-EO + os: windows + + customBuildTags: + - ES365AIMigrationTooling + stages: + - stage: Build + displayName: Build & Package Extension + jobs: + - job: Build + displayName: Build Job + pool: + name: VSEngSS-MicroBuild2022-1ES # use windows for codesigning to make things easier https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/650/MicroBuild-Signing + os: windows + templateContext: + mb: + signing: + enabled: true + signType: real + signWithProd: true + outputs: + - output: pipelineArtifact + displayName: 'Publish Drop Artifact' + targetPath: '$(Build.StagingDirectory)\drop' + artifactName: drop + sbomEnabled: true + steps: + - task: NodeTool@0 + inputs: + versionSpec: '22.x' + checkLatest: true + displayName: Select Node 20 LTS + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.13' + addToPath: true + architecture: 'x64' + displayName: Select Python version + + - script: npm ci + displayName: Install NPM dependencies + + - script: python -m pip install -U pip + displayName: Upgrade pip + + - script: python -m pip install wheel + displayName: Install wheel + + - script: python -m pip install nox + displayName: Install nox + + - script: python -m nox --session install_bundled_libs + displayName: Install Python dependencies + + - script: python ./build/update_ext_version.py --for-publishing + displayName: Update build number + + - script: npm run vsce-package + displayName: Build VSIX + + - powershell: New-Item -ItemType Directory -Path "$(Build.StagingDirectory)\drop" -Force | Out-Null; Copy-Item "$(Build.SourcesDirectory)\$(VsixName)" "$(Build.StagingDirectory)\drop\$(VsixName)" -Force; if (!(Test-Path "$(Build.StagingDirectory)\drop\$(VsixName)")) { Write-Error 'VSIX copy failed'; exit 1 }; Get-Item "$(Build.StagingDirectory)\drop\$(VsixName)" | Format-Table Name,Length,LastWriteTime -AutoSize + displayName: Copy VSIX into drop + + - script: npx vsce generate-manifest -i "$(Build.StagingDirectory)\drop\$(VsixName)" -o "$(Build.StagingDirectory)\drop\extension.manifest" + displayName: Generate extension manifest + + - template: build/templates/sign.yml@self + parameters: + vsixName: $(VsixName) + workingDirectory: $(Build.StagingDirectory)\drop + signType: real + verifySignature: true + + - ${{ if eq(parameters.publishExtension, true) }}: + - template: build/templates/publish.yml@self + parameters: + azureSubscription: PylancePublishPipelineSecureConnectionWithManagedIdentity + vsixName: $(VsixName) + manifestName: extension.manifest + signatureName: extension.signature.p7s + publishFolder: drop + preRelease: true + noVerify: true diff --git a/build/azure-devdiv-pipeline.stable.yml b/build/azure-devdiv-pipeline.stable.yml new file mode 100644 index 00000000..46695db6 --- /dev/null +++ b/build/azure-devdiv-pipeline.stable.yml @@ -0,0 +1,118 @@ +name: Publish Release +trigger: + branches: + include: + - refs/tags/* + +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release +variables: + - name: TeamName + value: VSCode-black-formatter + - name: VsixName + value: black-formatter.vsix + +parameters: + - name: publishExtension + displayName: 🚀 Publish Extension + type: boolean + default: false + +extends: + template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate + parameters: + sdl: + sourceAnalysisPool: VSEngSS-MicroBuild2022-1ES + codeSignValidation: + enabled: true + sbom: + enabled: false # Disable global SBOM generation; we'll enable selectively per artifact output + pool: + name: AzurePipelines-EO + os: windows + + customBuildTags: + - ES365AIMigrationTooling + stages: + - stage: Build + displayName: Build & Package Extension + jobs: + - job: Build + displayName: Build Job + pool: + name: VSEngSS-MicroBuild2022-1ES # use windows for codesigning to make things easier https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/650/MicroBuild-Signing + os: windows + templateContext: + mb: + signing: + enabled: true + signType: real + signWithProd: true + outputs: + - output: pipelineArtifact + displayName: 'Publish Drop Artifact' + targetPath: '$(Build.StagingDirectory)\drop' + artifactName: drop + sbomEnabled: true + steps: + - task: NodeTool@0 + inputs: + versionSpec: '22.x' + checkLatest: true + displayName: Select Node 20 LTS + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.13' + addToPath: true + architecture: 'x64' + displayName: Select Python version + + - script: npm ci + displayName: Install NPM dependencies + + - script: python -m pip install -U pip + displayName: Upgrade pip + + - script: python -m pip install wheel + displayName: Install wheel + + - script: python -m pip install nox + displayName: Install nox + + - script: python -m nox --session install_bundled_libs + displayName: Install Python dependencies + + - script: python ./build/update_ext_version.py --release --for-publishing + displayName: Update build number + + - script: npm run vsce-package + displayName: Build VSIX + + - powershell: | + New-Item -ItemType Directory -Path "$(Build.StagingDirectory)\drop" -Force | Out-Null; Copy-Item "$(Build.SourcesDirectory)\$(VsixName)" "$(Build.StagingDirectory)\drop\$(VsixName)" -Force; if (!(Test-Path "$(Build.StagingDirectory)\drop\$(VsixName)")) { Write-Error 'VSIX copy failed'; exit 1 }; Get-Item "$(Build.StagingDirectory)\drop\$(VsixName)" | Format-Table Name,Length,LastWriteTime -AutoSize + displayName: Copy VSIX into drop + + - script: npx vsce generate-manifest -i "$(Build.StagingDirectory)\drop\$(VsixName)" -o "$(Build.StagingDirectory)\drop\extension.manifest" + displayName: Generate extension manifest + + - template: build/templates/sign.yml@self + parameters: + vsixName: $(VsixName) + workingDirectory: $(Build.StagingDirectory)\drop + signType: real + verifySignature: true + + - ${{ if eq(parameters.publishExtension, true) }}: + - template: build/templates/publish.yml@self + parameters: + azureSubscription: PylancePublishPipelineSecureConnectionWithManagedIdentity + vsixName: $(VsixName) + manifestName: extension.manifest + signatureName: extension.signature.p7s + publishFolder: drop + preRelease: false + noVerify: true diff --git a/build/templates/publish.yml b/build/templates/publish.yml new file mode 100644 index 00000000..3f57e46b --- /dev/null +++ b/build/templates/publish.yml @@ -0,0 +1,118 @@ +# Template (steps): PublishMarketplace for black-formatter extension +# Expects working directory already populated (or artifact previously downloaded) with: black-formatter.vsix, extension.manifest, extension.signature.p7s +# Provides optional prerelease publishing via parameter. +# +# Usage (example inside a stage job): +# steps: +# - template: build/templates/publish.yml@self +# parameters: +# azureSubscription: +# artifactName: drop +# vsixName: black-formatter.vsix +# manifestName: extension.manifest +# signatureName: extension.signature.p7s +# publishFolder: vscode-black-formatter +# preRelease: true +# noVerify: true +# +# Notes: +# - Azure DevOps Marketplace resource GUID (499b84ac-1321-427f-aa17-267ca6975798) is hardcoded in publish script. +# - This uses Managed Identity via AzureCLI@2 to acquire an AAD token and passes it as a PAT. +# - Requires extension artifacts already signed (signature file present). +# - Node & vsce expected to be prepared by parent pipeline; omit local installation here. + +parameters: + - name: azureSubscription + type: string + - name: vsixName + type: string + default: black-formatter.vsix + - name: manifestName + type: string + default: extension.manifest + - name: signatureName + type: string + default: extension.signature.p7s + - name: publishFolder + type: string + default: vscode-black-formatter + - name: preRelease + type: boolean + default: false + - name: noVerify + type: boolean + default: true + +steps: + # Node & vsce expected to be prepared by parent pipeline; omit local installation. + + # Assumes files already present at $(Build.ArtifactStagingDirectory)/publishFolder + + # Step 1: Acquire token only (store secret variable MarketplaceAADToken) + - task: AzureCLI@2 + displayName: Acquire Marketplace AAD token + inputs: + azureSubscription: ${{ parameters.azureSubscription }} + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $resource = "499b84ac-1321-427f-aa17-267ca6975798" + Write-Host "Acquiring AAD token for resource: $resource" + az rest -u https://app.vssps.visualstudio.com/_apis/profile/profiles/me --resource $resource | Out-Null + $aadToken = az account get-access-token --query accessToken --resource $resource -o tsv + if (-not $aadToken) { Write-Error 'Failed to acquire AAD token.'; exit 1 } + Write-Host "##vso[task.setvariable variable=MarketplaceAADToken;isSecret=true]$aadToken" + Write-Host "Token stored in secret variable MarketplaceAADToken" + + # Step 2: Validate artifacts & publish + - task: PowerShell@2 + displayName: Publish extension (vsce) + inputs: + targetType: inline + script: | + $aadToken = "$(MarketplaceAADToken)" + if (-not $aadToken) { Write-Error 'MarketplaceAADToken is empty (token acquisition failed).'; exit 1 } + + $root = "$(Build.ArtifactStagingDirectory)/${{ parameters.publishFolder }}" + $vsixPath = Join-Path $root "${{ parameters.vsixName }}" + $manifestPath = Join-Path $root "${{ parameters.manifestName }}" + $signaturePath = Join-Path $root "${{ parameters.signatureName }}" + + Write-Host "VSIX Path: $vsixPath" + Write-Host "Manifest Path: $manifestPath" + Write-Host "Signature Path: $signaturePath" + + if (-not (Test-Path $vsixPath)) { Write-Error "VSIX file not found: $vsixPath"; exit 1 } + if (-not (Test-Path $manifestPath)) { Write-Error "Manifest file not found: $manifestPath"; exit 1 } + if (-not (Test-Path $signaturePath)) { Write-Error "Signature file not found: $signaturePath"; exit 1 } + + Write-Host "Listing publish folder contents: $root" + Get-ChildItem -Recurse $root | Select-Object FullName,Length | Format-Table -AutoSize + + $extraFlags = '' + if ('${{ parameters.noVerify }}' -eq 'True') { $extraFlags = "$extraFlags --noVerify" } + + if ('${{ parameters.preRelease }}' -eq 'True') { + Write-Host 'Publishing as pre-release' + # disabled for now; uncomment when ready + npx vsce publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath $extraFlags --pre-release + } else { + Write-Host 'Publishing as stable release' + # disabled for now; uncomment when ready + npx vsce publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath $extraFlags + } + + if ($LASTEXITCODE -ne 0) { + Write-Error "vsce publish failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } + Write-Host 'Publish step completed (publish command currently disabled).' + + - task: PowerShell@2 + displayName: Post-publish summary + inputs: + targetType: inline + script: | + Write-Host 'Published extension artifacts:' + Get-ChildItem "$(Build.ArtifactStagingDirectory)/${{ parameters.publishFolder }}" -File | Select-Object Name,Length | Format-Table -AutoSize + Write-Host "Pre-release parameter: ${{ parameters.preRelease }}" diff --git a/build/templates/sign.yml b/build/templates/sign.yml new file mode 100644 index 00000000..1bce60a8 --- /dev/null +++ b/build/templates/sign.yml @@ -0,0 +1,134 @@ +# Template: Sign and validate VS Code extension artifacts +# Usage Example: +# - template: build/templates/sign.yml@self +# parameters: +# vsixName: black-formatter.vsix +# workingDirectory: $(Build.SourcesDirectory) +# signType: real +# Note: vsce CLI is invoked via 'npx vsce' (devDependency), no global install required. + +parameters: + - name: vsixName + type: string + default: black-formatter.vsix + - name: manifestName + type: string + default: extension.manifest + - name: signatureName + type: string + default: extension.signature.p7s + - name: workingDirectory + type: string + default: '$(Build.SourcesDirectory)' + - name: signType + type: string + default: real + - name: verifySignature + type: boolean + default: true + - name: prepareRoot + type: boolean + default: true + # vsceVersion parameter removed; rely on pinned devDependency version via npx. + +steps: + # vsce CLI expected to be installed by parent pipeline; no local install here. + + - task: NuGetToolInstaller@1 + displayName: Install NuGet + + - task: NuGetCommand@2 + displayName: Restore signing packages + inputs: + command: restore + restoreSolution: '$(Build.SourcesDirectory)/packages.config' + restoreDirectory: '$(Build.SourcesDirectory)/packages' + + - task: PowerShell@2 + displayName: Pre-sign inspection + inputs: + targetType: inline + script: | + $wd = "${{ parameters.workingDirectory }}" + $vsixName = "${{ parameters.vsixName }}" + $manifestName = "${{ parameters.manifestName }}" + $signatureName = "${{ parameters.signatureName }}" + Write-Host "Pre-sign contents of working directory: $wd" + Get-ChildItem -Recurse $wd | Select-Object FullName,Length | Format-Table -AutoSize + $vsix = Join-Path $wd $vsixName + if (!(Test-Path $vsix)) { Write-Error "VSIX missing: $vsix"; exit 1 } + $manifest = Join-Path $wd $manifestName + if (!(Test-Path $manifest)) { Write-Error "Manifest missing: $manifest"; exit 1 } + $sig = Join-Path $wd $signatureName + if (!(Test-Path $sig)) { Write-Warning "Signature placeholder missing (will attempt signing anyway)." } + + # Deprecated prepareRoot step removed: we now sign directly in workingDirectory by overriding BaseOutputDirectory. + + - task: MSBuild@1 + displayName: Run signing (MSBuild) + inputs: + solution: '$(Build.SourcesDirectory)/build/sign.proj' + msbuildArguments: '/verbosity:detailed /bl:"${{ parameters.workingDirectory }}\\signing.binlog" /p:SignType=${{ parameters.signType }} /p:BaseOutputDirectory=${{ parameters.workingDirectory }} /p:OutDir=${{ parameters.workingDirectory }} /p:IntermediateOutputPath=${{ parameters.workingDirectory }}\\intermediate' + + # No copy-back needed; signing outputs now land directly in workingDirectory. + + - task: PowerShell@2 + displayName: Post-sign inspection + inputs: + targetType: inline + script: | + $wd = "${{ parameters.workingDirectory }}" + $signatureName = "${{ parameters.signatureName }}" + Write-Host "Post-sign file listing:" + Get-ChildItem $wd -File | Select-Object Name,Length | Format-Table -AutoSize + $sig = Join-Path $wd $signatureName + if (Test-Path $sig) { + Write-Host "Signature file present: $sig" + } else { + Write-Warning "Signature file NOT present after signing step."; exit 0 + } + + - task: PowerShell@2 + displayName: Validate signature differs from manifest (hash check) + inputs: + targetType: inline + script: | + $wd = "${{ parameters.workingDirectory }}" + $manifest = Join-Path $wd "${{ parameters.manifestName }}" + $signature = Join-Path $wd "${{ parameters.signatureName }}" + if (!(Test-Path $manifest)) { Write-Error "Manifest missing for hash comparison: $manifest"; exit 1 } + if (!(Test-Path $signature)) { Write-Error "Signature missing for hash comparison: $signature"; exit 1 } + $manifestHash = (Get-FileHash -Algorithm SHA256 $manifest).Hash + $signatureHash = (Get-FileHash -Algorithm SHA256 $signature).Hash + Write-Host "Manifest SHA256 : $manifestHash" + Write-Host "Signature SHA256: $signatureHash" + if ($manifestHash -eq $signatureHash) { + Write-Error "Signature file is identical to manifest (placeholder detected). Failing build."; exit 1 + } else { + Write-Host "Hashes differ ✅ (signature not a direct copy of manifest)" + } + + - ${{ if eq(parameters.verifySignature, true) }}: + - task: PowerShell@2 + displayName: Verify VSIX signature + inputs: + targetType: inline + script: | + $wd = "${{ parameters.workingDirectory }}" + $vsix = Join-Path $wd "${{ parameters.vsixName }}" + $manifest = Join-Path $wd "${{ parameters.manifestName }}" + $signature = Join-Path $wd "${{ parameters.signatureName }}" + Write-Host "Verifying signature:" + Write-Host " packagePath : $vsix" + Write-Host " manifestPath : $manifest" + Write-Host " signaturePath: $signature" + if (!(Test-Path $vsix)) { Write-Error "Missing VSIX: $vsix"; exit 1 } + if (!(Test-Path $manifest)) { Write-Error "Missing manifest: $manifest"; exit 1 } + if (!(Test-Path $signature)) { Write-Error "Missing signature file: $signature"; exit 1 } + npx vsce verify-signature --packagePath "$vsix" --manifestPath "$manifest" --signaturePath "$signature" + if ($LASTEXITCODE -ne 0) { + Write-Error "vsce verify-signature failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } else { + Write-Host "vsce verify-signature succeeded ✅" + } diff --git a/package.json b/package.json index b729e4e8..3e179e0a 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "scripts": { "compile": "webpack", "compile-tests": "tsc -p . --outDir out", - "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.yml' '.github/**/*.yml'", + "format-check": "prettier --check src/**/*.ts build/**/*.yml .github/**/*.yml", + "fix-format": "prettier --write src/**/*.ts build/**/*.yml .github/**/*.yml", "lint": "eslint src --ext ts", "package": "webpack --mode production --devtool hidden-source-map", "pretest": "npm run compile-tests && npm run compile && npm run lint",