diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml
index c2c644db8..da78d9334 100644
--- a/.azure-pipelines/release.yml
+++ b/.azure-pipelines/release.yml
@@ -1,4 +1,4 @@
-name: Release-$(Date:yyyyMMdd)$(Rev:.r)
+name: $(Date:yyyyMMdd)$(Rev:.r)
trigger: none
pr: none
@@ -12,21 +12,820 @@ resources:
parameters:
- name: 'esrp'
type: boolean
- default: false
+ default: true
displayName: 'Enable ESRP code signing'
+ - name: 'github'
+ type: boolean
+ default: true
+ displayName: 'Enable GitHub release publishing'
+ - name: 'nuget'
+ type: boolean
+ default: true
+ displayName: 'Enable NuGet package publishing'
+
+#
+# 1ES Pipeline Templates do not allow using a matrix strategy so we create
+# a YAML object parameter with and foreach to create jobs for each entry.
+# Each OS has its own matrix object since their build steps differ.
+#
+ - name: windows_matrix
+ type: object
+ default:
+ - id: windows_x64
+ jobName: 'Windows (x86)'
+ runtime: win-x86
+ pool: GitClientPME-1ESHostedPool-intel-pc
+ image: win-x86_64-ado1es
+ os: windows
+
+ - name: macos_matrix
+ type: object
+ default:
+ - id: macos_x64
+ jobName: 'macOS (x64)'
+ runtime: osx-x64
+ pool: 'Azure Pipelines'
+ image: macOS-latest
+ os: macos
+ - id: macos_arm64
+ jobName: 'macOS (ARM64)'
+ runtime: osx-arm64
+ pool: 'Azure Pipelines'
+ image: macOS-latest
+ os: macos
+
+ - name: linux_matrix
+ type: object
+ default:
+ - id: linux_x64
+ jobName: 'Linux (x64)'
+ runtime: linux-x64
+ pool: GitClientPME-1ESHostedPool-intel-pc
+ image: ubuntu-x86_64-ado1es
+ os: linux
+
+variables:
+ - name: 'esrpAppConnectionName'
+ value: '1ESGitClient-ESRP-App'
+ - name: 'esrpMIConnectionName'
+ value: '1ESGitClient-ESRP-MI'
+ - name: 'githubConnectionName'
+ value: 'GitHub-GitCredentialManager'
+ - name: 'nugetConnectionName'
+ value: '1ESGitClient-NuGet'
+ # ESRP signing variables set in the pipeline settings:
+ # - esrpEndpointUrl
+ # - esrpClientId
+ # - esrpTenantId
+ # - esrpKeyVaultName
+ # - esrpSignReqCertName
extends:
template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelines
parameters:
+ sdl:
+ # SDL source analysis tasks only run on Windows images
+ sourceAnalysisPool:
+ name: GitClientPME-1ESHostedPool-intel-pc
+ image: win-x86_64-ado1es
+ os: windows
stages:
- - stage: windows
- displayName: 'Windows'
+ - stage: build
+ displayName: 'Build and Sign'
jobs:
- - job: win_x86_build
- displayName: 'Windows Build and Sign (x86)'
+ #
+ # Windows build jobs
+ #
+ - ${{ each dim in parameters.windows_matrix }}:
+ - job: ${{ dim.id }}
+ displayName: ${{ dim.jobName }}
+ pool:
+ name: ${{ dim.pool }}
+ image: ${{ dim.image }}
+ os: ${{ dim.os }}
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ targetPath: '$(Build.ArtifactStagingDirectory)\_final'
+ artifactName: '${{ dim.runtime }}'
+ steps:
+ - checkout: self
+ - task: PowerShell@2
+ displayName: 'Read version file'
+ inputs:
+ targetType: inline
+ script: |
+ $version = (Get-Content .\VERSION) -replace '\.\d+$', ''
+ Write-Host "##vso[task.setvariable variable=version;isReadOnly=true]$version"
+ - task: UseDotNet@2
+ displayName: 'Use .NET 8 SDK'
+ inputs:
+ packageType: sdk
+ version: '8.x'
+ - task: PowerShell@2
+ displayName: 'Build payload'
+ inputs:
+ targetType: filePath
+ filePath: '.\src\windows\Installer.Windows\layout.ps1'
+ arguments: |
+ -Configuration Release `
+ -Output $(Build.ArtifactStagingDirectory)\payload `
+ -SymbolOutput $(Build.ArtifactStagingDirectory)\symbols_raw
+ - task: ArchiveFiles@2
+ displayName: 'Archive symbols'
+ inputs:
+ rootFolderOrFile: '$(Build.ArtifactStagingDirectory)\symbols_raw'
+ includeRootFolder: false
+ archiveType: zip
+ archiveFile: '$(Build.ArtifactStagingDirectory)\symbols\gcm-win-x86-$(version)-symbols.zip'
+ - task: EsrpCodeSigning@5
+ condition: and(succeeded(), eq('${{ parameters.esrp }}', true))
+ displayName: 'Sign payload'
+ inputs:
+ connectedServiceName: '$(esrpAppConnectionName)'
+ useMSIAuthentication: true
+ appRegistrationClientId: '$(esrpClientId)'
+ appRegistrationTenantId: '$(esrpTenantId)'
+ authAkvName: '$(esrpKeyVaultName)'
+ authSignCertName: '$(esrpSignReqCertName)'
+ serviceEndpointUrl: '$(esrpEndpointUrl)'
+ folderPath: '$(Build.ArtifactStagingDirectory)\payload'
+ pattern: |
+ **/*.exe
+ **/*.dll
+ useMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "KeyCode": "CP-230012",
+ "OperationCode": "SigntoolSign",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {
+ "OpusName": "Microsoft",
+ "OpusInfo": "https://www.microsoft.com",
+ "FileDigest": "/fd SHA256",
+ "PageHash": "/NPH",
+ "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+ }
+ },
+ {
+ "KeyCode": "CP-230012",
+ "OperationCode": "SigntoolVerify",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {}
+ }
+ ]
+ - task: PowerShell@2
+ displayName: 'Build installers'
+ inputs:
+ targetType: inline
+ script: |
+ dotnet build '.\src\windows\Installer.Windows\Installer.Windows.csproj' `
+ --configuration Release `
+ --no-dependencies `
+ -p:NoLayout=true `
+ -p:PayloadPath="$(Build.ArtifactStagingDirectory)\payload" `
+ -p:OutputPath="$(Build.ArtifactStagingDirectory)\installers"
+ - task: EsrpCodeSigning@5
+ condition: and(succeeded(), eq('${{ parameters.esrp }}', true))
+ displayName: 'Sign installers'
+ inputs:
+ connectedServiceName: '$(esrpAppConnectionName)'
+ useMSIAuthentication: true
+ appRegistrationClientId: '$(esrpClientId)'
+ appRegistrationTenantId: '$(esrpTenantId)'
+ authAkvName: '$(esrpKeyVaultName)'
+ authSignCertName: '$(esrpSignReqCertName)'
+ serviceEndpointUrl: '$(esrpEndpointUrl)'
+ folderPath: '$(Build.ArtifactStagingDirectory)\installers'
+ pattern: '**/*.exe'
+ useMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "KeyCode": "CP-230012",
+ "OperationCode": "SigntoolSign",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {
+ "OpusName": "Microsoft",
+ "OpusInfo": "https://www.microsoft.com",
+ "FileDigest": "/fd SHA256",
+ "PageHash": "/NPH",
+ "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+ }
+ },
+ {
+ "KeyCode": "CP-230012",
+ "OperationCode": "SigntoolVerify",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {}
+ }
+ ]
+ - task: ArchiveFiles@2
+ displayName: 'Archive signed payload'
+ inputs:
+ rootFolderOrFile: '$(Build.ArtifactStagingDirectory)\payload'
+ includeRootFolder: false
+ archiveType: zip
+ archiveFile: '$(Build.ArtifactStagingDirectory)\installers\gcm-win-x86-$(version).zip'
+ - task: PowerShell@2
+ displayName: 'Collect artifacts for publishing'
+ inputs:
+ targetType: inline
+ script: |
+ New-Item -Path "$(Build.ArtifactStagingDirectory)\_final" -ItemType Directory -Force
+ Copy-Item "$(Build.ArtifactStagingDirectory)\installers\*.exe" -Destination "$(Build.ArtifactStagingDirectory)\_final"
+ Copy-Item "$(Build.ArtifactStagingDirectory)\installers\*.zip" -Destination "$(Build.ArtifactStagingDirectory)\_final"
+ Copy-Item "$(Build.ArtifactStagingDirectory)\symbols\*.zip" -Destination "$(Build.ArtifactStagingDirectory)\_final"
+ Copy-Item "$(Build.ArtifactStagingDirectory)\payload" -Destination "$(Build.ArtifactStagingDirectory)\_final" -Recurse
+
+ #
+ # macOS build jobs
+ #
+ - ${{ each dim in parameters.macos_matrix }}:
+ - job: ${{ dim.id }}
+ displayName: ${{ dim.jobName }}
+ pool:
+ name: ${{ dim.pool }}
+ image: ${{ dim.image }}
+ os: ${{ dim.os }}
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ targetPath: '$(Build.ArtifactStagingDirectory)/_final'
+ artifactName: '${{ dim.runtime }}'
+ steps:
+ - checkout: self
+ - task: Bash@3
+ displayName: 'Read version file'
+ inputs:
+ targetType: inline
+ script: |
+ echo "##vso[task.setvariable variable=version;isReadOnly=true]$(cat ./VERSION | sed -E 's/.[0-9]+$//')"
+ - task: UseDotNet@2
+ displayName: 'Use .NET 8 SDK'
+ inputs:
+ packageType: sdk
+ version: '8.x'
+ - task: Bash@3
+ displayName: 'Build payload'
+ inputs:
+ targetType: filePath
+ filePath: './src/osx/Installer.Mac/layout.sh'
+ arguments: |
+ --runtime="${{ dim.runtime }}" \
+ --configuration="Release" \
+ --output="$(Build.ArtifactStagingDirectory)/payload" \
+ --symbol-output="$(Build.ArtifactStagingDirectory)/symbols_raw"
+ - task: ArchiveFiles@2
+ displayName: 'Archive symbols'
+ inputs:
+ rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/symbols_raw'
+ includeRootFolder: false
+ archiveType: tar
+ tarCompression: gz
+ archiveFile: '$(Build.ArtifactStagingDirectory)/symbols/gcm-${{ dim.runtime }}-$(version)-symbols.tar.gz'
+ - task: AzureKeyVault@2
+ displayName: 'Download developer certificate'
+ inputs:
+ azureSubscription: '$(esrpMIConnectionName)'
+ keyVaultName: '$(esrpKeyVaultName)'
+ secretsFilter: 'mac-developer-certificate,mac-developer-certificate-password,mac-developer-certificate-identity'
+ - task: Bash@3
+ displayName: 'Import developer certificate'
+ inputs:
+ targetType: inline
+ script: |
+ # Create and unlock a keychain for the developer certificate
+ security create-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain
+ security default-keychain -s $(Agent.TempDirectory)/buildagent.keychain
+ security unlock-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain
+
+ echo $(mac-developer-certificate) | base64 -D > $(Agent.TempDirectory)/cert.p12
+ echo $(mac-developer-certificate-password) > $(Agent.TempDirectory)/cert.password
+
+ # Import the developer certificate
+ security import $(Agent.TempDirectory)/cert.p12 \
+ -k $(Agent.TempDirectory)/buildagent.keychain \
+ -P "$(mac-developer-certificate-password)" \
+ -T /usr/bin/codesign
+
+ # Clean up the cert file immediately after import
+ rm $(Agent.TempDirectory)/cert.p12
+
+ # Set ACLs to allow codesign to access the private key
+ security set-key-partition-list \
+ -S apple-tool:,apple:,codesign: \
+ -s -k pwd \
+ $(Agent.TempDirectory)/buildagent.keychain
+ - task: Bash@3
+ displayName: 'Developer sign payload files'
+ inputs:
+ targetType: inline
+ script: |
+ mkdir -p $(Build.ArtifactStagingDirectory)/tosign/payload
+
+ # Copy the files that need signing (Mach-o executables and dylibs)
+ pushd $(Build.ArtifactStagingDirectory)/payload
+ find . -type f -exec file --mime {} + \
+ | sed -n '/mach/s/: .*//p' \
+ | while IFS= read -r f; do
+ rel="${f#./}"
+ tgt="$(Build.ArtifactStagingDirectory)/tosign/payload/$rel"
+ mkdir -p "$(dirname "$tgt")"
+ cp -- "$f" "$tgt"
+ done
+ popd
+
+ # Developer sign the files
+ ./src/osx/Installer.Mac/codesign.sh \
+ "$(Build.ArtifactStagingDirectory)/tosign/payload" \
+ "$(mac-developer-certificate-identity)" \
+ "$PWD/src/osx/Installer.Mac/entitlements.xml"
+ # ESRP code signing for macOS requires the files be packaged in a zip file for submission
+ - task: ArchiveFiles@2
+ displayName: 'Archive files for signing'
+ inputs:
+ rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/tosign/payload'
+ includeRootFolder: false
+ archiveType: zip
+ archiveFile: '$(Build.ArtifactStagingDirectory)/tosign/payload.zip'
+ - task: EsrpCodeSigning@5
+ condition: and(succeeded(), eq('${{ parameters.esrp }}', true))
+ displayName: 'Sign payload'
+ inputs:
+ connectedServiceName: '$(esrpAppConnectionName)'
+ useMSIAuthentication: true
+ appRegistrationClientId: '$(esrpClientId)'
+ appRegistrationTenantId: '$(esrpTenantId)'
+ authAkvName: '$(esrpKeyVaultName)'
+ authSignCertName: '$(esrpSignReqCertName)'
+ serviceEndpointUrl: '$(esrpEndpointUrl)'
+ folderPath: '$(Build.ArtifactStagingDirectory)/tosign'
+ pattern: 'payload.zip'
+ useMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "KeyCode": "CP-401337-Apple",
+ "OperationCode": "MacAppDeveloperSign",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {
+ "Hardening": "Enable"
+ }
+ }
+ ]
+ # Extract signed files, overwriting the unsigned files, ready for packaging
+ - task: Bash@3
+ displayName: 'Extract signed payload files'
+ inputs:
+ targetType: inline
+ script: |
+ unzip -uo $(Build.ArtifactStagingDirectory)/tosign/payload.zip -d $(Build.ArtifactStagingDirectory)/payload
+ - task: Bash@3
+ displayName: 'Build component package'
+ inputs:
+ targetType: filePath
+ filePath: './src/osx/Installer.Mac/pack.sh'
+ arguments: |
+ --version="$(version)" \
+ --payload="$(Build.ArtifactStagingDirectory)/payload" \
+ --output="$(Build.ArtifactStagingDirectory)/pkg/com.microsoft.gitcredentialmanager.component.pkg"
+ - task: Bash@3
+ displayName: 'Build installer package'
+ inputs:
+ targetType: filePath
+ filePath: './src/osx/Installer.Mac/dist.sh'
+ arguments: |
+ --version="$(version)" \
+ --runtime="${{ dim.runtime }}" \
+ --package-path="$(Build.ArtifactStagingDirectory)/pkg" \
+ --output="$(Build.ArtifactStagingDirectory)/installers-presign/gcm-${{ dim.runtime }}-$(version).pkg"
+ # ESRP code signing for macOS requires the files be packaged in a zip file first
+ - task: Bash@3
+ displayName: 'Prepare installer package for signing'
+ inputs:
+ targetType: inline
+ script: |
+ mkdir -p $(Build.ArtifactStagingDirectory)/tosign
+ cd $(Build.ArtifactStagingDirectory)/installers-presign
+ zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers-presign.zip *.pkg
+ - task: EsrpCodeSigning@5
+ condition: and(succeeded(), eq('${{ parameters.esrp }}', true))
+ displayName: 'Sign installer package'
+ inputs:
+ connectedServiceName: '$(esrpAppConnectionName)'
+ useMSIAuthentication: true
+ appRegistrationClientId: '$(esrpClientId)'
+ appRegistrationTenantId: '$(esrpTenantId)'
+ authAkvName: '$(esrpKeyVaultName)'
+ authSignCertName: '$(esrpSignReqCertName)'
+ serviceEndpointUrl: '$(esrpEndpointUrl)'
+ folderPath: '$(Build.ArtifactStagingDirectory)/tosign'
+ pattern: 'installers-presign.zip'
+ useMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "KeyCode": "CP-401337-Apple",
+ "OperationCode": "MacAppDeveloperSign",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {
+ "Hardening": "Enable"
+ }
+ }
+ ]
+ # Extract signed installer, overwriting the unsigned installer
+ - task: Bash@3
+ displayName: 'Extract signed installer package'
+ inputs:
+ targetType: inline
+ script: |
+ unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers-presign.zip -d $(Build.ArtifactStagingDirectory)/installers
+ - task: Bash@3
+ displayName: 'Prepare installer package for notarization'
+ inputs:
+ targetType: inline
+ script: |
+ mkdir -p $(Build.ArtifactStagingDirectory)/tosign
+ cd $(Build.ArtifactStagingDirectory)/installers
+ zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg
+ - task: EsrpCodeSigning@5
+ condition: and(succeeded(), eq('${{ parameters.esrp }}', true))
+ displayName: 'Notarize installer package'
+ inputs:
+ connectedServiceName: '$(esrpAppConnectionName)'
+ useMSIAuthentication: true
+ appRegistrationClientId: '$(esrpClientId)'
+ appRegistrationTenantId: '$(esrpTenantId)'
+ authAkvName: '$(esrpKeyVaultName)'
+ authSignCertName: '$(esrpSignReqCertName)'
+ serviceEndpointUrl: '$(esrpEndpointUrl)'
+ folderPath: '$(Build.ArtifactStagingDirectory)/tosign'
+ pattern: 'installers.zip'
+ useMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "KeyCode": "CP-401337-Apple",
+ "OperationCode": "MacAppNotarize",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {
+ "BundleId": "com.microsoft.gitcredentialmanager"
+ }
+ }
+ ]
+ # Extract signed and notarized installer pkg files, overwriting the unsigned files, ready for upload
+ - task: Bash@3
+ displayName: 'Extract signed and notarized installer package'
+ inputs:
+ targetType: inline
+ script: |
+ unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers
+ - task: ArchiveFiles@2
+ displayName: 'Archive signed payload'
+ inputs:
+ rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/payload'
+ includeRootFolder: false
+ archiveType: tar
+ tarCompression: gz
+ archiveFile: '$(Build.ArtifactStagingDirectory)/installers/gcm-${{ dim.runtime }}-$(version).tar.gz'
+ - task: Bash@3
+ displayName: 'Collect artifacts for publishing'
+ inputs:
+ targetType: inline
+ script: |
+ mkdir -p $(Build.ArtifactStagingDirectory)/_final
+ cp $(Build.ArtifactStagingDirectory)/installers/*.pkg $(Build.ArtifactStagingDirectory)/_final
+ cp $(Build.ArtifactStagingDirectory)/installers/*.tar.gz $(Build.ArtifactStagingDirectory)/_final
+ cp $(Build.ArtifactStagingDirectory)/symbols/*.tar.gz $(Build.ArtifactStagingDirectory)/_final
+ cp -r $(Build.ArtifactStagingDirectory)/payload $(Build.ArtifactStagingDirectory)/_final
+
+ #
+ # Linux build jobs
+ #
+ - ${{ each dim in parameters.linux_matrix }}:
+ - job: ${{ dim.id }}
+ displayName: ${{ dim.jobName }}
+ pool:
+ name: ${{ dim.pool }}
+ image: ${{ dim.image }}
+ os: ${{ dim.os }}
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ targetPath: '$(Build.ArtifactStagingDirectory)/_final'
+ artifactName: '${{ dim.runtime }}'
+ steps:
+ - checkout: self
+ - task: Bash@3
+ displayName: 'Read version file'
+ inputs:
+ targetType: inline
+ script: |
+ echo "##vso[task.setvariable variable=version;isReadOnly=true]$(cat ./VERSION | sed -E 's/.[0-9]+$//')"
+ - task: UseDotNet@2
+ displayName: 'Use .NET 8 SDK'
+ inputs:
+ packageType: sdk
+ version: '8.x'
+ - task: Bash@3
+ displayName: 'Build payload'
+ inputs:
+ targetType: filePath
+ filePath: './src/linux/Packaging.Linux/layout.sh'
+ arguments: |
+ --runtime="${{ dim.runtime }}" \
+ --configuration="Release" \
+ --output="$(Build.ArtifactStagingDirectory)/payload" \
+ --symbol-output="$(Build.ArtifactStagingDirectory)/symbols_raw"
+ - task: Bash@3
+ displayName: 'Build packages'
+ inputs:
+ targetType: filePath
+ filePath: './src/linux/Packaging.Linux/pack.sh'
+ arguments: |
+ --version="$(version)" \
+ --runtime="${{ dim.runtime }}" \
+ --payload="$(Build.ArtifactStagingDirectory)/payload" \
+ --symbols="$(Build.ArtifactStagingDirectory)/symbols_raw" \
+ --output="$(Build.ArtifactStagingDirectory)/pkg"
+ - task: Bash@3
+ displayName: 'Move packages'
+ inputs:
+ targetType: inline
+ script: |
+ # Move symbols
+ mkdir -p $(Build.ArtifactStagingDirectory)/symbols
+ mv $(Build.ArtifactStagingDirectory)/pkg/tar/gcm-*-symbols.tar.gz $(Build.ArtifactStagingDirectory)/symbols
+
+ # Move binary packages
+ mkdir -p $(Build.ArtifactStagingDirectory)/installers
+ mv $(Build.ArtifactStagingDirectory)/pkg/tar/*.tar.gz $(Build.ArtifactStagingDirectory)/installers
+ mv $(Build.ArtifactStagingDirectory)/pkg/deb/*.deb $(Build.ArtifactStagingDirectory)/installers
+ - task: EsrpCodeSigning@5
+ condition: and(succeeded(), eq('${{ parameters.esrp }}', true))
+ displayName: 'Sign Debian package'
+ inputs:
+ connectedServiceName: '$(esrpAppConnectionName)'
+ useMSIAuthentication: true
+ appRegistrationClientId: '$(esrpClientId)'
+ appRegistrationTenantId: '$(esrpTenantId)'
+ authAkvName: '$(esrpKeyVaultName)'
+ authSignCertName: '$(esrpSignReqCertName)'
+ serviceEndpointUrl: '$(esrpEndpointUrl)'
+ folderPath: '$(Build.ArtifactStagingDirectory)/installers'
+ pattern: |
+ **/*.deb
+ useMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "KeyCode": "CP-453387-Pgp",
+ "OperationCode": "LinuxSign",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {}
+ }
+ ]
+ - task: Bash@3
+ displayName: 'Collect artifacts for publishing'
+ inputs:
+ targetType: inline
+ script: |
+ mkdir -p $(Build.ArtifactStagingDirectory)/_final
+ cp $(Build.ArtifactStagingDirectory)/installers/*.deb $(Build.ArtifactStagingDirectory)/_final
+ cp $(Build.ArtifactStagingDirectory)/installers/*.tar.gz $(Build.ArtifactStagingDirectory)/_final
+ cp $(Build.ArtifactStagingDirectory)/symbols/*.tar.gz $(Build.ArtifactStagingDirectory)/_final
+ cp -r $(Build.ArtifactStagingDirectory)/payload $(Build.ArtifactStagingDirectory)/_final
+
+ #
+ # .NET Tool build job
+ #
+ - job: dotnet_tool
+ displayName: '.NET Tool NuGet Package'
pool:
- name: GitClient-1ESHostedPool-intel-pc
+ name: GitClientPME-1ESHostedPool-intel-pc
image: win-x86_64-ado1es
os: windows
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ targetPath: '$(Build.ArtifactStagingDirectory)/packages'
+ artifactName: 'dotnet-tool'
steps:
- checkout: self
+ - task: PowerShell@2
+ displayName: 'Read version file'
+ inputs:
+ targetType: inline
+ script: |
+ $version = (Get-Content .\VERSION) -replace '\.\d+$', ''
+ Write-Host "##vso[task.setvariable variable=version;isReadOnly=true]$version"
+ - task: UseDotNet@2
+ displayName: 'Use .NET 8 SDK'
+ inputs:
+ packageType: sdk
+ version: '8.x'
+ - task: NuGetToolInstaller@1
+ displayName: 'Install NuGet CLI'
+ inputs:
+ versionSpec: '>= 6.0'
+ - task: PowerShell@2
+ displayName: 'Build payload'
+ inputs:
+ targetType: filePath
+ filePath: './src/shared/DotnetTool/layout.ps1'
+ arguments: |
+ -Configuration Release `
+ -Output "$(Build.ArtifactStagingDirectory)/nupkg"
+ - task: EsrpCodeSigning@5
+ condition: and(succeeded(), eq('${{ parameters.esrp }}', true))
+ displayName: 'Sign payload'
+ inputs:
+ connectedServiceName: '$(esrpAppConnectionName)'
+ useMSIAuthentication: true
+ appRegistrationClientId: '$(esrpClientId)'
+ appRegistrationTenantId: '$(esrpTenantId)'
+ authAkvName: '$(esrpKeyVaultName)'
+ authSignCertName: '$(esrpSignReqCertName)'
+ serviceEndpointUrl: '$(esrpEndpointUrl)'
+ folderPath: '$(Build.ArtifactStagingDirectory)/nupkg'
+ pattern: |
+ **/*.exe
+ **/*.dll
+ useMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "KeyCode": "CP-230012",
+ "OperationCode": "SigntoolSign",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {
+ "OpusName": "Microsoft",
+ "OpusInfo": "https://www.microsoft.com",
+ "FileDigest": "/fd SHA256",
+ "PageHash": "/NPH",
+ "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+ }
+ },
+ {
+ "KeyCode": "CP-230012",
+ "OperationCode": "SigntoolVerify",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {}
+ }
+ ]
+ - task: PowerShell@2
+ displayName: 'Create NuGet packages'
+ inputs:
+ targetType: filePath
+ filePath: './src/shared/DotnetTool/pack.ps1'
+ arguments: |
+ -Configuration Release `
+ -Version "$(version)" `
+ -PackageRoot "$(Build.ArtifactStagingDirectory)/nupkg" `
+ -Output "$(Build.ArtifactStagingDirectory)/packages"
+ - task: EsrpCodeSigning@5
+ condition: and(succeeded(), eq('${{ parameters.esrp }}', true))
+ displayName: 'Sign NuGet packages'
+ inputs:
+ connectedServiceName: '$(esrpAppConnectionName)'
+ useMSIAuthentication: true
+ appRegistrationClientId: '$(esrpClientId)'
+ appRegistrationTenantId: '$(esrpTenantId)'
+ authAkvName: '$(esrpKeyVaultName)'
+ authSignCertName: '$(esrpSignReqCertName)'
+ serviceEndpointUrl: '$(esrpEndpointUrl)'
+ folderPath: '$(Build.ArtifactStagingDirectory)/packages'
+ pattern: |
+ **/*.nupkg
+ **/*.snupkg
+ useMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "KeyCode": "CP-401405",
+ "OperationCode": "NuGetSign",
+ "ToolName": "sign",
+ "ToolVersion": "1.0",
+ "Parameters": {}
+ }
+ ]
+
+ - stage: release
+ displayName: 'Release'
+ dependsOn: [build]
+ condition: and(succeeded(), or(eq('${{ parameters.github }}', true), eq('${{ parameters.nuget }}', true)))
+ jobs:
+ - job: release_validation
+ displayName: 'Release validation'
+ pool:
+ name: GitClientPME-1ESHostedPool-intel-pc
+ image: ubuntu-x86_64-ado1es
+ os: linux
+ steps:
+ - task: Bash@3
+ displayName: 'Read version file'
+ name: version
+ inputs:
+ targetType: inline
+ script: |
+ echo "##vso[task.setvariable variable=value;isOutput=true;isReadOnly=true]$(cat ./VERSION | sed -E 's/.[0-9]+$//')"
+
+ - job: github
+ displayName: 'Publish GitHub release'
+ dependsOn: release_validation
+ condition: and(succeeded(), eq('${{ parameters.github }}', true))
+ pool:
+ name: GitClientPME-1ESHostedPool-intel-pc
+ image: ubuntu-x86_64-ado1es
+ os: linux
+ variables:
+ version: $[dependencies.release_validation.outputs['version.value']]
+ templateContext:
+ type: releaseJob
+ isProduction: true
+ inputs:
+ # Installers and packages
+ - input: pipelineArtifact
+ artifactName: 'win-x86'
+ targetPath: $(Pipeline.Workspace)/assets/win-x86
+ - input: pipelineArtifact
+ artifactName: 'osx-x64'
+ targetPath: $(Pipeline.Workspace)/assets/osx-x64
+ - input: pipelineArtifact
+ artifactName: 'osx-arm64'
+ targetPath: $(Pipeline.Workspace)/assets/osx-arm64
+ - input: pipelineArtifact
+ artifactName: 'linux-x64'
+ targetPath: $(Pipeline.Workspace)/assets/linux-x64
+ - input: pipelineArtifact
+ artifactName: 'dotnet-tool'
+ targetPath: $(Pipeline.Workspace)/assets/dotnet-tool
+ steps:
+ - task: GitHubRelease@1
+ displayName: 'Create Draft GitHub Release'
+ condition: and(succeeded(), eq('${{ parameters.github }}', true))
+ inputs:
+ gitHubConnection: $(githubConnectionName)
+ repositoryName: git-ecosystem/git-credential-manager
+ tag: 'v$(version)'
+ tagSource: userSpecifiedTag
+ target: release
+ title: 'GCM $(version)'
+ isDraft: true
+ addChangeLog: false
+ assets: |
+ $(Pipeline.Workspace)/assets/win-x86/*.exe
+ $(Pipeline.Workspace)/assets/win-x86/*.zip
+ $(Pipeline.Workspace)/assets/osx-x64/*.pkg
+ $(Pipeline.Workspace)/assets/osx-x64/*.tar.gz
+ $(Pipeline.Workspace)/assets/osx-arm64/*.pkg
+ $(Pipeline.Workspace)/assets/osx-arm64/*.tar.gz
+ $(Pipeline.Workspace)/assets/linux-x64/*.deb
+ $(Pipeline.Workspace)/assets/linux-x64/*.tar.gz
+ $(Pipeline.Workspace)/assets/dotnet-tool/*.nupkg
+ $(Pipeline.Workspace)/assets/dotnet-tool/*.snupkg
+
+ - job: nuget
+ displayName: 'Publish NuGet package'
+ dependsOn: release_validation
+ condition: and(succeeded(), eq('${{ parameters.nuget }}', true))
+ pool:
+ name: GitClientPME-1ESHostedPool-intel-pc
+ image: ubuntu-x86_64-ado1es
+ os: linux
+ variables:
+ version: $[dependencies.release_validation.outputs['version.value']]
+ templateContext:
+ inputs:
+ - input: pipelineArtifact
+ artifactName: 'dotnet-tool'
+ targetPath: $(Pipeline.Workspace)/assets/dotnet-tool
+ outputs:
+ - output: nuget
+ condition: and(succeeded(), eq('${{ parameters.nuget }}', true))
+ displayName: 'Publish .NET Tool NuGet package'
+ packagesToPush: '$(Pipeline.Workspace)/assets/dotnet-tool/*.nupkg;$(Pipeline.Workspace)/assets/dotnet-tool/*.snupkg'
+ packageParentPath: $(Pipeline.Workspace)/assets/dotnet-tool
+ nuGetFeedType: external
+ publishPackageMetadata: true
+ publishFeedCredentials: $(nugetConnectionName)
diff --git a/.github/workflows/release-dotnet-tool.yaml b/.github/workflows/release-dotnet-tool.yaml
deleted file mode 100644
index 594a2f4a3..000000000
--- a/.github/workflows/release-dotnet-tool.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: release-dotnet-tool
-on:
- release:
- types: [released]
-
-jobs:
- release:
- runs-on: windows-latest
- environment: release
- steps:
- - name: Download NuGet package from release and publish
- run: |
- # Get asset information
- $github = Get-Content '${{ github.event_path }}' | ConvertFrom-Json
- $asset = $github.release.assets | Where-Object -Property name -match '.nupkg$'
-
- # Download asset
- Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $asset.name
-
- # Publish asset
- dotnet nuget push $asset.name --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
- shell: powershell
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index 2640fe21f..000000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,672 +0,0 @@
-name: release
-
-on:
- workflow_dispatch:
-
-permissions:
- id-token: write
- contents: write
-
-jobs:
- prereqs:
- name: Prerequisites
- runs-on: ubuntu-latest
- outputs:
- version: ${{ steps.version.outputs.version }}
- steps:
- - uses: actions/checkout@v5
-
- - name: Set version
- run: echo "version=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_OUTPUT
- id: version
-
-# ================================
-# macOS
-# ================================
- create-macos-artifacts:
- name: Create macOS artifacts
- runs-on: macos-latest
- environment: release
- needs: prereqs
- strategy:
- matrix:
- runtime: [ osx-x64, osx-arm64 ]
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v5.0.0
- with:
- dotnet-version: 8.0.x
-
- - name: Build
- run: |
- dotnet build src/osx/Installer.Mac/*.csproj \
- --configuration=MacRelease --no-self-contained \
- --runtime=${{ matrix.runtime }}
-
- - name: Run macOS unit tests
- run: |
- dotnet test --configuration=MacRelease
-
- - name: Lay out payload and symbols
- run: |
- src/osx/Installer.Mac/layout.sh \
- --configuration=MacRelease --output=payload \
- --symbol-output=symbols --runtime=${{ matrix.runtime }}
-
- - name: Set up signing/notarization infrastructure
- env:
- A1: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }}
- A2: ${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}
- I1: ${{ secrets.INSTALLER_CERTIFICATE_BASE64 }}
- I2: ${{ secrets.INSTALLER_CERTIFICATE_PASSWORD }}
- N1: ${{ secrets.APPLE_TEAM_ID }}
- N2: ${{ secrets.APPLE_DEVELOPER_ID }}
- N3: ${{ secrets.APPLE_DEVELOPER_PASSWORD }}
- N4: ${{ secrets.APPLE_KEYCHAIN_PROFILE }}
- run: |
- echo "Setting up signing certificates"
- security create-keychain -p pwd $RUNNER_TEMP/buildagent.keychain
- security default-keychain -s $RUNNER_TEMP/buildagent.keychain
- security unlock-keychain -p pwd $RUNNER_TEMP/buildagent.keychain
-
- echo $A1 | base64 -D > $RUNNER_TEMP/cert.p12
- security import $RUNNER_TEMP/cert.p12 \
- -k $RUNNER_TEMP/buildagent.keychain \
- -P $A2 \
- -T /usr/bin/codesign
- security set-key-partition-list \
- -S apple-tool:,apple:,codesign: \
- -s -k pwd \
- $RUNNER_TEMP/buildagent.keychain
-
- echo $I1 | base64 -D > $RUNNER_TEMP/cert.p12
- security import $RUNNER_TEMP/cert.p12 \
- -k $RUNNER_TEMP/buildagent.keychain \
- -P $I2 \
- -T /usr/bin/productbuild
- security set-key-partition-list \
- -S apple-tool:,apple:,productbuild: \
- -s -k pwd \
- $RUNNER_TEMP/buildagent.keychain
-
- echo "Setting up notarytool"
- xcrun notarytool store-credentials \
- --team-id $N1 \
- --apple-id $N2 \
- --password $N3 \
- "$N4"
-
- - name: Run codesign against payload
- env:
- A3: ${{ secrets.APPLE_APPLICATION_SIGNING_IDENTITY }}
- run: |
- ./src/osx/Installer.Mac/codesign.sh "payload" "$A3" \
- "$GITHUB_WORKSPACE/src/osx/Installer.Mac/entitlements.xml"
-
- - name: Create component package
- run: |
- src/osx/Installer.Mac/pack.sh --payload="payload" \
- --version="${{ needs.prereqs.outputs.version }}" \
- --output="components/com.microsoft.gitcredentialmanager.component.pkg"
-
- - name: Create and sign product archive
- env:
- I3: ${{ secrets.APPLE_INSTALLER_SIGNING_IDENTITY }}
- run: |
- src/osx/Installer.Mac/dist.sh --package-path=components \
- --version="${{ needs.prereqs.outputs.version }}" \
- --runtime="${{ matrix.runtime }}" \
- --output="pkg/gcm-${{ matrix.runtime }}-${{ needs.prereqs.outputs.version }}.pkg" \
- --identity="$I3" || exit 1
-
- - name: Notarize product archive
- env:
- N4: ${{ secrets.APPLE_KEYCHAIN_PROFILE }}
- run: |
- src/osx/Installer.Mac/notarize.sh \
- --package="pkg/gcm-${{ matrix.runtime }}-${{ needs.prereqs.outputs.version }}.pkg" \
- --keychain-profile="$N4"
-
- - name: Upload artifacts
- uses: actions/upload-artifact@v5
- with:
- name: macos-${{ matrix.runtime }}-artifacts
- path: |
- ./pkg/*
- ./symbols/*
- ./payload/*
-
-# ================================
-# Windows
-# ================================
- create-windows-artifacts:
- name: Create Windows Artifacts
- runs-on: windows-latest
- environment: release
- needs: prereqs
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v5.0.0
- with:
- dotnet-version: 8.0.x
-
- - name: Build
- run: |
- dotnet build --configuration=WindowsRelease
-
- - name: Run Windows unit tests
- run: |
- dotnet test --configuration=WindowsRelease
-
- - name: Lay out Windows payload and symbols
- run: |
- cd $env:GITHUB_WORKSPACE\src\windows\Installer.Windows\
- ./layout.ps1 -Configuration WindowsRelease `
- -Output $env:GITHUB_WORKSPACE\payload `
- -SymbolOutput $env:GITHUB_WORKSPACE\symbols
-
- - name: Log into Azure
- uses: azure/login@v2
- with:
- client-id: ${{ secrets.AZURE_CLIENT_ID }}
- tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
-
- - name: Sign payload files with Azure Code Signing
- uses: azure/trusted-signing-action@v0.5.10
- with:
- endpoint: https://wus2.codesigning.azure.net/
- trusted-signing-account-name: git-fundamentals-signing
- certificate-profile-name: git-fundamentals-windows-signing
- files-folder: ${{ github.workspace }}\payload
- files-folder-filter: exe,dll
- file-digest: SHA256
- timestamp-rfc3161: http://timestamp.acs.microsoft.com
- timestamp-digest: SHA256
-
- # The Azure Code Signing action overrides the .NET version, so we reset it.
- - name: Set up .NET
- uses: actions/setup-dotnet@v5.0.0
- with:
- dotnet-version: 8.0.x
-
- - name: Build with signed payload
- run: |
- dotnet build $env:GITHUB_WORKSPACE\src\windows\Installer.Windows `
- /p:PayloadPath=$env:GITHUB_WORKSPACE\payload /p:NoLayout=true `
- --configuration=WindowsRelease
- mkdir installers
- Move-Item -Path .\out\windows\Installer.Windows\bin\Release\net472\*.exe `
- -Destination $env:GITHUB_WORKSPACE\installers
-
- - name: Sign installers with Azure Code Signing
- uses: azure/trusted-signing-action@v0.5.10
- with:
- endpoint: https://wus2.codesigning.azure.net/
- trusted-signing-account-name: git-fundamentals-signing
- certificate-profile-name: git-fundamentals-windows-signing
- files-folder: ${{ github.workspace }}\installers
- files-folder-filter: exe
- file-digest: SHA256
- timestamp-rfc3161: http://timestamp.acs.microsoft.com
- timestamp-digest: SHA256
-
- - name: Upload artifacts
- uses: actions/upload-artifact@v5
- with:
- name: windows-artifacts
- path: |
- payload
- installers
- symbols
-
-# ================================
-# Linux
-# ================================
- create-linux-artifacts:
- name: Create Linux Artifacts
- runs-on: ubuntu-latest
- environment: release
- needs: prereqs
- strategy:
- matrix:
- runtime: [ linux-x64, linux-arm64, linux-arm ]
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v5.0.0
- with:
- dotnet-version: 8.0.x
-
- - name: Build
- run: |
- dotnet build src/linux/Packaging.Linux/*.csproj \
- --configuration=LinuxRelease --no-self-contained \
- --runtime=${{ matrix.runtime }}
-
- - name: Run Linux unit tests
- run: |
- dotnet test --configuration=LinuxRelease
-
- - name: Log into Azure
- uses: azure/login@v2
- with:
- client-id: ${{ secrets.AZURE_CLIENT_ID }}
- tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
-
- - name: Prepare for GPG signing
- env:
- AZURE_VAULT: ${{ secrets.AZURE_VAULT }}
- GPG_KEY_SECRET_NAME: ${{ secrets.GPG_KEY_SECRET_NAME }}
- GPG_PASSPHRASE_SECRET_NAME: ${{ secrets.GPG_PASSPHRASE_SECRET_NAME }}
- GPG_KEYGRIP_SECRET_NAME: ${{ secrets.GPG_KEYGRIP_SECRET_NAME }}
- run: |
- # Install debsigs
- sudo apt install debsigs
-
- # Download GPG key, passphrase, and keygrip from Azure Key Vault
- key=$(az keyvault secret show --name $GPG_KEY_SECRET_NAME --vault-name $AZURE_VAULT --query "value")
- passphrase=$(az keyvault secret show --name $GPG_PASSPHRASE_SECRET_NAME --vault-name $AZURE_VAULT --query "value")
- keygrip=$(az keyvault secret show --name $GPG_KEYGRIP_SECRET_NAME --vault-name $AZURE_VAULT --query "value")
-
- # Remove quotes from downloaded values
- key=$(sed -e 's/^"//' -e 's/"$//' <<<"$key")
- passphrase=$(sed -e 's/^"//' -e 's/"$//' <<<"$passphrase")
- keygrip=$(sed -e 's/^"//' -e 's/"$//' <<<"$keygrip")
-
- # Import GPG key
- echo "$key" | base64 -d | gpg --import --no-tty --batch --yes
-
- # Configure GPG
- echo "allow-preset-passphrase" > ~/.gnupg/gpg-agent.conf
- gpg-connect-agent RELOADAGENT /bye
- /usr/lib/gnupg2/gpg-preset-passphrase --preset "$keygrip" <<<"$passphrase"
-
- - name: Sign Debian package and tarball
- run: |
- # Sign Debian package
- version=${{ needs.prereqs.outputs.version }}
- mv out/linux/Packaging.Linux/Release/deb/gcm-${{ matrix.runtime }}.$version.deb .
- debsigs --sign=origin --verify --check gcm-${{ matrix.runtime }}.$version.deb
-
- # Generate tarball signature file
- mv -v out/linux/Packaging.Linux/Release/tar/* .
- gpg --batch --yes --armor --output gcm-${{ matrix.runtime }}.$version.tar.gz.asc \
- --detach-sig gcm-${{ matrix.runtime }}.$version.tar.gz
-
- - name: Upload artifacts
- uses: actions/upload-artifact@v5
- with:
- name: ${{ matrix.runtime }}-artifacts
- path: |
- ./*.deb
- ./*.asc
- ./*.tar.gz
-
-# ================================
-# .NET Tool
-# ================================
- dotnet-tool-build:
- name: Build .NET tool
- runs-on: ubuntu-latest
- needs: prereqs
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v5.0.0
- with:
- dotnet-version: 8.0.x
-
- - name: Build .NET tool
- run: |
- src/shared/DotnetTool/layout.sh --configuration=Release
-
- - name: Upload .NET tool artifacts
- uses: actions/upload-artifact@v5
- with:
- name: tmp.dotnet-tool-build
- path: |
- out/shared/DotnetTool/nupkg/Release
-
- dotnet-tool-payload-sign:
- name: Sign .NET tool payload
- runs-on: windows-latest
- environment: release
- needs: dotnet-tool-build
- steps:
- - uses: actions/checkout@v5
-
- - name: Download payload
- uses: actions/download-artifact@v6
- with:
- name: tmp.dotnet-tool-build
-
- - name: Log into Azure
- uses: azure/login@v2
- with:
- client-id: ${{ secrets.AZURE_CLIENT_ID }}
- tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
-
- - name: Install sign CLI tool
- run: |
- dotnet tool install -g sign --version 0.9.1-beta.24325.5
-
- - name: Sign payload
- run: |
- sign.exe code trusted-signing payload/* `
- -tse https://wus2.codesigning.azure.net/ `
- -tsa git-fundamentals-signing `
- -tscp git-fundamentals-windows-signing
-
- - name: Lay out signed payload, images, and symbols
- shell: bash
- run: |
- mkdir dotnet-tool-payload-sign
- mv images payload.sym payload -t dotnet-tool-payload-sign
-
- - name: Upload signed payload
- uses: actions/upload-artifact@v5
- with:
- name: dotnet-tool-payload-sign
- path: |
- dotnet-tool-payload-sign
-
- dotnet-tool-pack:
- name: Package .NET tool
- runs-on: ubuntu-latest
- needs: [ prereqs, dotnet-tool-payload-sign ]
- steps:
- - uses: actions/checkout@v5
-
- - name: Download signed payload
- uses: actions/download-artifact@v6
- with:
- name: dotnet-tool-payload-sign
- path: signed
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v5.0.0
- with:
- dotnet-version: 8.0.x
-
- - name: Package tool
- run: |
- src/shared/DotnetTool/pack.sh --configuration=Release \
- --version="${{ needs.prereqs.outputs.version }}" \
- --publish-dir=$(pwd)/signed
-
- - name: Upload unsigned package
- uses: actions/upload-artifact@v5
- with:
- name: tmp.dotnet-tool-package-unsigned
- path: |
- out/shared/DotnetTool/nupkg/Release/*.nupkg
-
- dotnet-tool-sign:
- name: Sign .NET tool package
- runs-on: windows-latest
- environment: release
- needs: dotnet-tool-pack
- steps:
- - uses: actions/checkout@v5
-
- - name: Download unsigned package
- uses: actions/download-artifact@v6
- with:
- name: tmp.dotnet-tool-package-unsigned
- path: nupkg
-
- - name: Log into Azure
- uses: azure/login@v2
- with:
- client-id: ${{ secrets.AZURE_CLIENT_ID }}
- tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
-
- - name: Install sign CLI tool
- run: |
- dotnet tool install -g sign --version 0.9.1-beta.24325.5
-
- - name: Sign package
- run: |
- sign.exe code trusted-signing nupkg/* `
- -tse https://wus2.codesigning.azure.net/ `
- -tsa git-fundamentals-signing `
- -tscp git-fundamentals-windows-signing
-
- mv nupkg/* .
-
- # Remove this once NuGet supports the subscriber identity validation EKU:
- # https://github.com/NuGet/NuGetGallery/issues/10027
- - name: Extract signing certificate from package
- shell: pwsh
- run: |
- dotnet tool install --global Knapcode.CertificateExtractor
- $nupkg = gci *.nupkg
- nuget-cert-extractor --file $nupkg --output certs --code-signing --author --leaf
- $cert = gci certs\*.cer
- mv $cert .\nuget-signing.cer
-
- - name: Publish signed package and certificate
- uses: actions/upload-artifact@v5
- with:
- name: dotnet-tool-sign
- path: |
- *.nupkg
- *.cer
-
-# ================================
-# Validate
-# ================================
- validate:
- name: Validate installers
- strategy:
- matrix:
- component:
- - os: ubuntu-latest
- artifact: linux-x64-artifacts
- command: git-credential-manager
- description: linux-x64
- - os: macos-latest
- artifact: macos-osx-x64-artifacts
- command: git-credential-manager
- description: osx-x64
- - os: windows-latest
- artifact: windows-artifacts
- # Even when a standalone GCM version is installed, GitHub actions
- # runners still only recognize the version bundled with Git for
- # Windows due to its placement on the PATH. For this reason, we use
- # the full path to our installation to validate the Windows version.
- command: "$PROGRAMFILES (x86)/Git Credential Manager/git-credential-manager.exe"
- description: windows
- - os: ubuntu-latest
- artifact: dotnet-tool-sign
- command: git-credential-manager
- description: dotnet-tool
- runs-on: ${{ matrix.component.os }}
- needs: [ create-macos-artifacts, create-windows-artifacts, create-linux-artifacts, dotnet-tool-sign ]
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v5.0.0
- with:
- dotnet-version: 8.0.x
-
- - name: Download artifacts
- uses: actions/download-artifact@v6
- with:
- name: ${{ matrix.component.artifact }}
-
- - name: Install Windows
- if: contains(matrix.component.description, 'windows')
- shell: pwsh
- run: |
- $exePaths = Get-ChildItem -Path ./installers/*.exe | %{$_.FullName}
- foreach ($exePath in $exePaths)
- {
- Start-Process -Wait -FilePath "$exePath" -ArgumentList "/SILENT /VERYSILENT /NORESTART"
- }
-
- - name: Install Linux x64 (Debian package)
- if: contains(matrix.component.description, 'linux-x64')
- run: |
- debpath=$(find ./*.deb)
- sudo apt install $debpath
- "${{ matrix.component.command }}" configure
-
- - name: Install Linux x64 (tarball)
- if: contains(matrix.component.description, 'linux-x64')
- run: |
- # Ensure we find only the source tarball, not the symbols
- tarpath=$(find . -name '*[[:digit:]].tar.gz')
- tar -xvf $tarpath -C /usr/local/bin
- "${{ matrix.component.command }}" configure
-
- - name: Install macOS
- if: contains(matrix.component.description, 'osx-x64')
- run: |
- # Only validate x64, given arm64 agents are not available
- pkgpath=$(find ./pkg/*.pkg)
- sudo installer -pkg $pkgpath -target /
-
- - name: Install .NET tool
- if: contains(matrix.component.description, 'dotnet-tool')
- run: |
- nupkgpath=$(find ./*.nupkg)
- dotnet tool install -g --add-source $(dirname "$nupkgpath") git-credential-manager
- "${{ matrix.component.command }}" configure
-
- - name: Validate
- shell: bash
- run: |
- "${{ matrix.component.command }}" --version | sed 's/+.*//' >actual
- cat VERSION | sed -E 's/.[0-9]+$//' >expect
- cmp expect actual || exit 1
-
-# ================================
-# Publish
-# ================================
- create-github-release:
- name: Publish GitHub draft release
- runs-on: ubuntu-latest
- env:
- AZURE_VAULT: ${{ secrets.AZURE_VAULT }}
- GPG_PUBLIC_KEY_SECRET_NAME: ${{ secrets.GPG_PUBLIC_KEY_SECRET_NAME }}
- environment: release
- needs: [ prereqs, validate ]
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v5.0.0
- with:
- dotnet-version: 8.0.x
-
- - name: Download artifacts
- uses: actions/download-artifact@v6
-
- - name: Archive macOS payload and symbols
- run: |
- version="${{ needs.prereqs.outputs.version }}"
- mkdir osx-payload-and-symbols
-
- tar -C macos-osx-x64-artifacts/payload -czf osx-payload-and-symbols/gcm-osx-x64-$version.tar.gz .
- tar -C macos-osx-x64-artifacts/symbols -czf osx-payload-and-symbols/gcm-osx-x64-$version-symbols.tar.gz .
-
- tar -C macos-osx-arm64-artifacts/payload -czf osx-payload-and-symbols/gcm-osx-arm64-$version.tar.gz .
- tar -C macos-osx-arm64-artifacts/symbols -czf osx-payload-and-symbols/gcm-osx-arm64-$version-symbols.tar.gz .
-
- - name: Archive Windows payload and symbols
- run: |
- version="${{ needs.prereqs.outputs.version }}"
- mkdir win-x86-payload-and-symbols
- zip -jr win-x86-payload-and-symbols/gcm-win-x86-$version.zip windows-artifacts/payload
- zip -jr win-x86-payload-and-symbols/gcm-win-x86-$version-symbols.zip windows-artifacts/symbols
-
- - name: Log into Azure
- uses: azure/login@v2
- with:
- client-id: ${{ secrets.AZURE_CLIENT_ID }}
- tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
-
- - name: Download GPG public key signature file
- run: |
- az keyvault secret show --name "$GPG_PUBLIC_KEY_SECRET_NAME" \
- --vault-name "$AZURE_VAULT" --query "value" \
- | sed -e 's/^"//' -e 's/"$//' | base64 -d >gcm-public.asc
- cp gcm-public.asc linux-x64-artifacts/
- cp gcm-public.asc linux-arm64-artifacts/
- mv gcm-public.asc linux-arm-artifacts
-
- - uses: actions/github-script@v8
- with:
- script: |
- const fs = require('fs');
- const path = require('path');
- const version = "${{ needs.prereqs.outputs.version }}"
-
- var releaseMetadata = {
- owner: context.repo.owner,
- repo: context.repo.repo
- };
-
- // Create the release
- var tagName = `v${version}`;
- var createdRelease = await github.rest.repos.createRelease({
- ...releaseMetadata,
- draft: true,
- tag_name: tagName,
- target_commitish: context.sha,
- name: `GCM ${version}`
- });
- releaseMetadata.release_id = createdRelease.data.id;
-
- // Uploads contents of directory to the release created above
- async function uploadDirectoryToRelease(directory, includeExtensions=[]) {
- return fs.promises.readdir(directory)
- .then(async(files) => Promise.all(
- files.filter(file => {
- return includeExtensions.length==0 || includeExtensions.includes(path.extname(file).toLowerCase());
- })
- .map(async (file) => {
- var filePath = path.join(directory, file);
- github.rest.repos.uploadReleaseAsset({
- ...releaseMetadata,
- name: file,
- headers: {
- "content-length": (await fs.promises.stat(filePath)).size
- },
- data: fs.createReadStream(filePath)
- });
- }))
- );
- }
-
- await Promise.all([
- // Upload Windows artifacts
- uploadDirectoryToRelease('windows-artifacts/installers'),
- uploadDirectoryToRelease('win-x86-payload-and-symbols'),
-
- // Upload macOS artifacts
- uploadDirectoryToRelease('macos-osx-x64-artifacts/pkg'),
- uploadDirectoryToRelease('macos-osx-arm64-artifacts/pkg'),
- uploadDirectoryToRelease('osx-payload-and-symbols'),
-
- // Upload Linux artifacts
- uploadDirectoryToRelease('linux-x64-artifacts'),
- uploadDirectoryToRelease('linux-arm64-artifacts'),
- uploadDirectoryToRelease('linux-arm-artifacts'),
-
- // Upload .NET tool package
- uploadDirectoryToRelease('dotnet-tool-sign'),
- ]);
diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh
index 62352a7e8..4a77eff69 100755
--- a/src/linux/Packaging.Linux/build.sh
+++ b/src/linux/Packaging.Linux/build.sh
@@ -64,7 +64,7 @@ PAYLOAD="$OUTDIR/payload"
SYMBOLS="$OUTDIR/payload.sym"
# Lay out payload
-"$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" --runtime="$RUNTIME" || exit 1
+"$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" --runtime="$RUNTIME" --output="$PAYLOAD" --symbol-output="$SYMBOLS" || exit 1
if [ $INSTALL_FROM_SOURCE = true ]; then
echo "Installing to $INSTALL_PREFIX"
diff --git a/src/linux/Packaging.Linux/layout.sh b/src/linux/Packaging.Linux/layout.sh
index ccf031156..fe3a0f2b8 100755
--- a/src/linux/Packaging.Linux/layout.sh
+++ b/src/linux/Packaging.Linux/layout.sh
@@ -23,10 +23,17 @@ case "$i" in
CONFIGURATION="${i#*=}"
shift # past argument=value
;;
+ --output=*)
+ PAYLOAD="${i#*=}"
+ shift # past argument=value
+ ;;
--runtime=*)
RUNTIME="${i#*=}"
shift # past argument=value
;;
+ --symbol-output=*)
+ SYMBOLOUT="${i#*=}"
+ ;;
*)
# unknown option
;;
@@ -46,10 +53,12 @@ FRAMEWORK=net8.0
# Perform pre-execution checks
CONFIGURATION="${CONFIGURATION:=Debug}"
-
-# Outputs
-PAYLOAD="$PROJ_OUT/$CONFIGURATION/payload"
-SYMBOLOUT="$PROJ_OUT/$CONFIGURATION/payload.sym"
+if [ -z "$PAYLOAD" ]; then
+ die "--output was not set"
+fi
+if [ -z "$SYMBOLOUT" ]; then
+ SYMBOLOUT="$PAYLOAD.sym"
+fi
# Cleanup payload directory
if [ -d "$PAYLOAD" ]; then
diff --git a/src/linux/Packaging.Linux/pack.sh b/src/linux/Packaging.Linux/pack.sh
index e69783bb6..52d0137e4 100755
--- a/src/linux/Packaging.Linux/pack.sh
+++ b/src/linux/Packaging.Linux/pack.sh
@@ -36,6 +36,10 @@ case "$i" in
CONFIGURATION="${i#*=}"
shift # past argument=value
;;
+ --output=*)
+ OUTPUT_ROOT="${i#*=}"
+ shift # past argument=value
+ ;;
*)
# unknown option
;;
@@ -59,11 +63,15 @@ if [ -z "$RUNTIME" ]; then
die "--runtime was not set"
fi
-TAROUT="$PROJ_OUT/$CONFIGURATION/tar/"
+if [ -z "$OUTPUT_ROOT" ]; then
+ OUTPUT_ROOT="$PROJ_OUT/$CONFIGURATION"
+fi
+
+TAROUT="$OUTPUT_ROOT/tar"
TARBALL="$TAROUT/gcm-$RUNTIME.$VERSION.tar.gz"
SYMTARBALL="$TAROUT/gcm-$RUNTIME.$VERSION-symbols.tar.gz"
-DEBOUT="$PROJ_OUT/$CONFIGURATION/deb"
+DEBOUT="$OUTPUT_ROOT/deb"
DEBROOT="$DEBOUT/root"
DEBPKG="$DEBOUT/gcm-$RUNTIME.$VERSION.deb"
mkdir -p "$DEBROOT"
diff --git a/src/osx/Installer.Mac/codesign.sh b/src/osx/Installer.Mac/codesign.sh
index d66c8acd9..44feedb6f 100755
--- a/src/osx/Installer.Mac/codesign.sh
+++ b/src/osx/Installer.Mac/codesign.sh
@@ -15,32 +15,45 @@ elif [ -z "$ENTITLEMENTS_FILE" ]; then
exit 1
fi
+# The codesign command needs the entitlements file to be given as an absolute
+# file path; relative paths can cause issues.
+if [[ "${ENTITLEMENTS_FILE}" != /* ]]; then
+ echo "error: entitlements file argument must be an absolute path"
+ exit 1
+fi
+
echo "======== INPUTS ========"
echo "Directory: $SIGN_DIR"
echo "Developer ID: $DEVELOPER_ID"
echo "Entitlements: $ENTITLEMENTS_FILE"
echo "======== END INPUTS ========"
+echo
+echo "======== ENTITLEMENTS ========"
+cat "$ENTITLEMENTS_FILE"
+echo "======== END ENTITLEMENTS ========"
+echo
-cd $SIGN_DIR
+cd "$SIGN_DIR" || exit 1
for f in *
do
- macho=$(file --mime $f | grep mach)
+ macho=$(file --mime "$f" | grep mach)
# Runtime sign dylibs and Mach-O binaries
- if [[ $f == *.dylib ]] || [ ! -z "$macho" ];
+ if [[ $f == *.dylib ]] || [ -n "$macho" ];
then
- echo "Runtime Signing $f"
- codesign -s "$DEVELOPER_ID" $f --timestamp --force --options=runtime --entitlements $ENTITLEMENTS_FILE
+ echo "Signing with entitlements and hardening: $f"
+ codesign -s "$DEVELOPER_ID" "$f" --timestamp --force --options=runtime --entitlements "$ENTITLEMENTS_FILE"
elif [ -d "$f" ];
then
- echo "Signing files in subdirectory $f"
- cd $f
- for i in *
- do
- codesign -s "$DEVELOPER_ID" $i --timestamp --force
- done
- cd ..
+ echo "Signing files in subdirectory: $f"
+ (
+ cd "$f" || exit 1
+ for i in *
+ do
+ codesign -s "$DEVELOPER_ID" "$i" --timestamp --force
+ done
+ )
else
- echo "Signing $f"
- codesign -s "$DEVELOPER_ID" $f --timestamp --force
+ echo "Signing: $f"
+ codesign -s "$DEVELOPER_ID" "$f" --timestamp --force
fi
done
diff --git a/src/shared/DotnetTool/dotnet-tool.nuspec b/src/shared/DotnetTool/dotnet-tool.nuspec
index cf9ba7444..35f81ebc9 100644
--- a/src/shared/DotnetTool/dotnet-tool.nuspec
+++ b/src/shared/DotnetTool/dotnet-tool.nuspec
@@ -6,13 +6,12 @@
Secure, cross-platform Git credential storage with authentication to Azure Repos, GitHub, and other popular Git hosting services.
git-credential-manager
images\icon.png
- https://raw.githubusercontent.com/git-ecosystem/git-credential-manager/main/assets/gcm-transparent.png
-
-
+
+
diff --git a/src/shared/DotnetTool/layout.ps1 b/src/shared/DotnetTool/layout.ps1
new file mode 100644
index 000000000..ca9b13011
--- /dev/null
+++ b/src/shared/DotnetTool/layout.ps1
@@ -0,0 +1,90 @@
+<#
+.SYNOPSIS
+ Lays out the .NET tool package directory.
+
+.PARAMETER Configuration
+ Build configuration (Debug/Release). Defaults to Debug.
+
+.PARAMETER Output
+ Root output directory for the nupkg layout. If omitted:
+ out/shared/DotnetTool/nupkg/
+
+.EXAMPLE
+ pwsh ./layout.ps1 -Configuration Release
+
+.EXAMPLE
+ pwsh ./layout.ps1 -Output C:\temp\tool-layout
+
+#>
+
+[CmdletBinding()]
+param(
+ [string]$Configuration = "Debug",
+ [string]$Output
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+function Make-Absolute {
+ param([string]$Path)
+ if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
+ if ([System.IO.Path]::IsPathRooted($Path)) { return $Path }
+ return (Join-Path -Path (Get-Location) -ChildPath $Path)
+}
+
+Write-Host "Starting layout..." -ForegroundColor Cyan
+
+# Directories
+$ScriptDir = $PSScriptRoot
+$Root = (Resolve-Path (Join-Path $ScriptDir "..\..\..")).Path
+$Src = Join-Path $Root "src"
+$Out = Join-Path $Root "out"
+$DotnetToolRel = "shared/DotnetTool"
+$GcmSrc = Join-Path $Src "shared\Git-Credential-Manager"
+$ProjOut = Join-Path $Out $DotnetToolRel
+
+$Framework = "net8.0"
+
+if (-not $Output -or $Output.Trim() -eq "") {
+ $Output = Join-Path $ProjOut "nupkg\$Configuration"
+}
+
+$ImgOut = Join-Path $Output "images"
+$BinOut = Join-Path $Output "tools\$Framework\any"
+
+# Cleanup previous layout
+if (Test-Path $Output) {
+ Write-Host "Cleaning existing output directory '$Output'..."
+ Remove-Item -Force -Recurse $Output
+}
+
+# Recreate directories
+$null = New-Item -ItemType Directory -Path $BinOut -Force
+$null = New-Item -ItemType Directory -Path $ImgOut -Force
+
+# Determine DOTNET_ROOT if not set
+if (-not $env:DOTNET_ROOT -or $env:DOTNET_ROOT.Trim() -eq "") {
+ $dotnetCmd = Get-Command dotnet -ErrorAction Stop
+ $env:DOTNET_ROOT = Split-Path -Parent $dotnetCmd.Source
+}
+
+Write-Host "Publishing core application..."
+& "$env:DOTNET_ROOT/dotnet" publish $GcmSrc `
+ --configuration $Configuration `
+ --framework $Framework `
+ --output (Make-Absolute $BinOut) `
+ -p:UseAppHost=false
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Error "dotnet publish failed with exit code $LASTEXITCODE"
+ exit $LASTEXITCODE
+}
+
+Write-Host "Copying package configuration file..."
+Copy-Item -Path (Join-Path $Src "$DotnetToolRel\DotnetToolSettings.xml") -Destination $BinOut -Force
+
+Write-Host "Copying images..."
+Copy-Item -Path (Join-Path $Src "$DotnetToolRel\icon.png") -Destination $ImgOut -Force
+
+Write-Host "Layout complete." -ForegroundColor Green
diff --git a/src/shared/DotnetTool/layout.sh b/src/shared/DotnetTool/layout.sh
deleted file mode 100755
index f5244dbbd..000000000
--- a/src/shared/DotnetTool/layout.sh
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/bin/bash
-make_absolute () {
- case "$1" in
- /*)
- echo "$1"
- ;;
- *)
- echo "$PWD/$1"
- ;;
- esac
-}
-
-#####################################################################
-# Lay out
-#####################################################################
-# Parse script arguments
-for i in "$@"
-do
-case "$i" in
- --configuration=*)
- CONFIGURATION="${i#*=}"
- shift # past argument=value
- ;;
- *)
- # unknown option
- ;;
-esac
-done
-
-# Directories
-THISDIR="$( cd "$(dirname "$0")" ; pwd -P )"
-ROOT="$( cd "$THISDIR"/../../.. ; pwd -P )"
-SRC="$ROOT/src"
-OUT="$ROOT/out"
-GCM_SRC="$SRC/shared/Git-Credential-Manager"
-DOTNET_TOOL="shared/DotnetTool"
-PROJ_OUT="$OUT/$DOTNET_TOOL"
-
-CONFIGURATION="${CONFIGURATION:=Debug}"
-
-# Build parameters
-FRAMEWORK=net8.0
-
-# Outputs
-OUTDIR="$PROJ_OUT/nupkg/$CONFIGURATION"
-IMGOUT="$OUTDIR/images"
-PAYLOAD="$OUTDIR/payload"
-SYMBOLOUT="$OUTDIR/payload.sym"
-
-# Cleanup output directory
-if [ -d "$OUTDIR" ]; then
- echo "Cleaning existing output directory '$OUTDIR'..."
- rm -rf "$OUTDIR"
-fi
-
-# Ensure output directories exist
-mkdir -p "$PAYLOAD" "$SYMBOLOUT" "$IMGOUT"
-
-if [ -z "$DOTNET_ROOT" ]; then
- DOTNET_ROOT="$(dirname $(which dotnet))"
-fi
-
-# Publish core application executables
-echo "Publishing core application..."
-$DOTNET_ROOT/dotnet publish "$GCM_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --output="$(make_absolute "$PAYLOAD")" \
- -p:UseAppHost=false || exit 1
-
-# Collect symbols
-echo "Collecting managed symbols..."
-mv "$PAYLOAD"/*.pdb "$SYMBOLOUT" || exit 1
-
-# Copy DotnetToolSettings.xml file
-echo "Copying out package configuration files..."
-cp "$SRC/$DOTNET_TOOL/DotnetToolSettings.xml" "$PAYLOAD/"
-
-# Copy package icon image
-echo "Copying images..."
-cp "$SRC/$DOTNET_TOOL/icon.png" "$IMGOUT" || exit 1
-
-echo "Build complete."
diff --git a/src/shared/DotnetTool/pack.ps1 b/src/shared/DotnetTool/pack.ps1
new file mode 100644
index 000000000..6842d030a
--- /dev/null
+++ b/src/shared/DotnetTool/pack.ps1
@@ -0,0 +1,95 @@
+<#
+.SYNOPSIS
+ Creates the NuGet package for the .NET tool.
+
+.PARAMETER Configuration
+ Build configuration (Debug/Release). Defaults to Debug.
+
+.PARAMETER Version
+ Package version (required).
+
+.PARAMETER PackageRoot
+ Root of the pre-laid-out package structure (from layout). Defaults to:
+ out/shared/DotnetTool/nupkg/
+
+.PARAMETER Output
+ Optional directory for the produced .nupkg/.snupkg. If omitted NuGet chooses.
+
+.EXAMPLE
+ pwsh ./pack.ps1 -Version 2.0.123-beta
+
+.EXAMPLE
+ pwsh ./pack.ps1 -Configuration Release -Version 2.1.0 -Output C:\pkgs
+
+#>
+
+[CmdletBinding()]
+param(
+ [string]$Configuration = "Debug",
+ [Parameter(Mandatory = $true)]
+ [string]$Version,
+ [string]$PackageRoot,
+ [string]$Output
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+Write-Host "Starting pack..." -ForegroundColor Cyan
+
+# Directories
+$ScriptDir = $PSScriptRoot
+$Root = (Resolve-Path (Join-Path $ScriptDir "..\..\..")).Path
+$Src = Join-Path $Root "src"
+$Out = Join-Path $Root "out"
+$DotnetToolRel = "shared\DotnetTool"
+$NuspecFile = Join-Path $Src "$DotnetToolRel\dotnet-tool.nuspec"
+
+if (-not (Test-Path $NuspecFile)) {
+ Write-Error "Could not locate nuspec file at '$NuspecFile'"
+ exit 1
+}
+
+if (-not $PackageRoot -or $PackageRoot.Trim() -eq "") {
+ $PackageRoot = Join-Path $Out "$DotnetToolRel\nupkg\$Configuration"
+}
+
+if (-not (Test-Path $PackageRoot)) {
+ Write-Error "Package root '$PackageRoot' does not exist. Run layout.ps1 first."
+ exit 1
+}
+
+# Locate nuget
+$nugetCmd = Get-Command nuget -ErrorAction SilentlyContinue
+if (-not $nugetCmd) {
+ Write-Error "nuget CLI not found in PATH (install: https://www.nuget.org/downloads)"
+ exit 1
+}
+$nugetExe = $nugetCmd.Source
+
+Write-Host "Creating .NET tool package..."
+
+$packArgs = @(
+ "pack", "$NuspecFile",
+ "-Properties", "Configuration=$Configuration",
+ "-Version", $Version,
+ "-Symbols", "-SymbolPackageFormat", "snupkg",
+ "-BasePath", "$PackageRoot"
+)
+
+if ($Output -and $Output.Trim() -ne "") {
+ if (-not (Test-Path $Output)) {
+ Write-Host "Creating output directory '$Output'..."
+ New-Item -ItemType Directory -Force -Path $Output | Out-Null
+ }
+ $packArgs += @("-OutputDirectory", "$Output")
+}
+
+& $nugetExe @packArgs
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Error "nuget pack failed with exit code $LASTEXITCODE"
+ exit $LASTEXITCODE
+}
+
+Write-Host ".NET tool pack complete." -ForegroundColor Green
diff --git a/src/shared/DotnetTool/pack.sh b/src/shared/DotnetTool/pack.sh
deleted file mode 100755
index 5b2eaf8dc..000000000
--- a/src/shared/DotnetTool/pack.sh
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/bin/bash
-die () {
- echo "$*" >&2
- exit 1
-}
-
-# Parse script arguments
-for i in "$@"
-do
-case "$i" in
- --configuration=*)
- CONFIGURATION="${i#*=}"
- shift # past argument=value
- ;;
- --version=*)
- VERSION="${i#*=}"
- shift # past argument=value
- ;;
- --publish-dir=*)
- PUBLISH_DIR="${i#*=}"
- shift # past argument=value
- ;;
- *)
- # unknown option
- ;;
-esac
-done
-
-CONFIGURATION="${CONFIGURATION:=Debug}"
-if [ -z "$VERSION" ]; then
- die "--version was not set"
-fi
-
-# Directories
-THISDIR="$( cd "$(dirname "$0")" ; pwd -P )"
-ROOT="$( cd "$THISDIR"/../../.. ; pwd -P )"
-SRC="$ROOT/src"
-OUT="$ROOT/out"
-DOTNET_TOOL="shared/DotnetTool"
-
-if [ -z "$PUBLISH_DIR" ]; then
- PUBLISH_DIR="$OUT/$DOTNET_TOOL/nupkg/$CONFIGURATION"
-fi
-
-echo "Creating dotnet tool package..."
-
-dotnet pack "$SRC/$DOTNET_TOOL/DotnetTool.csproj" \
- /p:Configuration="$CONFIGURATION" \
- /p:PackageVersion="$VERSION" \
- /p:PublishDir="$PUBLISH_DIR/"
-
-echo "Dotnet tool pack complete."