diff --git a/.pipelines/1es-migration/azure-pipelines.yml b/.pipelines/1es-migration/azure-pipelines.yml index ad80c9984..0c1256cba 100644 --- a/.pipelines/1es-migration/azure-pipelines.yml +++ b/.pipelines/1es-migration/azure-pipelines.yml @@ -1,14 +1,10 @@ # Azure Pipelines Extensions 1ES Build Pipeline -# This pipeline builds, signs, and publishes Azure DevOps extensions using 1ES standards -# Compliant with Secure Extension Onboarding initiative +# Single build stage: MAIN -> copy, TEST -> copy; then sign, approve, publish (TEST then PUBLIC) name: Extension $(ExtensionName) - $(Date:yyyyMMdd)$(Rev:.r) appendCommitMessageToRunName: false trigger: none -# branches: -# include: -# - main parameters: - name: extensionName @@ -44,12 +40,12 @@ variables: # 1ES Security Scanning - name: CodeQL.Enabled value: true -# Extension registry settings +# Publisher - name: PublisherId value: 'ms-vscs-rm' -# Signing variables from secure group +# ESRP Signing secrets - group: EPS.ESRPSigningProdAME -# Dynamic variables +# Dynamic - name: ExtensionName value: ${{ parameters.extensionName }} - name: IsMainBranchBuild @@ -57,7 +53,6 @@ variables: resources: repositories: - # 1ES Pipeline Templates - repository: 1ESPipelineTemplates type: git name: 1ESPipelineTemplates/1ESPipelineTemplates @@ -66,12 +61,11 @@ resources: extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates parameters: - # SDL Configuration sdl: sourceAnalysisPool: name: 1ESPtTfsAgentBuildPoolSDL spotBugs: - enabled: false # Not applicable for TypeScript/JavaScript + enabled: false credscan: enabled: true binskim: @@ -79,11 +73,9 @@ extends: eslint: enabled: true - # Build Pool pool: name: 1ESPtTfsAgentBuildPool1 - # Custom Build Tags customBuildTags: - ES365AIMigrationTooling - 1ES-AzureExtensions @@ -92,98 +84,156 @@ extends: stages: - - stage: BuildAndTest - displayName: 'Build and Test ${{ parameters.extensionName }}' + # ============================== Build & Package ============================== + - stage: Build_And_Package_Main_And_Test + displayName: 'Build & Package Extensions (Main + Test)' jobs: - - job: BuildAndTestJob - displayName: 'Build Extension' + - job: BuildAndPackage + displayName: 'Build and Package: MAIN then TEST' templateContext: outputs: - output: pipelineArtifact - displayName: 'Publish Build Artifacts (Unsigned)' - targetPath: '$(System.ArtifactsDirectory)' + displayName: 'Publish MAIN Build Artifacts (Unsigned)' + targetPath: '$(System.ArtifactsDirectory)/unsigned' artifactName: 'vsix-unsigned' + - output: pipelineArtifact + displayName: 'Publish TEST Build Artifacts (Unsigned)' + targetPath: '$(System.ArtifactsDirectory)/test-unsigned' + artifactName: 'vsix-test-unsigned' steps: - checkout: self - displayName: 'Checkout Extensions Repository' + displayName: 'Checkout repository' clean: true fetchTags: false - task: NodeTool@0 - displayName: 'Install Node.js' + displayName: 'Use Node.js 20.x' inputs: - versionSpec: '10.x' + versionSpec: '20.x' - task: Npm@1 - displayName: 'Install Dependencies' + displayName: 'Install dependencies' inputs: - command: 'install' + command: install verbose: false - task: Npm@1 displayName: 'Install TFX CLI' inputs: - command: 'custom' - customCommand: 'install -g tfx-cli' + command: custom + customCommand: 'install -g tfx-cli' - task: Npm@1 displayName: 'Install Gulp CLI' inputs: - command: 'custom' + command: custom customCommand: 'install -g gulp-cli' - task: PowerShell@2 - displayName: 'Build Extensions with Gulp' + displayName: 'Build (MAIN)' inputs: - targetType: 'inline' + targetType: inline script: 'gulp build' workingDirectory: '$(Build.SourcesDirectory)' - task: PowerShell@2 - displayName: 'Package Extensions with Gulp' + displayName: 'Package (MAIN)' + inputs: + targetType: inline + script: 'gulp package' + workingDirectory: '$(Build.SourcesDirectory)' + + - task: CopyFiles@2 + displayName: 'Copy MAIN VSIX to unsigned artifacts' + inputs: + SourceFolder: '$(Build.SourcesDirectory)/_package' + Contents: '${{ parameters.extensionName }}/*.vsix' + TargetFolder: '$(System.ArtifactsDirectory)/unsigned' + flattenFolders: true + + - task: PowerShell@2 + displayName: 'Build (TEST)' + inputs: + targetType: inline + script: 'gulp build --test' + workingDirectory: '$(Build.SourcesDirectory)' + + - task: PowerShell@2 + displayName: 'Package (TEST)' inputs: - targetType: 'inline' + targetType: inline script: 'gulp package' workingDirectory: '$(Build.SourcesDirectory)' - # Copy VSIX files to artifacts directory - task: CopyFiles@2 - displayName: 'Copy VSIX Files to Artifacts' + displayName: 'Copy TEST VSIX to test-unsigned artifacts' inputs: SourceFolder: '$(Build.SourcesDirectory)/_package' Contents: '${{ parameters.extensionName }}/*.vsix' - TargetFolder: '$(System.ArtifactsDirectory)' + TargetFolder: '$(System.ArtifactsDirectory)/test-unsigned' flattenFolders: true - - stage: CodeSigning + # ================================ ESRP Code Signing ================================ + - stage: ESRP_Code_Signing_Main_And_Test + displayName: 'ESRP Code Signing (Main + Test)' + dependsOn: Build_And_Package_Main_And_Test condition: or(eq(variables['IsMainBranchBuild'], 'true'), ${{ eq(parameters.forceCodeSign, 'true') }}, ${{ eq(parameters.simulateCodeSigningError, 'true') }}) - dependsOn: BuildAndTest jobs: - - job: CodeSigningJob - displayName: CodeSigning + - job: SignVsix + displayName: 'Sign VSIX: MAIN and TEST' templateContext: outputs: - output: pipelineArtifact - displayName: 'Signed vsix artifact' - targetPath: '$(System.ArtifactsDirectory)' - artifactName: vsix-signed + displayName: 'Publish Signed MAIN Artifact' + targetPath: '$(System.ArtifactsDirectory)/signed' + artifactName: 'vsix-signed' + - output: pipelineArtifact + displayName: 'Publish Signed TEST Artifact' + targetPath: '$(System.ArtifactsDirectory)/test-signed' + artifactName: 'vsix-test-signed' steps: - download: current artifact: vsix-unsigned - displayName: Download Artifact + displayName: 'Download unsigned (MAIN)' + + - download: current + artifact: vsix-test-unsigned + displayName: 'Download unsigned (TEST)' + - script: | echo "Simulated error in CodeSigning step." exit 1 - displayName: Simulate CodeSigning Error + displayName: 'Simulate CodeSigning error' condition: ${{ eq(parameters.simulateCodeSigningError, 'true') }} + + - task: EsrpCodeSigning@5 + displayName: 'ESRP Sign (MAIN)' + inputs: + ConnectedServiceName: '$(Control.EsrpServiceConnectionName)' + AppRegistrationClientId: '$(Control.AppRegistrationClientId)' + AppRegistrationTenantId: '$(Control.AppRegistrationTenantId)' + AuthAKVName: '$(Control.AuthAKVName)' + AuthCertName: '$(Control.AuthCertName)' + AuthSignCertName: '$(Control.AuthSignCertName)' + FolderPath: '$(Pipeline.Workspace)\vsix-unsigned' + Pattern: '*.vsix' + signConfigType: inlineSignParams + inlineOperation: |- + [ + {"KeyCode":"CP-500813","OperationCode":"AdoExtensionSign","ToolName":"sign","ToolVersion":"1.0","Parameters":{}}, + {"KeyCode":"CP-500813","OperationCode":"AdoExtensionVerify","ToolName":"sign","ToolVersion":"1.0","Parameters":{}} + ] + SessionTimeout: 30 + - task: CopyFiles@2 - displayName: 'Copy Files to: $(System.DefaultWorkingDirectory)' + displayName: 'Collect signed (MAIN)' inputs: SourceFolder: '$(Pipeline.Workspace)\vsix-unsigned' Contents: '*.vsix' - TargetFolder: $(System.DefaultWorkingDirectory) + TargetFolder: '$(System.ArtifactsDirectory)/signed' + - task: EsrpCodeSigning@5 - displayName: ESRP CodeSigning + displayName: 'ESRP Sign (TEST)' inputs: ConnectedServiceName: '$(Control.EsrpServiceConnectionName)' AppRegistrationClientId: '$(Control.AppRegistrationClientId)' @@ -191,60 +241,119 @@ extends: AuthAKVName: '$(Control.AuthAKVName)' AuthCertName: '$(Control.AuthCertName)' AuthSignCertName: '$(Control.AuthSignCertName)' - FolderPath: $(System.DefaultWorkingDirectory) + FolderPath: '$(Pipeline.Workspace)\vsix-test-unsigned' Pattern: '*.vsix' signConfigType: inlineSignParams inlineOperation: |- - [ - { - "KeyCode": "CP-500813", - "OperationCode": "AdoExtensionSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - }, - { - "KeyCode": "CP-500813", - "OperationCode": "AdoExtensionVerify", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } + [ + {"KeyCode":"CP-500813","OperationCode":"AdoExtensionSign","ToolName":"sign","ToolVersion":"1.0","Parameters":{}}, + {"KeyCode":"CP-500813","OperationCode":"AdoExtensionVerify","ToolName":"sign","ToolVersion":"1.0","Parameters":{}} ] SessionTimeout: 30 + - task: CopyFiles@2 - name: CopyFiles_5 - displayName: 'Copy Files to: $(System.ArtifactsDirectory)' + displayName: 'Collect signed (TEST)' inputs: + SourceFolder: '$(Pipeline.Workspace)\vsix-test-unsigned' Contents: '*.vsix' - TargetFolder: $(System.ArtifactsDirectory) + TargetFolder: '$(System.ArtifactsDirectory)/test-signed' + + # ======================== Approval: Publish TEST (private) ======================== + - stage: Approval_For_Test_Publish + displayName: 'Approval: Publish TEST (Private)' + dependsOn: ESRP_Code_Signing_Main_And_Test + condition: and(succeeded(), ${{ eq(parameters.publishExtension, 'true') }}) + jobs: + - job: ApproveTest + displayName: 'Manual Approval for TEST publish' + pool: server + steps: + - task: ManualValidation@0 + timeoutInMinutes: 1440 + inputs: + notifyUsers: 'razvanmanole@microsoft.com' + instructions: 'Approve this step to publish the TEST extension (private).' + + # ================================= Publish TEST (private) ============================ + - stage: Publish_Test_Extension_Private + displayName: 'Publish TEST Extension (Private Marketplace)' + dependsOn: Approval_For_Test_Publish + condition: and(succeeded(), ${{ eq(parameters.publishExtension, 'true') }}) + jobs: + - job: PublishTest + displayName: 'Publish TEST VSIX to Marketplace (Private)' + steps: + - download: current + artifact: vsix-test-signed + displayName: 'Download signed TEST artifact' + + - task: NodeTool@0 + displayName: 'Use Node.js 20.x' + inputs: + versionSpec: '20.x' + + - task: TfxInstaller@5 + displayName: 'Install TFX' + inputs: + version: 'v0.21.1' + + - task: 1ES.PublishAzureDevOpsExtension@1 + displayName: 'Publish TEST extension (private)' + inputs: + connectTo: AzureRM + connectedServiceNameAzureRM: '1es-extensions-publication-secure-service-connection' + fileType: vsix + vsixFile: '$(Pipeline.Workspace)/vsix-test-signed/*.vsix' + targetPath: '$(Pipeline.Workspace)/vsix-test-signed' + validateExtension: false + useV5: true + + # ======================== Approval: Publish PUBLIC ============================== + - stage: Approval_For_Public_Publish + displayName: 'Approval: Publish PUBLIC' + dependsOn: Publish_Test_Extension_Private + condition: and(succeeded(), ${{ eq(parameters.publishExtension, 'true') }}) + jobs: + - job: ApprovePublic + displayName: 'Manual Approval for PUBLIC publish' + pool: server + steps: + - task: ManualValidation@0 + timeoutInMinutes: 1440 + inputs: + notifyUsers: 'razvanmanole@microsoft.com' + instructions: 'Approve this step to publish the PUBLIC extension.' - - stage: PublishToMarketplace - dependsOn: CodeSigning + # ===================================== Publish PUBLIC ================================ + - stage: Publish_Public_Extension + displayName: 'Publish PUBLIC Extension (Marketplace)' + dependsOn: Approval_For_Public_Publish + condition: and(succeeded(), ${{ eq(parameters.publishExtension, 'true') }}) jobs: - - job: PublishVSIX - displayName: Publish vsix to marketplace + - job: PublishPublic + displayName: 'Publish PUBLIC VSIX to Marketplace' steps: - download: current artifact: vsix-signed - displayName: Download Signed Artifact + displayName: 'Download signed MAIN artifact' - task: NodeTool@0 - displayName: 'Install Node.js' + displayName: 'Use Node.js 20.x' inputs: - versionSpec: '10.x' + versionSpec: '20.x' - task: TfxInstaller@5 + displayName: 'Install TFX' inputs: version: 'v0.21.1' - task: 1ES.PublishAzureDevOpsExtension@1 - displayName: 'Publish the public extension to ms-vscs-rm' + displayName: 'Publish PUBLIC extension (publisher: ms-vscs-rm)' inputs: - connectTo: 'AzureRM' + connectTo: AzureRM connectedServiceNameAzureRM: '1es-extensions-publication-secure-service-connection' - fileType: 'vsix' + fileType: vsix vsixFile: '$(Pipeline.Workspace)/vsix-signed/*.vsix' targetPath: '$(Pipeline.Workspace)/vsix-signed' validateExtension: false - useV5: true \ No newline at end of file + useV5: true diff --git a/Extensions/Ansible/Src/vss-extension.json b/Extensions/Ansible/Src/vss-extension.json index 1a413fc27..eb2d8a3e1 100644 --- a/Extensions/Ansible/Src/vss-extension.json +++ b/Extensions/Ansible/Src/vss-extension.json @@ -3,7 +3,7 @@ "id": "vss-services-ansible", "name": "Ansible", "publisher": "ms-vscs-rm", - "version": "0.258.1", + "version": "0.261.2", "public": true, "description": "This extension executes an Ansible playbook using a specified inventory via command line interface", "_description.comment": "The below format to define extensions is currently in preview and may change in future.", diff --git a/Extensions/AzureExp/Src/vss-extension.json b/Extensions/AzureExp/Src/vss-extension.json index 311ca13ea..3345a254e 100644 --- a/Extensions/AzureExp/Src/vss-extension.json +++ b/Extensions/AzureExp/Src/vss-extension.json @@ -7,7 +7,7 @@ "description": "Azure Exp feature rollout", "public": false, "categories": [ - "Build and release" + "Azure Pipelines" ], "files": [ { diff --git a/Extensions/BitBucket/Src/vss-extension.json b/Extensions/BitBucket/Src/vss-extension.json index e3ab1294f..5608fe76b 100644 --- a/Extensions/BitBucket/Src/vss-extension.json +++ b/Extensions/BitBucket/Src/vss-extension.json @@ -7,7 +7,7 @@ "public": true, "description": "Tools related to connecting with Bitbucket", "_description.comment": "The below format to define artifact extensions is currently in preview and may change in future.", - "categories": [ "Integrate" ], + "categories": [ "Azure Pipelines" ], "Tags": [ "Bitbucket", "Release", "DevOps", "Artifacts" ], "targets": [ { diff --git a/Extensions/CircleCI/Src/vss-extension.json b/Extensions/CircleCI/Src/vss-extension.json index ad04f1ee8..a6565054b 100644 --- a/Extensions/CircleCI/Src/vss-extension.json +++ b/Extensions/CircleCI/Src/vss-extension.json @@ -3,11 +3,11 @@ "id": "vss-services-circleci-extension", "name": "CircleCI artifacts for Release Pipeline", "publisher": "ms-vscs-rm", - "version": "0.258.0", + "version": "0.261.0", "public": true, "description": "Tool for connecting with CircleCI", "_description.comment": "The below format to define artifact extensions is currently in preview and may change in future.", - "categories": [ "Build and release", "Azure pipelines" ], + "categories": [ "Azure Pipelines" ], "Tags": [ "CircleCI", "Release", "DevOps", "Artifacts", "Pipelines" ], "targets": [ { diff --git a/Extensions/ExternalTfs/Src/vss-extension.json b/Extensions/ExternalTfs/Src/vss-extension.json index 21d15c0e6..90d24dd75 100644 --- a/Extensions/ExternalTfs/Src/vss-extension.json +++ b/Extensions/ExternalTfs/Src/vss-extension.json @@ -7,7 +7,7 @@ "description": "Deploy external TFS/ Azure DevOps artifacts using Release Management", "_description.comment": "The below format to define artifact extensions is currently in preview and may change in future.", "public": true, - "categories": [ "Build and release" ], + "categories": [ "Azure Pipelines" ], "tags": [ "Artifacts", "DevOps", "Release" ], "targets": [ { diff --git a/Extensions/IISWebAppDeploy/Src/vss-extension.json b/Extensions/IISWebAppDeploy/Src/vss-extension.json index 6b773d9c7..7ab1a7b37 100644 --- a/Extensions/IISWebAppDeploy/Src/vss-extension.json +++ b/Extensions/IISWebAppDeploy/Src/vss-extension.json @@ -2,7 +2,7 @@ "manifestVersion": 1, "extensionId": "iiswebapp", "name": "IIS Web App Deployment Using WinRM", - "version": "1.258.0", + "version": "1.261.1", "publisher": "ms-vscs-rm", "description": "Using WinRM connect to the host Computer, to deploy a Web project using Web Deploy or a SQL DB using sqlpackage.exe.", "public": true, @@ -11,7 +11,7 @@ "large": "images/IIS_Web_App_Large.png" }, "categories": [ - "Build and release" + "Azure Pipelines" ], "tags": [ ], "links": { diff --git a/gulpfile.js b/gulpfile.js index 158795f03..c317e1e5c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -160,7 +160,7 @@ gulp.task("TaskModuleTest", gulp.series('copy:TaskModuleTest', function() { })); gulp.task('prepublish:TaskModulePublish', function (done) { - return del([TaskModulesTestRoot], done); + return del([TaskModulesTestRoot], done); }); gulp.task('TaskModulePublish', gulp.series('prepublish:TaskModulePublish', function (done) { @@ -651,7 +651,86 @@ function compileUIExtensions(extensionRoot) { }; } -gulp.task("build", gulp.series("compileNode")); +gulp.task("updateTestIds", function(cb) { + if (args.test) { + var buildExtensionsRoot = path.join(_buildRoot, 'Extensions'); + if (fs.existsSync(buildExtensionsRoot)) { + var extensionDirs = fs.readdirSync(buildExtensionsRoot).filter(function (file) { + return fs.statSync(path.join(buildExtensionsRoot, file)).isDirectory(); + }); + extensionDirs.forEach(function (extName) { + // Check multiple possible locations for vss-extension.json + var possiblePaths = [ + path.join(buildExtensionsRoot, extName, 'Src', 'vss-extension.json'), + path.join(buildExtensionsRoot, extName, 'src', 'vss-extension.json'), + path.join(buildExtensionsRoot, extName, 'vss-extension.json') + ]; + + var manifestPath = null; + for (var i = 0; i < possiblePaths.length; i++) { + if (fs.existsSync(possiblePaths[i])) { + manifestPath = possiblePaths[i]; + break; + } + } + + if (manifestPath) { + try { + // Read file and handle potential BOM + var fileContent = fs.readFileSync(manifestPath, 'utf8'); + // Remove BOM if present + if (fileContent.charCodeAt(0) === 0xFEFF) { + fileContent = fileContent.slice(1); + } + + var manifest = JSON.parse(fileContent); + var updated = false; + + // Check for both 'id' and 'extensionId' properties + var idProperty = manifest.id ? 'id' : (manifest.extensionId ? 'extensionId' : null); + + if (idProperty && manifest[idProperty] && !manifest[idProperty].endsWith('-test')) { + manifest[idProperty] = manifest[idProperty] + '-test'; + updated = true; + } + + // Update name property + if (manifest.hasOwnProperty('name')) { + manifest.name = `${manifest.name} (Test)`; + } + + // Always set public to false if it exists + if (manifest.hasOwnProperty('public') && manifest.public !== false) { + manifest.public = false; + updated = true; + } else if (!manifest.hasOwnProperty('public')) { + // Add public: false if it doesn't exist + manifest.public = false; + updated = true; + } + + if (updated) { + // Write back without BOM + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + console.log('Updated manifest for test build: ' + manifestPath); + console.log(' - ' + idProperty + ': ' + manifest[idProperty]); + console.log(' - Public: ' + manifest.public); + } else if (idProperty && manifest[idProperty].endsWith('-test')) { + console.log('Extension already has -test suffix: ' + manifestPath); + } + } catch (e) { + console.error('Failed to update manifest: ' + manifestPath + ' - ' + e.message); + } + } else { + console.log('No vss-extension.json found for extension: ' + extName); + } + }); + } + } + cb(); +}); + +gulp.task("build", gulp.series("compileNode", "updateTestIds")); gulp.task("handoff", function(cb) { handoff(cb);