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."