diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml
new file mode 100644
index 000000000..aaf5e2d43
--- /dev/null
+++ b/.azure-pipelines/release.yml
@@ -0,0 +1,866 @@
+name: $(Date:yyyyMMdd)$(Rev:.r)
+trigger: none
+pr: none
+
+resources:
+ repositories:
+ - repository: 1ESPipelines
+ type: git
+ name: 1ESPipelineTemplates/1ESPipelineTemplates
+ ref: refs/tags/release
+
+parameters:
+ - name: 'esrp'
+ type: boolean
+ 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_x86
+ jobName: 'Windows (x86)'
+ runtime: win-x86
+ pool: GitClientPME-1ESHostedPool-intel-pc
+ image: win-x86_64-ado1es
+ os: windows
+ - id: windows_x64
+ jobName: 'Windows (x64)'
+ runtime: win-x64
+ pool: GitClientPME-1ESHostedPool-intel-pc
+ image: win-x86_64-ado1es
+ os: windows
+ - id: windows_arm64
+ jobName: 'Windows (ARM64)'
+ runtime: win-arm64
+ pool: GitClientPME-1ESHostedPool-arm64-pc
+ image: win-arm64-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
+ - id: linux_arm64
+ jobName: 'Linux (ARM64)'
+ runtime: linux-arm64
+ pool: GitClientPME-1ESHostedPool-arm64-pc
+ image: ubuntu-arm64-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: build
+ displayName: 'Build and Sign'
+ jobs:
+ #
+ # 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 `
+ -RuntimeIdentifier ${{ dim.runtime }}
+ - task: ArchiveFiles@2
+ displayName: 'Archive symbols'
+ inputs:
+ rootFolderOrFile: '$(Build.ArtifactStagingDirectory)\symbols_raw'
+ includeRootFolder: false
+ archiveType: zip
+ archiveFile: '$(Build.ArtifactStagingDirectory)\symbols\gcm-${{ dim.runtime }}-$(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" `
+ -p:RuntimeIdentifier="${{ dim.runtime }}"
+ - 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-${{ dim.runtime }}-$(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: 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: 'win-x64'
+ targetPath: $(Pipeline.Workspace)/assets/win-x64
+ - input: pipelineArtifact
+ artifactName: 'win-arm64'
+ targetPath: $(Pipeline.Workspace)/assets/win-arm64
+ - 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: 'linux-arm64'
+ targetPath: $(Pipeline.Workspace)/assets/linux-arm64
+ - 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/win-x64/*.exe
+ $(Pipeline.Workspace)/assets/win-x64/*.zip
+ $(Pipeline.Workspace)/assets/win-arm64/*.exe
+ $(Pipeline.Workspace)/assets/win-arm64/*.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/linux-arm64/*.deb
+ $(Pipeline.Workspace)/assets/linux-arm64/*.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/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 745027d8b..b89613684 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -22,16 +22,16 @@ jobs:
language: [ 'csharp' ]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Setup .NET
- uses: actions/setup-dotnet@v4.0.1
+ uses: actions/setup-dotnet@v5.0.1
with:
dotnet-version: 8.0.x
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3
+ uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
@@ -39,4 +39,4 @@ jobs:
dotnet build
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v4
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 27834c10e..7bb45f26a 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -13,13 +13,22 @@ jobs:
# ================================
windows:
name: Windows
- runs-on: windows-latest
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ include:
+ - runtime: win-x86
+ os: windows-latest
+ - runtime: win-x64
+ os: windows-latest
+ - runtime: win-arm64
+ os: windows-11-arm
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Setup .NET
- uses: actions/setup-dotnet@v4.0.1
+ uses: actions/setup-dotnet@v5.0.1
with:
dotnet-version: 8.0.x
@@ -27,24 +36,29 @@ jobs:
run: dotnet restore
- name: Build
- run: dotnet build --configuration WindowsRelease
+ run: |
+ dotnet build src/windows/Installer.Windows/Installer.Windows.csproj `
+ --configuration=Release `
+ --runtime=${{ matrix.runtime }}
- name: Test
run: |
- dotnet test --verbosity normal --configuration=WindowsRelease
+ dotnet test --verbosity normal `
+ --configuration=WindowsRelease `
+ --runtime=${{ matrix.runtime }}
- name: Prepare artifacts
shell: bash
run: |
mkdir -p artifacts/bin
- mv out/windows/Installer.Windows/bin/Release/net472/win-x86 artifacts/bin/
- cp out/windows/Installer.Windows/bin/Release/net472/win-x86.sym/* artifacts/bin/win-x86/
- mv out/windows/Installer.Windows/bin/Release/net472/gcm*.exe artifacts/
+ mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}/gcm*.exe artifacts/
+ mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }} artifacts/bin/
+ cp out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}.sym/* artifacts/bin/${{ matrix.runtime }}/
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
- name: win-x86
+ name: ${{ matrix.runtime }}
path: |
artifacts
@@ -54,12 +68,15 @@ jobs:
linux:
name: Linux
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ runtime: [ linux-x64, linux-arm64, linux-arm ]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Setup .NET
- uses: actions/setup-dotnet@v4.0.1
+ uses: actions/setup-dotnet@v5.0.1
with:
dotnet-version: 8.0.x
@@ -67,7 +84,10 @@ jobs:
run: dotnet restore
- name: Build
- run: dotnet build --configuration LinuxRelease
+ run: |
+ dotnet build src/linux/Packaging.Linux/*.csproj \
+ --configuration=Release --no-self-contained \
+ --runtime=${{ matrix.runtime }}
- name: Test
run: |
@@ -80,9 +100,9 @@ jobs:
mv out/linux/Packaging.Linux/Release/tar/*.tar.gz artifacts/
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
- name: linux-x64
+ name: ${{ matrix.runtime }}
path: |
artifacts
@@ -97,10 +117,10 @@ jobs:
runtime: [ osx-x64, osx-arm64 ]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Setup .NET
- uses: actions/setup-dotnet@v4.0.1
+ uses: actions/setup-dotnet@v5.0.1
with:
dotnet-version: 8.0.x
@@ -125,7 +145,7 @@ jobs:
mv out/osx/Installer.Mac/pkg/Release/gcm*.pkg artifacts/
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: ${{ matrix.runtime }}
path: |
diff --git a/.github/workflows/lint-docs.yml b/.github/workflows/lint-docs.yml
index 6f153d5f6..9fecfd863 100644
--- a/.github/workflows/lint-docs.yml
+++ b/.github/workflows/lint-docs.yml
@@ -18,9 +18,9 @@ jobs:
name: Lint markdown files
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- - uses: DavidAnson/markdownlint-cli2-action@b4c9feab76d8025d1e83c653fa3990936df0e6c8
+ - uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101
with:
globs: |
"**/*.md"
@@ -30,13 +30,12 @@ jobs:
name: Check for broken links
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Run link checker
# For any troubleshooting, see:
# https://github.com/lycheeverse/lychee/blob/master/docs/TROUBLESHOOTING.md
- uses: lycheeverse/lychee-action@2b973e86fc7b1f6b36a93795fe2c9c6ae1118621
-
+ uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0
with:
# user-agent: if a user agent is not specified, some websites (e.g.
# GitHub Docs) return HTTP errors which Lychee will interpret as
diff --git a/.github/workflows/maintainer-absence.yml b/.github/workflows/maintainer-absence.yml
index 433cb0f7e..20e6694e7 100644
--- a/.github/workflows/maintainer-absence.yml
+++ b/.github/workflows/maintainer-absence.yml
@@ -18,7 +18,7 @@ jobs:
name: create-issue
runs-on: ubuntu-latest
steps:
- - uses: actions/github-script@v7
+ - uses: actions/github-script@v8
with:
script: |
const startDate = new Date('${{ github.event.inputs.startDate }}');
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 9f578e109..000000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,662 +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@v4
-
- - 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@v4
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v4.0.1
- 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.APPLICATION_CERTIFICATE_BASE64 }}
- A2: ${{ secrets.APPLICATION_CERTIFICATE_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@v4
- 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@v4
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v4.0.1
- 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.4.0
- 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@v4.0.1
- 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.4.0
- 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@v4
- with:
- name: windows-artifacts
- path: |
- payload
- installers
- symbols
-
-# ================================
-# Linux
-# ================================
- create-linux-artifacts:
- name: Create Linux Artifacts
- runs-on: ubuntu-latest
- environment: release
- needs: prereqs
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v4.0.1
- with:
- dotnet-version: 8.0.x
-
- - name: Build
- run: dotnet build --configuration=LinuxRelease
-
- - 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-linux_amd64.$version.deb .
- debsigs --sign=origin --verify --check gcm-linux_amd64.$version.deb
-
- # Generate tarball signature file
- mv -v out/linux/Packaging.Linux/Release/tar/* .
- gpg --batch --yes --armor --output gcm-linux_amd64.$version.tar.gz.asc \
- --detach-sig gcm-linux_amd64.$version.tar.gz
-
- - name: Upload artifacts
- uses: actions/upload-artifact@v4
- with:
- name: linux-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@v4
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v4.0.1
- 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@v4
- 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@v4
-
- - name: Download payload
- uses: actions/download-artifact@v4
- 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@v4
- 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@v4
-
- - name: Download signed payload
- uses: actions/download-artifact@v4
- with:
- name: dotnet-tool-payload-sign
- path: signed
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v4.0.1
- 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@v4
- 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@v4
-
- - name: Download unsigned package
- uses: actions/download-artifact@v4
- 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@v4
- with:
- name: dotnet-tool-sign
- path: |
- *.nupkg
- *.cer
-
-# ================================
-# Validate
-# ================================
- validate:
- name: Validate installers
- strategy:
- matrix:
- component:
- - os: ubuntu-latest
- artifact: linux-artifacts
- command: git-credential-manager
- description: linux
- - 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@v4
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v4.0.1
- with:
- dotnet-version: 8.0.x
-
- - name: Download artifacts
- uses: actions/download-artifact@v4
- 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 (Debian package)
- if: contains(matrix.component.description, 'linux')
- run: |
- debpath=$(find ./*.deb)
- sudo apt install $debpath
- "${{ matrix.component.command }}" configure
-
- - name: Install Linux (tarball)
- if: contains(matrix.component.description, 'linux')
- 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@v4
-
- - name: Set up .NET
- uses: actions/setup-dotnet@v4.0.1
- with:
- dotnet-version: 8.0.x
-
- - name: Download artifacts
- uses: actions/download-artifact@v4
-
- - 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
- mv gcm-public.asc linux-artifacts
-
- - uses: actions/github-script@v7
- 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-artifacts'),
-
- // Upload .NET tool package
- uploadDirectoryToRelease('dotnet-tool-sign'),
- ]);
diff --git a/.github/workflows/validate-install-from-source.yml b/.github/workflows/validate-install-from-source.yml
index ca57d2daf..d1aea471a 100644
--- a/.github/workflows/validate-install-from-source.yml
+++ b/.github/workflows/validate-install-from-source.yml
@@ -21,13 +21,14 @@ jobs:
# tgagor is a contributor who pushes updated images weekly, which should
# be sufficient for our validation needs.
- image: tgagor/centos
- - image: tgagor/centos-stream
- image: redhat/ubi8
- image: alpine
- - image: alpine:3.14.10
+ - image: alpine:3.19.8
- image: opensuse/leap
- image: opensuse/tumbleweed
- image: registry.suse.com/suse/sle15:15.4.27.11.31
+ - image: archlinux
+ - image: mcr.microsoft.com/cbl-mariner/base/core:2.0
container: ${{matrix.vector.image}}
steps:
- run: |
@@ -35,9 +36,12 @@ jobs:
zypper -n install tar gzip
elif [[ ${{matrix.vector.image}} == *"centos"* ]]; then
dnf install which -y
+ elif [[ ${{matrix.vector.image}} == *"mariner"* ]]; then
+ GNUPGHOME=/root/.gnupg tdnf update -y &&
+ GNUPGHOME=/root/.gnupg tdnf install tar -y # needed for `actions/checkout`
fi
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- run: |
sh "${GITHUB_WORKSPACE}/src/linux/Packaging.Linux/install-from-source.sh" -y
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 000000000..87c4b2935
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,5 @@
+* @git-ecosystem/git-client
+/src/shared/Microsoft.AzureRepos/ @git-ecosystem/git-client @git-ecosystem/gcm-azure-maintainers
+/src/shared/Microsoft.AzureRepos.Tests/ @git-ecosystem/git-client @git-ecosystem/gcm-azure-maintainers
+/src/shared/GitHub/ @git-ecosystem/git-client @git-ecosystem/hubbers
+/src/shared/GitHub.Tests/ @git-ecosystem/git-client @git-ecosystem/hubbers
diff --git a/Directory.Build.props b/Directory.Build.props
index 5c0d87bdb..e7ed76eb9 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -28,7 +28,7 @@
- 8.0.4
+ 8.0.5
diff --git a/README.md b/README.md
index 18c9b1309..b9dcff451 100644
--- a/README.md
+++ b/README.md
@@ -47,8 +47,8 @@ Basic HTTP authentication support|✓|✓|✓
Proxy support|✓|✓|✓
`amd64` support|✓|✓|✓
`x86` support|✓|_N/A_|✗
-`arm64` support|best effort|✓|best effort, no packages
-`armhf` support|_N/A_|_N/A_|best effort, no packages
+`arm64` support|best effort|✓|✓
+`armhf` support|_N/A_|_N/A_|✓
(\*) GCM guarantees support only for [the Linux distributions that are officially
supported by dotnet][dotnet-distributions].
@@ -56,7 +56,7 @@ supported by dotnet][dotnet-distributions].
## Supported Git versions
Git Credential Manager tries to be compatible with the broadest set of Git
-versions (within reason). However there are some know problematic releases of
+versions (within reason). However there are some known problematic releases of
Git that are not compatible.
- Git 1.x
diff --git a/VERSION b/VERSION
index 82f00d533..21b5059f4 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.6.0.0
+2.7.0.0
diff --git a/build/GetVersion.cs b/build/GetVersion.cs
index 2b3473641..0078c4c12 100644
--- a/build/GetVersion.cs
+++ b/build/GetVersion.cs
@@ -34,7 +34,7 @@ public override bool Execute()
// The main version number we use for GCM contains the first three
// components.
// The assembly and file version numbers contain all components, as
- // ommitting the revision portion from these properties causes
+ // omitting the revision portion from these properties causes
// runtime failures on Windows.
Version = $"{fullVersion.Major}.{fullVersion.Minor}.{fullVersion.Build}";
AssemblyVersion = FileVersion = fullVersion.ToString();
diff --git a/docs/configuration.md b/docs/configuration.md
index a4fecf395..ba978ef30 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -255,6 +255,24 @@ Defaults to false (use hardware acceleration where available).
---
+### credential.allowUnsafeRemotes
+
+Allow transmitting credentials to unsafe remote URLs such as unencrypted HTTP
+URLs. This setting is not recommended for general use and should only be used
+when necessary.
+
+Defaults false (disallow unsafe remote URLs).
+
+#### Example
+
+```shell
+git config --global credential.allowUnsafeRemotes true
+```
+
+**Also see: [GCM_ALLOW_UNSAFE_REMOTES][gcm-allow-unsafe-remotes]**
+
+---
+
### credential.autoDetectTimeout
Set the maximum length of time, in milliseconds, that GCM should wait for a
@@ -567,6 +585,7 @@ _(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|-
`gpg`|Use GPG to store encrypted files that are compatible with the [pass][pass] (requires GPG and `pass` to initialize the store).|macOS, Linux
`cache`|Git's built-in [credential cache][credential-cache].|macOS, Linux
`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`credential.plaintextStorePath`][credential-plaintextstorepath].|Windows, macOS, Linux
+`none`|Do not store credentials via GCM.|Windows, macOS, Linux
#### Example
@@ -1022,8 +1041,9 @@ Defaults to disabled.
[devbox]: https://azure.microsoft.com/en-us/products/dev-box
[enterprise-config]: enterprise-config.md
[envars]: environment.md
-[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/
+[freedesktop-ss]: https://specifications.freedesktop.org/secret-service-spec/
[gcm-allow-windowsauth]: environment.md#GCM_ALLOW_WINDOWSAUTH
+[gcm-allow-unsafe-remotes]: environment.md#GCM_ALLOW_UNSAFE_REMOTES
[gcm-authority]: environment.md#GCM_AUTHORITY-deprecated
[gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT
[gcm-azrepos-credentialtype]: environment.md#GCM_AZREPOS_CREDENTIALTYPE
diff --git a/docs/credstores.md b/docs/credstores.md
index 9154cd441..ca76f5692 100644
--- a/docs/credstores.md
+++ b/docs/credstores.md
@@ -9,6 +9,7 @@ There are several options for storing credentials that GCM supports:
- GPG/[`pass`][passwordstore] compatible files
- Git's built-in [credential cache][credential-cache]
- Plaintext files
+- Passthrough/no-op (no credential store)
The default credential stores on macOS and Windows are the macOS Keychain and
the Windows Credential Manager, respectively.
@@ -251,13 +252,38 @@ permissions on this directory such that no other users or applications can
access files within. If possible, use a path that exists on an external volume
that you take with you and use full-disk encryption.
+## Passthrough/no-op (no credential store)
+
+**Available on:** _Windows, macOS, Linux_
+
+**:warning: .**
+
+```batch
+SET GCM_CREDENTIAL_STORE="none"
+```
+
+or
+
+```shell
+git config --global credential.credentialStore none
+```
+
+This option disables the internal credential store. All operations to store or
+retrieve credentials will do nothing, and will return success. This is useful if
+you want to use a different credential store, chained in sequence via Git
+configuration, and don't want GCM to store credentials.
+
+Note that you'll want to ensure that another credential helper is placed before
+GCM in the `credential.helper` Git configuration or else you will be prompted to
+enter your credentials every time you interact with a remote repository.
+
[access-windows-credential-manager]: https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0
[aws-cloudshell]: https://aws.amazon.com/cloudshell/
[azure-cloudshell]: https://docs.microsoft.com/azure/cloud-shell/overview
[cmdkey]: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmdkey
[credential-store]: configuration.md#credentialcredentialstore
[credential-cache]: https://git-scm.com/docs/git-credential-cache
-[freedesktop-secret-service]: https://specifications.freedesktop.org/secret-service-spec/latest/
+[freedesktop-secret-service]: https://specifications.freedesktop.org/secret-service/
[gcm-credential-store]: environment.md#GCM_CREDENTIAL_STORE
[git-credential-store]: https://git-scm.com/docs/git-credential-store
[mac-keychain-management]: https://support.apple.com/en-gb/guide/mac-help/mchlf375f392/mac
diff --git a/docs/development.md b/docs/development.md
index 7729556f9..0242d68b8 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -54,6 +54,12 @@ To build from the command line, run:
dotnet build -c LinuxDebug
```
+If you want to build for a specific architecture, you can provide `linux-x64` or `linux-arm64` or `linux-arm` as the runtime:
+
+```shell
+dotnet build -c LinuxDebug -r linux-arm64
+```
+
You can find a copy of the Debian package (.deb) file in `out/linux/Packaging.Linux/deb/Debug`.
The flat binaries can also be found in `out/linux/Packaging.Linux/payload/Debug`.
diff --git a/docs/enterprise-config.md b/docs/enterprise-config.md
index bfdc7e302..97544a33f 100644
--- a/docs/enterprise-config.md
+++ b/docs/enterprise-config.md
@@ -55,7 +55,38 @@ those of the [Git configuration][config] settings.
The type of each registry key can be either `REG_SZ` (string) or `REG_DWORD`
(integer).
-## macOS/Linux
+## macOS
+
+Default settings values come from macOS's preferences system. Configuration
+profiles can be deployed to devices using a compatible Mobile Device Management
+(MDM) solution.
+
+Configuration for Git Credential Manager must take the form of a dictionary, set
+for the domain `git-credential-manager` under the key `configuration`. For
+example:
+
+```shell
+defaults write git-credential-manager configuration -dict-add
+```
+
+..where `` is the name of the settings from the [Git configuration][config]
+reference, and `` is the desired value.
+
+All values in the `configuration` dictionary must be strings. For boolean values
+use `true` or `false`, and for integer values use the number in string form.
+
+To read the current configuration:
+
+```console
+$ defaults read git-credential-manager configuration
+{
+ = ;
+ ...
+ = ;
+}
+```
+
+## Linux
Default configuration setting stores has not been implemented.
diff --git a/docs/environment.md b/docs/environment.md
index edda0d714..f321caa6c 100644
--- a/docs/environment.md
+++ b/docs/environment.md
@@ -302,6 +302,32 @@ Defaults to false (use hardware acceleration where available).
---
+### GCM_ALLOW_UNSAFE_REMOTES
+
+Allow transmitting credentials to unsafe remote URLs such as unencrypted HTTP
+URLs. This setting is not recommended for general use and should only be used
+when necessary.
+
+Defaults false (disallow unsafe remote URLs).
+
+#### Example
+
+##### Windows
+
+```batch
+SET GCM_ALLOW_UNSAFE_REMOTES=true
+```
+
+##### macOS/Linux
+
+```bash
+export GCM_ALLOW_UNSAFE_REMOTES=true
+```
+
+**Also see: [credential.allowUnsafeRemotes][credential-allowunsaferemotes]**
+
+---
+
### GCM_AUTODETECT_TIMEOUT
Set the maximum length of time, in milliseconds, that GCM should wait for a
@@ -690,6 +716,7 @@ _(unset)_|Windows: `wincredman`, macOS: `keychain`, Linux: _(none)_|-
`gpg`|Use GPG to store encrypted files that are compatible with the [`pass` utility][passwordstore] (requires GPG and `pass` to initialize the store).|macOS, Linux
`cache`|Git's built-in [credential cache][git-credential-cache].|Windows, macOS, Linux
`plaintext`|Store credentials in plaintext files (**UNSECURE**). Customize the plaintext store location with [`GCM_PLAINTEXT_STORE_PATH`][gcm-plaintext-store-path].|Windows, macOS, Linux
+`none`|Do not store credentials via GCM.|Windows, macOS, Linux
#### Windows
@@ -1153,7 +1180,8 @@ Defaults to disabled.
[autodetect]: autodetect.md
[azure-access-tokens]: azrepos-users-and-tokens.md
[configuration]: configuration.md
-[credential-allowwindowsauth]: environment.md#credentialallowWindowsAuth
+[credential-allowwindowsauth]: configuration.md#credentialallowwindowsauth
+[credential-allowunsaferemotes]: configuration.md#credentialallowunsaferemotes
[credential-authority]: configuration.md#credentialauthority-deprecated
[credential-autodetecttimeout]: configuration.md#credentialautodetecttimeout
[credential-azrepos-credential-type]: configuration.md#credentialazreposcredentialtype
@@ -1182,7 +1210,7 @@ Defaults to disabled.
[credential-trace-msauth]: configuration.md#credentialtracemsauth
[default-values]: enterprise-config.md
[devbox]: https://azure.microsoft.com/en-us/products/dev-box
-[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/
+[freedesktop-ss]: https://specifications.freedesktop.org/secret-service-spec/
[gcm]: usage.md
[gcm-interactive]: #gcm_interactive
[gcm-credential-store]: #gcm_credential_store
diff --git a/docs/img/git-for-windows-gcm-screenshot.png b/docs/img/git-for-windows-gcm-screenshot.png
new file mode 100644
index 000000000..aaf60b682
Binary files /dev/null and b/docs/img/git-for-windows-gcm-screenshot.png differ
diff --git a/docs/install.md b/docs/install.md
index 5ae7b44d5..9fa7da4ac 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -151,7 +151,7 @@ manager.
GCM is included with [Git for Windows][git-for-windows]. During installation
you will be asked to select a credential helper, with GCM listed as the default.
-![image][git-for-windows-screenshot]
+![image][git-for-windows-gcm-screenshot]
---
@@ -241,7 +241,7 @@ dotnet tool uninstall -g git-credential-manager
[gcm-credstores]: credstores.md
[gcm-wsl]: wsl.md
[git-for-windows]: https://gitforwindows.org/
-[git-for-windows-screenshot]: https://user-images.githubusercontent.com/5658207/140082529-1ac133c1-0922-4a24-af03-067e27b3988b.png
+[git-for-windows-gcm-screenshot]: img/git-for-windows-gcm-screenshot.png
[latest-release]: https://github.com/git-ecosystem/git-credential-manager/releases/latest
[linux-uninstall]: linux-fromsrc-uninstall.md
[linux-validate-gpg-debian]: ./linux-validate-gpg.md#debian-package
diff --git a/docs/netconfig.md b/docs/netconfig.md
index cf312336f..920344f15 100644
--- a/docs/netconfig.md
+++ b/docs/netconfig.md
@@ -191,6 +191,22 @@ network traffic inspection tool such as [Telerik Fiddler][telerik-fiddler]. If
you are using such tools please consult their documentation for trusting the
proxy root certificates.
+---
+
+## Unsafe Remote URLs
+
+If you are using a remote URL that is not considered safe, such as unencrypted
+HTTP (remote URLs that start with `http://`), host providers may prevent you
+from authenticating with your credentials.
+
+In this case, you should consider using a HTTPS (starting with `https://`)
+remote URL to ensure your credentials are transmitted securely.
+
+If you accept the risks associated with using an unsafe remote URL, you can
+configure GCM to allow the use of unsafe remote URLS by setting the environment
+variable [`GCM_ALLOW_UNSAFE_REMOTES`][unsafe-envar], or by using the Git
+configuration option [`credential.allowUnsafeRemotes`][unsafe-config] to `true`.
+
[environment]: environment.md
[configuration]: configuration.md
[git-http-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy
@@ -212,3 +228,5 @@ proxy root certificates.
[git-ssl-no-verify]: https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_networking
[git-http-ssl-verify]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify
[telerik-fiddler]: https://www.telerik.com/fiddler
+[unsafe-envar]: environment.md#gcm_allow_unsafe_remotes
+[unsafe-config]: configuration.md#credentialallowunsaferemotes
diff --git a/global.json b/global.json
new file mode 100644
index 000000000..5cc6b13a6
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "rollForward": "latestMajor",
+ "version": "8.0"
+ }
+}
+
diff --git a/src/linux/Packaging.Linux/Packaging.Linux.csproj b/src/linux/Packaging.Linux/Packaging.Linux.csproj
index 8b9755c78..ddfb31500 100644
--- a/src/linux/Packaging.Linux/Packaging.Linux.csproj
+++ b/src/linux/Packaging.Linux/Packaging.Linux.csproj
@@ -24,8 +24,8 @@
-
-
+
+
diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh
index 6672857d2..4a77eff69 100755
--- a/src/linux/Packaging.Linux/build.sh
+++ b/src/linux/Packaging.Linux/build.sh
@@ -30,6 +30,10 @@ case "$i" in
INSTALL_FROM_SOURCE="${i#*=}"
shift # past argument=value
;;
+ --runtime=*)
+ RUNTIME="${i#*=}"
+ shift # past argument=value
+ ;;
--install-prefix=*)
INSTALL_PREFIX="${i#*=}"
shift # past argument=value
@@ -41,10 +45,14 @@ esac
done
# Ensure install prefix exists
-if [! -d "$INSTALL_PREFIX" ]; then
+if [ ! -d "$INSTALL_PREFIX" ]; then
mkdir -p "$INSTALL_PREFIX"
fi
+if [ ! -z "$RUNTIME" ]; then
+ echo "Building for runtime ${RUNTIME}"
+fi
+
# Perform pre-execution checks
CONFIGURATION="${CONFIGURATION:=Debug}"
if [ -z "$VERSION" ]; then
@@ -56,7 +64,7 @@ PAYLOAD="$OUTDIR/payload"
SYMBOLS="$OUTDIR/payload.sym"
# Lay out payload
-"$INSTALLER_SRC/layout.sh" --configuration="$CONFIGURATION" || 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"
@@ -79,7 +87,7 @@ if [ $INSTALL_FROM_SOURCE = true ]; then
echo "Install complete."
else
# Pack
- "$INSTALLER_SRC/pack.sh" --configuration="$CONFIGURATION" --payload="$PAYLOAD" --symbols="$SYMBOLS" --version="$VERSION" || exit 1
+ "$INSTALLER_SRC/pack.sh" --configuration="$CONFIGURATION" --runtime="$RUNTIME" --payload="$PAYLOAD" --symbols="$SYMBOLS" --version="$VERSION" || exit 1
fi
echo "Build of Packaging.Linux complete."
diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh
index be6ea1579..1a8ede938 100755
--- a/src/linux/Packaging.Linux/install-from-source.sh
+++ b/src/linux/Packaging.Linux/install-from-source.sh
@@ -40,18 +40,26 @@ if [ -z $is_ci ]; then
Git Credential Manager is licensed under the MIT License: https://aka.ms/gcm/license"
- while true; do
- read -p "Do you want to continue? [Y/n] " yn
- case $yn in
- [Yy]*|"")
- break
- ;;
- [Nn]*)
- exit
- ;;
- *)
- echo "Please answer yes or no."
- ;;
+ while true; do
+ # Display prompt once before reading input
+ printf "Do you want to continue? [Y/n] "
+
+ # Prefer reading from the controlling terminal (TTY) when available,
+ # so that input works even if the script is piped (e.g. curl URL | sh)
+ if [ -r /dev/tty ]; then
+ read yn < /dev/tty
+ # If no TTY is available, attempt to read from standard input (stdin)
+ elif ! read yn; then
+ # If input is not possible via TTY or stdin, assume a non-interactive environment
+ # and abort with guidance for automated usage
+ echo "Interactive prompt unavailable in this environment. Use 'sh -s -- -y' for automated install."
+ exit 1
+ fi
+
+ case "$yn" in
+ [Yy]*|"") break ;;
+ [Nn]*) exit ;;
+ *) echo "Please answer yes or no." ;;
esac
done
fi
@@ -63,7 +71,7 @@ install_packages() {
for package in $packages; do
# Ensure we don't stomp on existing installations.
- if [ ! -z $(which $package) ]; then
+ if type $package >/dev/null 2>&1; then
continue
fi
@@ -181,7 +189,7 @@ case "$distribution" in
fi
fi
;;
- fedora | centos | rhel)
+ fedora | centos | rhel | ol)
$sudo_cmd dnf upgrade -y
# Install dotnet/GCM dependencies.
@@ -208,7 +216,7 @@ case "$distribution" in
$sudo_cmd zypper -n update
# Install dotnet/GCM dependencies.
- install_packages zypper install "curl git find krb5 libicu libopenssl1_1"
+ install_packages zypper install "curl git find krb5 libicu"
ensure_dotnet_installed
;;
@@ -228,7 +236,7 @@ case "$distribution" in
$sudo_cmd tdnf update -y
# Install dotnet/GCM dependencies.
- install_packages tdnf install "curl git krb5-libs libicu openssl-libs zlib findutils which bash"
+ install_packages tdnf install "curl ca-certificates git krb5-libs libicu openssl-libs zlib findutils which bash awk"
ensure_dotnet_installed
;;
diff --git a/src/linux/Packaging.Linux/layout.sh b/src/linux/Packaging.Linux/layout.sh
index 6679c39ca..fe3a0f2b8 100755
--- a/src/linux/Packaging.Linux/layout.sh
+++ b/src/linux/Packaging.Linux/layout.sh
@@ -23,6 +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
;;
@@ -39,14 +50,15 @@ PROJ_OUT="$OUT/linux/Packaging.Linux"
# Build parameters
FRAMEWORK=net8.0
-RUNTIME=linux-x64
# 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
@@ -69,13 +81,22 @@ fi
# Publish core application executables
echo "Publishing core application..."
-$DOTNET_ROOT/dotnet publish "$GCM_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- -p:PublishSingleFile=true \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
+if [ -z "$RUNTIME" ]; then
+ $DOTNET_ROOT/dotnet publish "$GCM_SRC" \
+ --configuration="$CONFIGURATION" \
+ --framework="$FRAMEWORK" \
+ --self-contained \
+ -p:PublishSingleFile=true \
+ --output="$(make_absolute "$PAYLOAD")" || exit 1
+else
+ $DOTNET_ROOT/dotnet publish "$GCM_SRC" \
+ --configuration="$CONFIGURATION" \
+ --framework="$FRAMEWORK" \
+ --runtime="$RUNTIME" \
+ --self-contained \
+ -p:PublishSingleFile=true \
+ --output="$(make_absolute "$PAYLOAD")" || exit 1
+fi
# Collect symbols
echo "Collecting managed symbols..."
diff --git a/src/linux/Packaging.Linux/pack.sh b/src/linux/Packaging.Linux/pack.sh
index 14d26aee5..821d66ea1 100755
--- a/src/linux/Packaging.Linux/pack.sh
+++ b/src/linux/Packaging.Linux/pack.sh
@@ -28,10 +28,18 @@ case "$i" in
SYMBOLS="${i#*=}"
shift # past argument=value
;;
+ --runtime=*)
+ RUNTIME="${i#*=}"
+ shift # past argument=value
+ ;;
--configuration=*)
CONFIGURATION="${i#*=}"
shift # past argument=value
;;
+ --output=*)
+ OUTPUT_ROOT="${i#*=}"
+ shift # past argument=value
+ ;;
*)
# unknown option
;;
@@ -51,20 +59,37 @@ fi
if [ -z "$SYMBOLS" ]; then
die "--symbols was not set"
fi
+if [ -z "$OUTPUT_ROOT" ]; then
+ OUTPUT_ROOT="$PROJ_OUT/$CONFIGURATION"
+fi
-ARCH="`dpkg-architecture -q DEB_HOST_ARCH`"
-
-if test -z "$ARCH"; then
- die "Could not determine host architecture!"
+# Fall back to host architecture if no explicit runtime is given.
+if test -z "$RUNTIME"; then
+ HOST_ARCH="`uname -m`"
+
+ case $HOST_ARCH in
+ x86_64|amd64)
+ RUNTIME="linux-x64"
+ ;;
+ aarch64|arm64)
+ RUNTIME="linux-arm64"
+ ;;
+ armhf)
+ RUNTIME="linux-arm"
+ ;;
+ *)
+ die "Could not determine host architecture! ($HOST_ARCH)"
+ ;;
+ esac
fi
-TAROUT="$PROJ_OUT/$CONFIGURATION/tar/"
-TARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION.tar.gz"
-SYMTARBALL="$TAROUT/gcm-linux_$ARCH.$VERSION-symbols.tar.gz"
+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-linux_$ARCH.$VERSION.deb"
+DEBPKG="$DEBOUT/gcm-$RUNTIME.$VERSION.deb"
mkdir -p "$DEBROOT"
# Set full read, write, execute permissions for owner and just read and execute permissions for group and other
@@ -75,7 +100,7 @@ echo "Packing Packaging.Linux..."
# Cleanup any old archive files
if [ -e "$TAROUT" ]; then
- echo "Deleteing old archive '$TAROUT'..."
+ echo "Deleting old archive '$TAROUT'..."
rm "$TAROUT"
fi
@@ -99,6 +124,22 @@ INSTALL_TO="$DEBROOT/usr/local/share/gcm-core/"
LINK_TO="$DEBROOT/usr/local/bin/"
mkdir -p "$DEBROOT/DEBIAN" "$INSTALL_TO" "$LINK_TO" || exit 1
+# Determine architecture for debian control file from the runtime architecture
+case $RUNTIME in
+ linux-x64)
+ ARCH="amd64"
+ ;;
+ linux-arm64)
+ ARCH="arm64"
+ ;;
+ linux-arm)
+ ARCH="armhf"
+ ;;
+ *)
+ die "Incompatible runtime architecture given for pack.sh"
+ ;;
+esac
+
# make the debian control file
# this is purposefully not indented, see
# https://stackoverflow.com/questions/9349616/bash-eof-in-if-statement
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/osx/Installer.Mac/dist.sh b/src/osx/Installer.Mac/dist.sh
index f26761e26..185da7248 100755
--- a/src/osx/Installer.Mac/dist.sh
+++ b/src/osx/Installer.Mac/dist.sh
@@ -82,7 +82,7 @@ fi
# Cleanup any old package
if [ -e "$DISTOUT" ]; then
- echo "Deleteing old product package '$DISTOUT'..."
+ echo "Deleting old product package '$DISTOUT'..."
rm "$DISTOUT"
fi
diff --git a/src/osx/Installer.Mac/pack.sh b/src/osx/Installer.Mac/pack.sh
index b58f4ce5a..77c6c623e 100755
--- a/src/osx/Installer.Mac/pack.sh
+++ b/src/osx/Installer.Mac/pack.sh
@@ -52,7 +52,7 @@ fi
# Cleanup any old component
if [ -e "$PKGOUT" ]; then
- echo "Deleteing old component '$PKGOUT'..."
+ echo "Deleting old component '$PKGOUT'..."
rm "$PKGOUT"
fi
diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs
index 35472682c..286398de9 100644
--- a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs
+++ b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs
@@ -55,8 +55,8 @@ public bool IsSupported(InputArguments input)
return false;
}
- // We do not support unencrypted HTTP communications to Bitbucket,
- // but we report `true` here for HTTP so that we can show a helpful
+ // We do not recommend unencrypted HTTP communications to Bitbucket, but it is possible.
+ // Therefore, we report `true` here for HTTP so that we can show a helpful
// error message for the user in `GetCredentialAsync`.
return (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") ||
StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "https")) &&
@@ -81,11 +81,14 @@ public bool IsSupported(HttpResponseMessage response)
public async Task GetCredentialAsync(InputArguments input)
{
// We should not allow unencrypted communication and should inform the user
- if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")
- && BitbucketHelper.IsBitbucketOrg(input))
+ if (!_context.Settings.AllowUnsafeRemotes &&
+ StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") &&
+ BitbucketHelper.IsBitbucketOrg(input))
{
throw new Trace2Exception(_context.Trace2,
- "Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS.");
+ "Unencrypted HTTP is not recommended for Bitbucket.org. " +
+ "Ensure the repository remote URL is using HTTPS " +
+ $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes.");
}
var authModes = await GetSupportedAuthenticationModesAsync(input);
diff --git a/src/shared/Core.Tests/GitStreamReaderTests.cs b/src/shared/Core.Tests/GitStreamReaderTests.cs
new file mode 100644
index 000000000..bf656d102
--- /dev/null
+++ b/src/shared/Core.Tests/GitStreamReaderTests.cs
@@ -0,0 +1,193 @@
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace GitCredentialManager.Tests;
+
+public class GitStreamReaderTests
+{
+ #region ReadLineAsync
+
+ [Fact]
+ public async Task GitStreamReader_ReadLineAsync_LF()
+ {
+ // hello\n
+ // world\n
+
+ byte[] buffer = Encoding.UTF8.GetBytes("hello\nworld\n");
+ using var stream = new MemoryStream(buffer);
+ var reader = new GitStreamReader(stream, Encoding.UTF8);
+
+ string actual1 = await reader.ReadLineAsync();
+ string actual2 = await reader.ReadLineAsync();
+ string actual3 = await reader.ReadLineAsync();
+
+ Assert.Equal("hello", actual1);
+ Assert.Equal("world", actual2);
+ Assert.Null(actual3);
+ }
+
+ [Fact]
+ public async Task GitStreamReader_ReadLineAsync_CR()
+ {
+ // hello\rworld\r
+
+ byte[] buffer = Encoding.UTF8.GetBytes("hello\rworld\r");
+ using var stream = new MemoryStream(buffer);
+ var reader = new GitStreamReader(stream, Encoding.UTF8);
+
+ string actual1 = await reader.ReadLineAsync();
+ string actual2 = await reader.ReadLineAsync();
+
+ Assert.Equal("hello\rworld\r", actual1);
+ Assert.Null(actual2);
+ }
+
+ [Fact]
+ public async Task GitStreamReader_ReadLineAsync_CRLF()
+ {
+ // hello\r\n
+ // world\r\n
+
+ byte[] buffer = Encoding.UTF8.GetBytes("hello\r\nworld\r\n");
+ using var stream = new MemoryStream(buffer);
+ var reader = new GitStreamReader(stream, Encoding.UTF8);
+
+ string actual1 = await reader.ReadLineAsync();
+ string actual2 = await reader.ReadLineAsync();
+ string actual3 = await reader.ReadLineAsync();
+
+ Assert.Equal("hello", actual1);
+ Assert.Equal("world", actual2);
+ Assert.Null(actual3);
+ }
+
+ [Fact]
+ public async Task GitStreamReader_ReadLineAsync_Mixed()
+ {
+ // hello\r\n
+ // world\rthis\n
+ // is\n
+ // a\n
+ // \rmixed\rnewline\r\n
+ // \n
+ // string\n
+
+ byte[] buffer = Encoding.UTF8.GetBytes("hello\r\nworld\rthis\nis\na\n\rmixed\rnewline\r\n\nstring\n");
+ using var stream = new MemoryStream(buffer);
+ var reader = new GitStreamReader(stream, Encoding.UTF8);
+
+ string actual1 = await reader.ReadLineAsync();
+ string actual2 = await reader.ReadLineAsync();
+ string actual3 = await reader.ReadLineAsync();
+ string actual4 = await reader.ReadLineAsync();
+ string actual5 = await reader.ReadLineAsync();
+ string actual6 = await reader.ReadLineAsync();
+ string actual7 = await reader.ReadLineAsync();
+ string actual8 = await reader.ReadLineAsync();
+
+ Assert.Equal("hello", actual1);
+ Assert.Equal("world\rthis", actual2);
+ Assert.Equal("is", actual3);
+ Assert.Equal("a", actual4);
+ Assert.Equal("\rmixed\rnewline", actual5);
+ Assert.Equal("", actual6);
+ Assert.Equal("string", actual7);
+ Assert.Null(actual8);
+ }
+
+ #endregion
+
+ #region ReadLine
+
+ [Fact]
+ public void GitStreamReader_ReadLine_LF()
+ {
+ // hello\n
+ // world\n
+
+ byte[] buffer = Encoding.UTF8.GetBytes("hello\nworld\n");
+ using var stream = new MemoryStream(buffer);
+ var reader = new GitStreamReader(stream, Encoding.UTF8);
+
+ string actual1 = reader.ReadLine();
+ string actual2 = reader.ReadLine();
+ string actual3 = reader.ReadLine();
+
+ Assert.Equal("hello", actual1);
+ Assert.Equal("world", actual2);
+ Assert.Null(actual3);
+ }
+
+ [Fact]
+ public void GitStreamReader_ReadLine_CR()
+ {
+ // hello\rworld\r
+
+ byte[] buffer = Encoding.UTF8.GetBytes("hello\rworld\r");
+ using var stream = new MemoryStream(buffer);
+ var reader = new GitStreamReader(stream, Encoding.UTF8);
+
+ string actual1 = reader.ReadLine();
+ string actual2 = reader.ReadLine();
+
+ Assert.Equal("hello\rworld\r", actual1);
+ Assert.Null(actual2);
+ }
+
+ [Fact]
+ public void GitStreamReader_ReadLine_CRLF()
+ {
+ // hello\r\n
+ // world\r\n
+
+ byte[] buffer = Encoding.UTF8.GetBytes("hello\r\nworld\r\n");
+ using var stream = new MemoryStream(buffer);
+ var reader = new GitStreamReader(stream, Encoding.UTF8);
+
+ string actual1 = reader.ReadLine();
+ string actual2 = reader.ReadLine();
+ string actual3 = reader.ReadLine();
+
+ Assert.Equal("hello", actual1);
+ Assert.Equal("world", actual2);
+ Assert.Null(actual3);
+ }
+
+ [Fact]
+ public void GitStreamReader_ReadLine_Mixed()
+ {
+ // hello\r\n
+ // world\rthis\n
+ // is\n
+ // a\n
+ // \rmixed\rnewline\r\n
+ // \n
+ // string\n
+
+ byte[] buffer = Encoding.UTF8.GetBytes("hello\r\nworld\rthis\nis\na\n\rmixed\rnewline\r\n\nstring\n");
+ using var stream = new MemoryStream(buffer);
+ var reader = new GitStreamReader(stream, Encoding.UTF8);
+
+ string actual1 = reader.ReadLine();
+ string actual2 = reader.ReadLine();
+ string actual3 = reader.ReadLine();
+ string actual4 = reader.ReadLine();
+ string actual5 = reader.ReadLine();
+ string actual6 = reader.ReadLine();
+ string actual7 = reader.ReadLine();
+ string actual8 = reader.ReadLine();
+
+ Assert.Equal("hello", actual1);
+ Assert.Equal("world\rthis", actual2);
+ Assert.Equal("is", actual3);
+ Assert.Equal("a", actual4);
+ Assert.Equal("\rmixed\rnewline", actual5);
+ Assert.Equal("", actual6);
+ Assert.Equal("string", actual7);
+ Assert.Null(actual8);
+ }
+
+ #endregion
+}
diff --git a/src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs b/src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs
new file mode 100644
index 000000000..0efb14471
--- /dev/null
+++ b/src/shared/Core.Tests/Interop/MacOS/MacOSPreferencesTests.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Xunit;
+using GitCredentialManager.Interop.MacOS;
+using static GitCredentialManager.Tests.TestUtils;
+
+namespace GitCredentialManager.Tests.Interop.MacOS;
+
+public class MacOSPreferencesTests
+{
+ private const string TestAppId = "com.example.gcm-test";
+ private const string DefaultsPath = "/usr/bin/defaults";
+
+ [MacOSFact]
+ public async Task MacOSPreferences_ReadPreferences()
+ {
+ try
+ {
+ await SetupTestPreferencesAsync();
+
+ var pref = new MacOSPreferences(TestAppId);
+
+ // Exists
+ string stringValue = pref.GetString("myString");
+ int? intValue = pref.GetInteger("myInt");
+ IDictionary dictValue = pref.GetDictionary("myDict");
+
+ Assert.NotNull(stringValue);
+ Assert.Equal("this is a string", stringValue);
+ Assert.NotNull(intValue);
+ Assert.Equal(42, intValue);
+ Assert.NotNull(dictValue);
+ Assert.Equal(2, dictValue.Count);
+ Assert.Equal("value1", dictValue["dict-k1"]);
+ Assert.Equal("value2", dictValue["dict-k2"]);
+
+ // Does not exist
+ string missingString = pref.GetString("missingString");
+ int? missingInt = pref.GetInteger("missingInt");
+ IDictionary missingDict = pref.GetDictionary("missingDict");
+
+ Assert.Null(missingString);
+ Assert.Null(missingInt);
+ Assert.Null(missingDict);
+ }
+ finally
+ {
+ await CleanupTestPreferencesAsync();
+ }
+ }
+
+ private static async Task SetupTestPreferencesAsync()
+ {
+ // Using the defaults command set up preferences for the test app
+ await RunCommandAsync(DefaultsPath, $"write {TestAppId} myString \"this is a string\"");
+ await RunCommandAsync(DefaultsPath, $"write {TestAppId} myInt -int 42");
+ await RunCommandAsync(DefaultsPath, $"write {TestAppId} myDict -dict dict-k1 value1 dict-k2 value2");
+ }
+
+ private static async Task CleanupTestPreferencesAsync()
+ {
+ // Delete the test app preferences
+ // defaults delete com.example.gcm-test
+ await RunCommandAsync(DefaultsPath, $"delete {TestAppId}");
+ }
+}
diff --git a/src/shared/Core.Tests/Trace2MessageTests.cs b/src/shared/Core.Tests/Trace2MessageTests.cs
index 82b161744..7e29a641f 100644
--- a/src/shared/Core.Tests/Trace2MessageTests.cs
+++ b/src/shared/Core.Tests/Trace2MessageTests.cs
@@ -54,7 +54,7 @@ public void Event_Message_Without_Snake_Case_ToJson_Creates_Expected_Json()
ParameterizedMessage = "baz"
};
- var expected = "{\"event\":\"error\",\"sid\":\"123\",\"thread\":\"main\",\"time\":\"0001-01-01T00:00:00+00:00\",\"file\":\"foo.cs\",\"line\":1,\"depth\":1,\"msg\":\"bar\",\"format\":\"baz\"}";
+ var expected = "{\"event\":\"error\",\"sid\":\"123\",\"thread\":\"main\",\"time\":\"0001-01-01T00:00:00+00:00\",\"file\":\"foo.cs\",\"line\":1,\"depth\":1,\"msg\":\"bar\",\"fmt\":\"baz\"}";
var actual = errorMessage.ToJson();
Assert.Equal(expected, actual);
diff --git a/src/shared/Core.Tests/Trace2Tests.cs b/src/shared/Core.Tests/Trace2Tests.cs
index 26df5ab98..38011275d 100644
--- a/src/shared/Core.Tests/Trace2Tests.cs
+++ b/src/shared/Core.Tests/Trace2Tests.cs
@@ -6,25 +6,26 @@ public class Trace2Tests
{
[PosixTheory]
[InlineData("af_unix:foo", "foo")]
- [InlineData("af_unix:stream:foo-bar", "foo-bar")]
- [InlineData("af_unix:dgram:foo-bar-baz", "foo-bar-baz")]
+ [InlineData("af_unix:foo/bar", "foo/bar")]
+ [InlineData("af_unix:stream:foo/bar", "foo/bar")]
+ [InlineData("af_unix:dgram:foo/bar/baz", "foo/bar/baz")]
public void TryGetPipeName_Posix_Returns_Expected_Value(string input, string expected)
{
var isSuccessful = Trace2.TryGetPipeName(input, out var actual);
Assert.True(isSuccessful);
- Assert.Matches(actual, expected);
+ Assert.Equal(actual, expected);
}
[WindowsTheory]
[InlineData("\\\\.\\pipe\\git-foo", "git-foo")]
[InlineData("\\\\.\\pipe\\git-foo-bar", "git-foo-bar")]
- [InlineData("\\\\.\\pipe\\foo\\git-bar", "git-bar")]
+ [InlineData("\\\\.\\pipe\\foo\\git-bar", "foo\\git-bar")]
public void TryGetPipeName_Windows_Returns_Expected_Value(string input, string expected)
{
var isSuccessful = Trace2.TryGetPipeName(input, out var actual);
Assert.True(isSuccessful);
- Assert.Matches(actual, expected);
+ Assert.Equal(expected, actual);
}
}
diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs
index 712db32e1..d3ef1dbf6 100644
--- a/src/shared/Core/CommandContext.cs
+++ b/src/shared/Core/CommandContext.cs
@@ -131,7 +131,7 @@ public CommandContext()
gitPath,
FileSystem.GetCurrentDirectory()
);
- Settings = new Settings(Environment, Git);
+ Settings = new MacOSSettings(Environment, Git, Trace);
}
else if (PlatformUtils.IsLinux())
{
diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs
index 210c991bc..4777b0cf8 100644
--- a/src/shared/Core/Constants.cs
+++ b/src/shared/Core/Constants.cs
@@ -16,6 +16,7 @@ public static class Constants
public const string GcmDataDirectoryName = ".gcm";
+ public const string MacOSBundleId = "git-credential-manager";
public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf");
///
@@ -38,6 +39,7 @@ public static class CredentialStoreNames
public const string SecretService = "secretservice";
public const string Plaintext = "plaintext";
public const string Cache = "cache";
+ public const string None = "none";
}
public static class RegexPatterns
@@ -119,6 +121,7 @@ public static class EnvironmentVariables
public const string OAuthDefaultUserName = "GCM_OAUTH_DEFAULT_USERNAME";
public const string GcmDevUseLegacyUiHelpers = "GCM_DEV_USELEGACYUIHELPERS";
public const string GcmGuiSoftwareRendering = "GCM_GUI_SOFTWARE_RENDERING";
+ public const string GcmAllowUnsafeRemotes = "GCM_ALLOW_UNSAFE_REMOTES";
}
public static class Http
@@ -163,6 +166,7 @@ public static class Credential
public const string MsAuthUseDefaultAccount = "msauthUseDefaultAccount";
public const string GuiSoftwareRendering = "guiSoftwareRendering";
public const string GpgPassStorePath = "gpgPassStorePath";
+ public const string AllowUnsafeRemotes = "allowUnsafeRemotes";
public const string OAuthAuthenticationModes = "oauthAuthModes";
public const string OAuthClientId = "oauthClientId";
@@ -226,6 +230,7 @@ public static class HelpUrls
public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect";
public const string GcmDefaultAccount = "https://aka.ms/gcm/defaultaccount";
public const string GcmMultipleUsers = "https://aka.ms/gcm/multipleusers";
+ public const string GcmUnsafeRemotes = "https://aka.ms/gcm/unsaferemotes";
}
private static Version _gcmVersion;
diff --git a/src/shared/Core/CredentialStore.cs b/src/shared/Core/CredentialStore.cs
index 83f915d1e..11dc83818 100644
--- a/src/shared/Core/CredentialStore.cs
+++ b/src/shared/Core/CredentialStore.cs
@@ -100,6 +100,10 @@ private void EnsureBackingStore()
_backingStore = new PlaintextCredentialStore(_context.FileSystem, plainStoreRoot, ns);
break;
+ case StoreNames.None:
+ _backingStore = new NullCredentialStore();
+ break;
+
default:
var sb = new StringBuilder();
sb.AppendLine(string.IsNullOrWhiteSpace(credStoreName)
@@ -168,6 +172,9 @@ private static void AppendAvailableStoreList(StringBuilder sb)
sb.AppendFormat(" {1,-13} : store credentials in plain-text files (UNSECURE){0}",
Environment.NewLine, StoreNames.Plaintext);
+
+ sb.AppendFormat(" {1, -13} : disable internal credential storage{0}",
+ Environment.NewLine, StoreNames.None);
}
private void ValidateWindowsCredentialManager()
diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs
index 447e465d5..19e1d6733 100644
--- a/src/shared/Core/GenericHostProvider.cs
+++ b/src/shared/Core/GenericHostProvider.cs
@@ -54,6 +54,17 @@ public override async Task GenerateCredentialAsync(InputArguments i
{
ThrowIfDisposed();
+ // We only want to *warn* about HTTP remotes for the generic provider because it supports all protocols
+ // and, historically, we never blocked HTTP remotes in this provider.
+ // The user can always set the 'GCM_ALLOW_UNSAFE' setting to silence the warning.
+ if (!Context.Settings.AllowUnsafeRemotes &&
+ StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http"))
+ {
+ Context.Streams.Error.WriteLine(
+ "warning: use of unencrypted HTTP remote URLs is not recommended; " +
+ $"see {Constants.HelpUrls.GcmUnsafeRemotes} for more information.");
+ }
+
Uri uri = input.GetRemoteUri();
// Determine the if the host supports Windows Integration Authentication (WIA) or OAuth
@@ -150,7 +161,7 @@ private async Task GetOAuthAccessToken(Uri remoteUri, string userNa
// Store new refresh token if we have been given one
if (!string.IsNullOrWhiteSpace(refreshResult.RefreshToken))
{
- Context.CredentialStore.AddOrUpdate(refreshService, refreshToken.Account, refreshToken.Password);
+ Context.CredentialStore.AddOrUpdate(refreshService, refreshToken.Account, refreshResult.RefreshToken);
}
// Return the new access token
diff --git a/src/shared/Core/GitStreamReader.cs b/src/shared/Core/GitStreamReader.cs
new file mode 100644
index 000000000..6512b2efc
--- /dev/null
+++ b/src/shared/Core/GitStreamReader.cs
@@ -0,0 +1,70 @@
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GitCredentialManager;
+
+///
+/// StreamReader that does NOT consider a lone carriage-return as a new-line character,
+/// only a line-feed or carriage-return immediately followed by a line-feed.
+///
+/// The only major operating system that uses a lone carriage-return as a new-line character
+/// is the classic Macintosh OS (before OS X), which is not supported by Git.
+///
+public class GitStreamReader : StreamReader
+{
+ public GitStreamReader(Stream stream, Encoding encoding) : base(stream, encoding) { }
+
+ public override string ReadLine()
+ {
+#if NETFRAMEWORK
+ return ReadLineAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+#else
+ return ReadLineAsync(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
+#endif
+ }
+
+#if NETFRAMEWORK
+ public override async Task ReadLineAsync()
+#else
+ public override async ValueTask ReadLineAsync(CancellationToken cancellationToken)
+#endif
+ {
+ int nr;
+ var sb = new StringBuilder();
+ var buffer = new char[1];
+ bool lastWasCR = false;
+
+ while ((nr = await base.ReadAsync(buffer, 0, 1).ConfigureAwait(false)) > 0)
+ {
+ char c = buffer[0];
+
+ // Only treat a line-feed as a new-line character.
+ // Carriage-returns alone are NOT considered new-line characters.
+ if (c == '\n')
+ {
+ if (lastWasCR)
+ {
+ // If the last character was a carriage-return we should remove it from the string builder
+ // since together with this line-feed it is considered a new-line character.
+ sb.Length--;
+ }
+
+ // We have a new-line character, so we should stop reading.
+ break;
+ }
+
+ lastWasCR = c == '\r';
+
+ sb.Append(c);
+ }
+
+ if (sb.Length == 0 && nr == 0)
+ {
+ return null;
+ }
+
+ return sb.ToString();
+ }
+}
diff --git a/src/shared/Core/Interop/Linux/SecretServiceCollection.cs b/src/shared/Core/Interop/Linux/SecretServiceCollection.cs
index 093baf5c3..0d6342af1 100644
--- a/src/shared/Core/Interop/Linux/SecretServiceCollection.cs
+++ b/src/shared/Core/Interop/Linux/SecretServiceCollection.cs
@@ -66,7 +66,7 @@ private unsafe IEnumerable Enumerate(string service, string account
secService,
ref schema,
queryAttrs,
- SecretSearchFlags.SECRET_SEARCH_UNLOCK,
+ SecretSearchFlags.SECRET_SEARCH_UNLOCK | SecretSearchFlags.SECRET_SEARCH_ALL,
IntPtr.Zero,
out error);
diff --git a/src/shared/Core/Interop/MacOS/MacOSKeychain.cs b/src/shared/Core/Interop/MacOS/MacOSKeychain.cs
index b024be129..9335e136d 100644
--- a/src/shared/Core/Interop/MacOS/MacOSKeychain.cs
+++ b/src/shared/Core/Interop/MacOS/MacOSKeychain.cs
@@ -302,35 +302,18 @@ private static string GetStringAttribute(IntPtr dict, IntPtr key)
return null;
}
- IntPtr buffer = IntPtr.Zero;
- try
+ if (CFDictionaryGetValueIfPresent(dict, key, out IntPtr value) && value != IntPtr.Zero)
{
- if (CFDictionaryGetValueIfPresent(dict, key, out IntPtr value) && value != IntPtr.Zero)
+ if (CFGetTypeID(value) == CFStringGetTypeID())
{
- if (CFGetTypeID(value) == CFStringGetTypeID())
- {
- int stringLength = (int)CFStringGetLength(value);
- int bufferSize = stringLength + 1;
- buffer = Marshal.AllocHGlobal(bufferSize);
- if (CFStringGetCString(value, buffer, bufferSize, CFStringEncoding.kCFStringEncodingUTF8))
- {
- return Marshal.PtrToStringAuto(buffer, stringLength);
- }
- }
-
- if (CFGetTypeID(value) == CFDataGetTypeID())
- {
- int length = CFDataGetLength(value);
- IntPtr ptr = CFDataGetBytePtr(value);
- return Marshal.PtrToStringAuto(ptr, length);
- }
+ return CFStringToString(value);
}
- }
- finally
- {
- if (buffer != IntPtr.Zero)
+
+ if (CFGetTypeID(value) == CFDataGetTypeID())
{
- Marshal.FreeHGlobal(buffer);
+ int length = CFDataGetLength(value);
+ IntPtr ptr = CFDataGetBytePtr(value);
+ return Marshal.PtrToStringAuto(ptr, length);
}
}
diff --git a/src/shared/Core/Interop/MacOS/MacOSPreferences.cs b/src/shared/Core/Interop/MacOS/MacOSPreferences.cs
new file mode 100644
index 000000000..f866b30a8
--- /dev/null
+++ b/src/shared/Core/Interop/MacOS/MacOSPreferences.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using GitCredentialManager.Interop.MacOS.Native;
+using static GitCredentialManager.Interop.MacOS.Native.CoreFoundation;
+
+namespace GitCredentialManager.Interop.MacOS;
+
+public class MacOSPreferences
+{
+ private readonly string _appId;
+
+ public MacOSPreferences(string appId)
+ {
+ EnsureArgument.NotNull(appId, nameof(appId));
+
+ _appId = appId;
+ }
+
+ ///
+ /// Return a typed value from the app preferences.
+ ///
+ /// Preference name.
+ /// Thrown if the preference is not a string.
+ ///
+ /// or null if the preference with the given key does not exist.
+ ///
+ public string GetString(string key)
+ {
+ return TryGet(key, CFStringToString, out string value)
+ ? value
+ : null;
+ }
+
+ ///
+ /// Return a typed value from the app preferences.
+ ///
+ /// Preference name.
+ /// Thrown if the preference is not an integer.
+ ///
+ /// or null if the preference with the given key does not exist.
+ ///
+ public int? GetInteger(string key)
+ {
+ return TryGet(key, CFNumberToInt32, out int value)
+ ? value
+ : null;
+ }
+
+ ///
+ /// Return a typed value from the app preferences.
+ ///
+ /// Preference name.
+ /// Thrown if the preference is not a dictionary.
+ ///
+ /// or null if the preference with the given key does not exist.
+ ///
+ public IDictionary GetDictionary(string key)
+ {
+ return TryGet(key, CFDictionaryToDictionary, out IDictionary value)
+ ? value
+ : null;
+ }
+
+ private bool TryGet(string key, Func converter, out T value)
+ {
+ IntPtr cfValue = IntPtr.Zero;
+ IntPtr keyPtr = IntPtr.Zero;
+ IntPtr appIdPtr = CreateAppIdPtr();
+
+ try
+ {
+ keyPtr = CFStringCreateWithCString(IntPtr.Zero, key, CFStringEncoding.kCFStringEncodingUTF8);
+ cfValue = CFPreferencesCopyAppValue(keyPtr, appIdPtr);
+
+ if (cfValue == IntPtr.Zero)
+ {
+ value = default;
+ return false;
+ }
+
+ value = converter(cfValue);
+ return true;
+ }
+ finally
+ {
+ if (cfValue != IntPtr.Zero) CFRelease(cfValue);
+ if (keyPtr != IntPtr.Zero) CFRelease(keyPtr);
+ if (appIdPtr != IntPtr.Zero) CFRelease(appIdPtr);
+ }
+ }
+
+ private IntPtr CreateAppIdPtr()
+ {
+ return CFStringCreateWithCString(IntPtr.Zero, _appId, CFStringEncoding.kCFStringEncodingUTF8);
+ }
+}
diff --git a/src/shared/Core/Interop/MacOS/MacOSSettings.cs b/src/shared/Core/Interop/MacOS/MacOSSettings.cs
new file mode 100644
index 000000000..3ef2c8247
--- /dev/null
+++ b/src/shared/Core/Interop/MacOS/MacOSSettings.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+
+namespace GitCredentialManager.Interop.MacOS
+{
+ ///
+ /// Reads settings from Git configuration, environment variables, and defaults from the system.
+ ///
+ public class MacOSSettings : Settings
+ {
+ private readonly ITrace _trace;
+
+ public MacOSSettings(IEnvironment environment, IGit git, ITrace trace)
+ : base(environment, git)
+ {
+ EnsureArgument.NotNull(trace, nameof(trace));
+ _trace = trace;
+
+ PlatformUtils.EnsureMacOS();
+ }
+
+ protected override bool TryGetExternalDefault(string section, string scope, string property, out string value)
+ {
+ value = null;
+
+ try
+ {
+ // Check for app default preferences for our bundle ID.
+ // Defaults can be deployed system administrators via device management profiles.
+ var prefs = new MacOSPreferences(Constants.MacOSBundleId);
+ IDictionary dict = prefs.GetDictionary("configuration");
+
+ if (dict is null)
+ {
+ // No configuration key exists
+ return false;
+ }
+
+ // Wrap the raw dictionary in one configured with the Git configuration key comparer.
+ // This means we can use the same key comparison rules as Git in our configuration plist dict,
+ // That is, sections and names are insensitive to case, but the scope is case-sensitive.
+ var config = new Dictionary(dict, GitConfigurationKeyComparer.Instance);
+
+ string name = string.IsNullOrWhiteSpace(scope)
+ ? $"{section}.{property}"
+ : $"{section}.{scope}.{property}";
+
+ if (!config.TryGetValue(name, out value))
+ {
+ // No property exists
+ return false;
+ }
+
+ _trace.WriteLine($"Default setting found in app preferences: {name}={value}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ // Reading defaults is not critical to the operation of the application
+ // so we can ignore any errors and just log the failure.
+ _trace.WriteLine("Failed to read default setting from app preferences.");
+ _trace.WriteException(ex);
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs b/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs
index 0f32a383b..9cab2ca8f 100644
--- a/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs
+++ b/src/shared/Core/Interop/MacOS/Native/CoreFoundation.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Runtime.InteropServices;
using static GitCredentialManager.Interop.MacOS.Native.LibSystem;
@@ -55,6 +56,9 @@ public static extern void CFDictionaryAddValue(
public static extern IntPtr CFStringCreateWithBytes(IntPtr alloc, byte[] bytes, long numBytes,
CFStringEncoding encoding, bool isExternalRepresentation);
+ [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr CFStringCreateWithCString(IntPtr alloc, string cStr, CFStringEncoding encoding);
+
[DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
public static extern long CFStringGetLength(IntPtr theString);
@@ -82,15 +86,130 @@ public static extern IntPtr CFStringCreateWithBytes(IntPtr alloc, byte[] bytes,
[DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
public static extern int CFArrayGetTypeID();
+ [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern int CFNumberGetTypeID();
+
[DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CFDataGetBytePtr(IntPtr theData);
[DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
public static extern int CFDataGetLength(IntPtr theData);
+
+ [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr CFPreferencesCopyAppValue(IntPtr key, IntPtr appID);
+
+ [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern bool CFNumberGetValue(IntPtr number, CFNumberType theType, out IntPtr valuePtr);
+
+ [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr CFDictionaryGetKeysAndValues(IntPtr theDict, IntPtr[] keys, IntPtr[] values);
+
+ [DllImport(CoreFoundationFrameworkLib, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
+ public static extern long CFDictionaryGetCount(IntPtr theDict);
+
+ public static string CFStringToString(IntPtr cfString)
+ {
+ if (cfString == IntPtr.Zero)
+ {
+ throw new ArgumentNullException(nameof(cfString));
+ }
+
+ if (CFGetTypeID(cfString) != CFStringGetTypeID())
+ {
+ throw new InvalidOperationException("Object is not a CFString.");
+ }
+
+ long length = CFStringGetLength(cfString);
+ IntPtr buffer = Marshal.AllocHGlobal((int)length + 1);
+
+ try
+ {
+ if (!CFStringGetCString(cfString, buffer, length + 1, CFStringEncoding.kCFStringEncodingUTF8))
+ {
+ throw new InvalidOperationException("Failed to convert CFString to C string.");
+ }
+
+ return Marshal.PtrToStringAnsi(buffer);
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+ }
+
+ public static int CFNumberToInt32(IntPtr cfNumber)
+ {
+ if (cfNumber == IntPtr.Zero)
+ {
+ throw new ArgumentNullException(nameof(cfNumber));
+ }
+
+ if (CFGetTypeID(cfNumber) != CFNumberGetTypeID())
+ {
+ throw new InvalidOperationException("Object is not a CFNumber.");
+ }
+
+ if (!CFNumberGetValue(cfNumber, CFNumberType.kCFNumberIntType, out IntPtr valuePtr))
+ {
+ throw new InvalidOperationException("Failed to convert CFNumber to Int32.");
+ }
+
+ return valuePtr.ToInt32();
+ }
+
+ public static IDictionary CFDictionaryToDictionary(IntPtr cfDict)
+ {
+ if (cfDict == IntPtr.Zero)
+ {
+ throw new ArgumentNullException(nameof(cfDict));
+ }
+
+ if (CFGetTypeID(cfDict) != CFDictionaryGetTypeID())
+ {
+ throw new InvalidOperationException("Object is not a CFDictionary.");
+ }
+
+ int count = (int)CFDictionaryGetCount(cfDict);
+ var keys = new IntPtr[count];
+ var values = new IntPtr[count];
+
+ CFDictionaryGetKeysAndValues(cfDict, keys, values);
+
+ var dict = new Dictionary(capacity: count);
+ for (int i = 0; i < count; i++)
+ {
+ string keyStr = CFStringToString(keys[i])!;
+ string valueStr = CFStringToString(values[i]);
+
+ dict[keyStr] = valueStr;
+ }
+
+ return dict;
+ }
}
public enum CFStringEncoding
{
kCFStringEncodingUTF8 = 0x08000100,
}
+
+ public enum CFNumberType
+ {
+ kCFNumberSInt8Type = 1,
+ kCFNumberSInt16Type = 2,
+ kCFNumberSInt32Type = 3,
+ kCFNumberSInt64Type = 4,
+ kCFNumberFloat32Type = 5,
+ kCFNumberFloat64Type = 6,
+ kCFNumberCharType = 7,
+ kCFNumberShortType = 8,
+ kCFNumberIntType = 9,
+ kCFNumberLongType = 10,
+ kCFNumberLongLongType = 11,
+ kCFNumberFloatType = 12,
+ kCFNumberDoubleType = 13,
+ kCFNumberCFIndexType = 14,
+ kCFNumberNSIntegerType = 15,
+ kCFNumberCGFloatType = 16
+ }
}
diff --git a/src/shared/Core/NullCredentialStore.cs b/src/shared/Core/NullCredentialStore.cs
new file mode 100644
index 000000000..fac92f47c
--- /dev/null
+++ b/src/shared/Core/NullCredentialStore.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+
+namespace GitCredentialManager;
+
+///
+/// Credential store that does nothing. This is useful when you want to disable internal credential storage
+/// and only use another helper configured in Git to store credentials.
+///
+public class NullCredentialStore : ICredentialStore
+{
+ public IList GetAccounts(string service) => Array.Empty();
+
+ public ICredential Get(string service, string account) => null;
+
+ public void AddOrUpdate(string service, string account, string secret) { }
+
+ public bool Remove(string service, string account) => false;
+}
diff --git a/src/shared/Core/Settings.cs b/src/shared/Core/Settings.cs
index 2aa71edf4..0e24ce9a3 100644
--- a/src/shared/Core/Settings.cs
+++ b/src/shared/Core/Settings.cs
@@ -189,6 +189,11 @@ public interface ISettings : IDisposable
///
bool UseSoftwareRendering { get; }
+ ///
+ /// Permit the use of unsafe remotes URLs such as regular HTTP.
+ ///
+ bool AllowUnsafeRemotes { get; }
+
///
/// Get TRACE2 settings.
///
@@ -580,6 +585,12 @@ public bool UseSoftwareRendering
}
}
+ public bool AllowUnsafeRemotes =>
+ TryGetSetting(KnownEnvars.GcmAllowUnsafeRemotes,
+ KnownGitCfg.Credential.SectionName,
+ KnownGitCfg.Credential.AllowUnsafeRemotes,
+ out string str) && str.ToBooleanyOrDefault(false);
+
public Trace2Settings GetTrace2Settings()
{
var settings = new Trace2Settings();
diff --git a/src/shared/Core/StandardStreams.cs b/src/shared/Core/StandardStreams.cs
index d0b3042b0..45f9f6cc7 100644
--- a/src/shared/Core/StandardStreams.cs
+++ b/src/shared/Core/StandardStreams.cs
@@ -39,7 +39,7 @@ public TextReader In
{
if (_stdIn == null)
{
- _stdIn = new StreamReader(Console.OpenStandardInput(), EncodingEx.UTF8NoBom);
+ _stdIn = new GitStreamReader(Console.OpenStandardInput(), EncodingEx.UTF8NoBom);
}
return _stdIn;
diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs
index d8eba64b5..535812ea8 100644
--- a/src/shared/Core/Trace2.cs
+++ b/src/shared/Core/Trace2.cs
@@ -480,13 +480,16 @@ protected override void ReleaseManagedResources()
internal static bool TryGetPipeName(string eventTarget, out string name)
{
// Use prefixes to determine whether target is a named pipe/socket
- if (eventTarget.Contains("af_unix:", StringComparison.OrdinalIgnoreCase) ||
- eventTarget.Contains("\\\\.\\pipe\\", StringComparison.OrdinalIgnoreCase) ||
- eventTarget.Contains("/./pipe/", StringComparison.OrdinalIgnoreCase))
+ if (eventTarget.StartsWith("af_unix:", StringComparison.OrdinalIgnoreCase) ||
+ eventTarget.StartsWith(@"\\.\pipe\", StringComparison.OrdinalIgnoreCase) ||
+ eventTarget.StartsWith("//./pipe/", StringComparison.OrdinalIgnoreCase))
{
name = PlatformUtils.IsWindows()
- ? eventTarget.TrimUntilLastIndexOf("\\")
- : eventTarget.TrimUntilLastIndexOf(":");
+ ? eventTarget.Replace('/', '\\')
+ .TrimUntilIndexOf(@"\\.\pipe\")
+ : eventTarget.Replace("af_unix:dgram:", "")
+ .Replace("af_unix:stream:", "")
+ .Replace("af_unix:", "");
return true;
}
diff --git a/src/shared/Core/Trace2Message.cs b/src/shared/Core/Trace2Message.cs
index cbbe48288..14327031f 100644
--- a/src/shared/Core/Trace2Message.cs
+++ b/src/shared/Core/Trace2Message.cs
@@ -409,7 +409,7 @@ public class ErrorMessage : Trace2Message
[JsonPropertyOrder(8)]
public string Message { get; set; }
- [JsonPropertyName("format")]
+ [JsonPropertyName("fmt")]
[JsonPropertyOrder(9)]
public string ParameterizedMessage { get; set; }
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."
diff --git a/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj b/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj
index 2b594e3eb..8c469897e 100644
--- a/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj
+++ b/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj
@@ -1,11 +1,10 @@
-
+
Exe
net8.0
net472;net8.0
- win-x86;osx-x64;linux-x64;osx-arm64
- x86
+ win-x86;win-x64;win-arm64;osx-x64;linux-x64;osx-arm64;linux-arm64;linux-arm
git-credential-manager
GitCredentialManager
$(RepoAssetsPath)gcmicon.ico
diff --git a/src/shared/GitHub/GitHubHostProvider.cs b/src/shared/GitHub/GitHubHostProvider.cs
index 918e859a0..21d29f651 100644
--- a/src/shared/GitHub/GitHubHostProvider.cs
+++ b/src/shared/GitHub/GitHubHostProvider.cs
@@ -285,10 +285,13 @@ public virtual Task EraseCredentialAsync(InputArguments input)
ThrowIfDisposed();
// We should not allow unencrypted communication and should inform the user
- if (StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http"))
+ if (!_context.Settings.AllowUnsafeRemotes &&
+ StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http"))
{
throw new Trace2Exception(_context.Trace2,
- "Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS.");
+ "Unencrypted HTTP is not recommended for GitHub. " +
+ "Ensure the repository remote URL is using HTTPS " +
+ $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes.");
}
string service = GetServiceName(remoteUri);
diff --git a/src/shared/GitLab/GitLabConstants.cs b/src/shared/GitLab/GitLabConstants.cs
index a686ece7a..69f1f9b9e 100644
--- a/src/shared/GitLab/GitLabConstants.cs
+++ b/src/shared/GitLab/GitLabConstants.cs
@@ -10,7 +10,6 @@ public static class GitLabConstants
// Owned by https://gitlab.com/gitcredentialmanager
public const string OAuthClientId = "172b9f227872b5dde33f4d9b1db06a6a5515ae79508e7a00c973c85ce490671e";
- public const string OAuthClientSecret = "7da92770d1447508601e4ba026bc5eb655c8268e818cd609889cc9bae2023f39";
public static readonly Uri OAuthRedirectUri = new Uri("http://127.0.0.1/");
// https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow
diff --git a/src/shared/GitLab/GitLabHostProvider.cs b/src/shared/GitLab/GitLabHostProvider.cs
index eda6e2f0f..6cda3c0e1 100644
--- a/src/shared/GitLab/GitLabHostProvider.cs
+++ b/src/shared/GitLab/GitLabHostProvider.cs
@@ -95,10 +95,13 @@ public override async Task GenerateCredentialAsync(InputArguments i
ThrowIfDisposed();
// We should not allow unencrypted communication and should inform the user
- if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http"))
+ if (!Context.Settings.AllowUnsafeRemotes &&
+ StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http"))
{
throw new Trace2Exception(Context.Trace2,
- "Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS.");
+ "Unencrypted HTTP is not recommended for GitLab. " +
+ "Ensure the repository remote URL is using HTTPS " +
+ $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes.");
}
Uri remoteUri = input.GetRemoteUri();
diff --git a/src/shared/GitLab/GitLabOAuth2Client.cs b/src/shared/GitLab/GitLabOAuth2Client.cs
index 3b146aaeb..ba72f5b41 100644
--- a/src/shared/GitLab/GitLabOAuth2Client.cs
+++ b/src/shared/GitLab/GitLabOAuth2Client.cs
@@ -59,7 +59,8 @@ private static string GetClientSecret(ISettings settings)
return clientSecret;
}
- return GitLabConstants.OAuthClientSecret;
+ // no secret necessary
+ return null;
}
}
}
diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs
index 1d5c649d0..525704886 100644
--- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs
+++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs
@@ -59,7 +59,7 @@ public bool IsSupported(InputArguments input)
return false;
}
- // We do not support unencrypted HTTP communications to Azure Repos,
+ // We do not recommend unencrypted HTTP communications to Azure Repos,
// but we report `true` here for HTTP so that we can show a helpful
// error message for the user in `CreateCredentialAsync`.
return input.TryGetHostAndPort(out string hostName, out _)
@@ -208,16 +208,22 @@ protected override void ReleaseManagedResources()
base.ReleaseManagedResources();
}
- private async Task GeneratePersonalAccessTokenAsync(InputArguments input)
+ private void ThrowIfUnsafeRemote(InputArguments input)
{
- ThrowIfDisposed();
-
- // We should not allow unencrypted communication and should inform the user
- if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http"))
+ if (!_context.Settings.AllowUnsafeRemotes &&
+ StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http"))
{
throw new Trace2Exception(_context.Trace2,
- "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS.");
+ "Unencrypted HTTP is not recommended for Azure Repos. " +
+ "Ensure the repository remote URL is using HTTPS " +
+ $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes.");
}
+ }
+
+ private async Task GeneratePersonalAccessTokenAsync(InputArguments input)
+ {
+ ThrowIfDisposed();
+ ThrowIfUnsafeRemote(input);
Uri remoteUserUri = input.GetRemoteUri(includeUser: true);
Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUserUri, out _);
@@ -257,16 +263,11 @@ private async Task GeneratePersonalAccessTokenAsync(InputArguments
private async Task GetAzureAccessTokenAsync(InputArguments input)
{
+ ThrowIfUnsafeRemote(input);
+
Uri remoteWithUserUri = input.GetRemoteUri(includeUser: true);
string userName = input.UserName;
- // We should not allow unencrypted communication and should inform the user
- if (StringComparer.OrdinalIgnoreCase.Equals(remoteWithUserUri.Scheme, "http"))
- {
- throw new Trace2Exception(_context.Trace2,
- "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS.");
- }
-
Uri orgUri = UriHelpers.CreateOrganizationUri(remoteWithUserUri, out string orgName);
_context.Trace.WriteLine($"Determining Microsoft Authentication authority for Azure DevOps organization '{orgName}'...");
diff --git a/src/shared/TestInfrastructure/Objects/TestSettings.cs b/src/shared/TestInfrastructure/Objects/TestSettings.cs
index f14bf6cc9..3e67e39b0 100644
--- a/src/shared/TestInfrastructure/Objects/TestSettings.cs
+++ b/src/shared/TestInfrastructure/Objects/TestSettings.cs
@@ -53,6 +53,8 @@ public class TestSettings : ISettings
public bool UseMsAuthDefaultAccount { get; set; }
+ public bool AllowUnsafeRemotes { get; set; } = false;
+
public Trace2Settings GetTrace2Settings()
{
return new Trace2Settings()
@@ -189,6 +191,8 @@ ProxyConfiguration ISettings.GetProxyConfiguration()
bool ISettings.UseSoftwareRendering => false;
+ bool ISettings.AllowUnsafeRemotes => AllowUnsafeRemotes;
+
#endregion
#region IDisposable
diff --git a/src/shared/TestInfrastructure/TestUtils.cs b/src/shared/TestInfrastructure/TestUtils.cs
index c547856d7..000b8e75e 100644
--- a/src/shared/TestInfrastructure/TestUtils.cs
+++ b/src/shared/TestInfrastructure/TestUtils.cs
@@ -1,5 +1,7 @@
using System;
+using System.Diagnostics;
using System.IO;
+using System.Threading.Tasks;
namespace GitCredentialManager.Tests
{
@@ -87,5 +89,41 @@ public static string GetUuid(int length = -1)
return uuid.Substring(0, length);
}
+
+ public static async Task RunCommandAsync(string filePath, string arguments, string workingDirectory = null)
+ {
+ using var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = filePath,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory
+ }
+ };
+
+ process.Start();
+
+ string output = await process.StandardOutput.ReadToEndAsync();
+ string error = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+
+ if (process.ExitCode != 0)
+ {
+ throw new InvalidOperationException(
+ $"Command `{filePath} {arguments}` failed with exit code {process.ExitCode}." +
+ Environment.NewLine +
+ $"Output: {output}" +
+ Environment.NewLine +
+ $"Error: {error}");
+ }
+
+ return output;
+ }
}
}
diff --git a/src/windows/Installer.Windows/Installer.Windows.csproj b/src/windows/Installer.Windows/Installer.Windows.csproj
index bbd49a291..eae3631f0 100644
--- a/src/windows/Installer.Windows/Installer.Windows.csproj
+++ b/src/windows/Installer.Windows/Installer.Windows.csproj
@@ -1,12 +1,19 @@
-
+
+
+
+ win-x64
+ win-x86
+ win-arm64
+
+
net472
false
false
- $(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\win-x86
+ $(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\$(RuntimeIdentifier)
6.3.1
@@ -27,12 +34,20 @@
- "$(NuGetPackageRoot)Tools.InnoSetup\$(InnoSetupVersion)\tools\ISCC.exe" /DPayloadDir="$(PayloadPath)" /DInstallTarget=system "$(RepoSrcPath)\windows\Installer.Windows\Setup.iss" /O"$(OutputPath)"
- "$(NuGetPackageRoot)Tools.InnoSetup\$(InnoSetupVersion)\tools\ISCC.exe" /DPayloadDir="$(PayloadPath)" /DInstallTarget=user "$(RepoSrcPath)\windows\Installer.Windows\Setup.iss" /O"$(OutputPath)"
+ "$(NuGetPackageRoot)Tools.InnoSetup\$(InnoSetupVersion)\tools\ISCC.exe" /DPayloadDir="$(PayloadPath)" /DInstallTarget=system /DGcmRuntimeIdentifier="$(RuntimeIdentifier)" "$(RepoSrcPath)\windows\Installer.Windows\Setup.iss" /O"$(OutputPath)"
+ "$(NuGetPackageRoot)Tools.InnoSetup\$(InnoSetupVersion)\tools\ISCC.exe" /DPayloadDir="$(PayloadPath)" /DInstallTarget=user /DGcmRuntimeIdentifier="$(RuntimeIdentifier)" "$(RepoSrcPath)\windows\Installer.Windows\Setup.iss" /O"$(OutputPath)"
-
+
+
+
+
+
+
diff --git a/src/windows/Installer.Windows/Setup.iss b/src/windows/Installer.Windows/Setup.iss
index f03d16c9b..c15efe6d8 100644
--- a/src/windows/Installer.Windows/Setup.iss
+++ b/src/windows/Installer.Windows/Setup.iss
@@ -15,6 +15,10 @@
#error Installer target property 'InstallTarget' must be specifed
#endif
+#ifndef GcmRuntimeIdentifier
+ #error GCM Runtime Identifier 'GcmRuntimeIdentifier' must be specifed (e.g. win-x64)
+#endif
+
#if InstallTarget == "user"
#define GcmAppId "{{aa76d31d-432c-42ee-844c-bc0bc801cef3}}"
#define GcmLongName "Git Credential Manager (User)"
@@ -40,7 +44,6 @@
#define GcmRepoRoot "..\..\.."
#define GcmAssets GcmRepoRoot + "\assets"
#define GcmExe "git-credential-manager.exe"
-#define GcmArch "x86"
#ifnexist PayloadDir + "\" + GcmExe
#error Payload files are missing
@@ -67,9 +70,17 @@ AppUpdatesURL={#GcmUrl}
AppContact={#GcmUrl}
AppCopyright={#GcmCopyright}
AppReadmeFile={#GcmReadme}
+; Windows ARM64 supports installing and running x64 binaries, but not vice versa.
+#if GcmRuntimeIdentifier=="win-x64"
+ArchitecturesAllowed=x64compatible
+ArchitecturesInstallIn64BitMode=x64compatible
+#elif GcmRuntimeIdentifier=="win-arm64"
+ArchitecturesAllowed=arm64
+ArchitecturesInstallIn64BitMode=arm64
+#endif
VersionInfoVersion={#GcmVersion}
LicenseFile={#GcmRepoRoot}\LICENSE
-OutputBaseFilename={#GcmSetupExe}-win-{#GcmArch}-{#GcmVersionSimple}
+OutputBaseFilename={#GcmSetupExe}-{#GcmRuntimeIdentifier}-{#GcmVersionSimple}
DefaultDirName={autopf}\{#GcmShortName}
Compression=lzma2
SolidCompression=yes
diff --git a/src/windows/Installer.Windows/layout.ps1 b/src/windows/Installer.Windows/layout.ps1
index 070c9bf49..3fc43ab36 100644
--- a/src/windows/Installer.Windows/layout.ps1
+++ b/src/windows/Installer.Windows/layout.ps1
@@ -1,20 +1,43 @@
# Inputs
-param ([Parameter(Mandatory)] $CONFIGURATION, [Parameter(Mandatory)] $OUTPUT, $SYMBOLOUTPUT)
+param ([Parameter(Mandatory)] $Configuration, [Parameter(Mandatory)] $Output, $RuntimeIdentifier, $SymbolOutput)
-Write-Output "Output: $OUTPUT"
+Write-Output "Output: $Output"
+
+# Determine a runtime if one was not provided
+if (-not $RuntimeIdentifier) {
+ $arch = $env:PROCESSOR_ARCHITECTURE
+ switch ($arch) {
+ "AMD64" { $RuntimeIdentifier = "win-x64" }
+ "x86" { $RuntimeIdentifier = "win-x86" }
+ "ARM64" { $RuntimeIdentifier = "win-arm64" }
+ default {
+ Write-Host "Unknown architecture: $arch"
+ exit 1
+ }
+ }
+}
+
+Write-Output "Building for runtime '$RuntimeIdentifier'"
+
+if ($RuntimeIdentifier -ne 'win-x86' -and $RuntimeIdentifier -ne 'win-x64' -and $RuntimeIdentifier -ne 'win-arm64') {
+ Write-Host "Unsupported RuntimeIdentifier: $RuntimeIdentifier"
+ exit 1
+}
# Directories
-$THISDIR = $pwd.path
-$ROOT = (Get-Item $THISDIR).parent.parent.parent.FullName
-$SRC = "$ROOT/src"
-$GCM_SRC = "$SRC/shared/Git-Credential-Manager"
+$THISDIR = $PSScriptRoot
+$ROOT = (Get-Item $THISDIR).Parent.Parent.Parent.FullName
+$SRC = "$ROOT\src"
+$GCM_SRC = "$SRC\shared\Git-Credential-Manager"
# Perform pre-execution checks
-$PAYLOAD = "$OUTPUT"
-if ($SYMBOLOUTPUT)
+$PAYLOAD = "$Output"
+if ($SymbolOutput)
+{
+ $SYMBOLS = "$SymbolOutput"
+}
+else
{
- $SYMBOLS = "$SYMBOLOUTPUT"
-} else {
$SYMBOLS = "$PAYLOAD.sym"
}
@@ -32,37 +55,65 @@ if (Test-Path -Path $SYMBOLS)
}
# Ensure payload and symbol directories exist
-mkdir -p "$PAYLOAD","$SYMBOLS"
+mkdir -p "$PAYLOAD","$SYMBOLS" | Out-Null
# Publish core application executables
Write-Output "Publishing core application..."
dotnet publish "$GCM_SRC" `
--framework net472 `
- --configuration "$CONFIGURATION" `
- --runtime win-x86 `
+ --configuration "$Configuration" `
+ --runtime $RuntimeIdentifier `
--output "$PAYLOAD"
# Delete libraries that are not needed for Windows but find their way
# into the publish output.
-Remove-Item -Path "$PAYLOAD/*.dylib" -Force
-
-# Delete extraneous files that get included for other architectures
-# We only care about x86 as the core GCM executable is only targeting x86
-Remove-Item -Path "$PAYLOAD/arm/" -Recurse -Force
-Remove-Item -Path "$PAYLOAD/arm64/" -Recurse -Force
-Remove-Item -Path "$PAYLOAD/x64/" -Recurse -Force
-Remove-Item -Path "$PAYLOAD/musl-x64/" -Recurse -Force
-Remove-Item -Path "$PAYLOAD/runtimes/win-arm64/" -Recurse -Force
-Remove-Item -Path "$PAYLOAD/runtimes/win-x64/" -Recurse -Force
-
-# The Avalonia and MSAL binaries in these directories are already included in
-# the $PAYLOAD directory directly, so we can delete these extra copies.
-Remove-Item -Path "$PAYLOAD/x86/libSkiaSharp.dll" -Recurse -Force
-Remove-Item -Path "$PAYLOAD/x86/libHarfBuzzSharp.dll" -Recurse -Force
-Remove-Item -Path "$PAYLOAD/runtimes/win-x86/native/msalruntime_x86.dll" -Recurse -Force
+Remove-Item -Path "$PAYLOAD/*.dylib" -Force -ErrorAction Ignore
+
+# Delete extraneous files that get included for other runtimes
+Remove-Item -Path "$PAYLOAD/musl-x64/" -Recurse -Force -ErrorAction Ignore
+
+switch ($RuntimeIdentifier) {
+ "win-x86" {
+ Remove-Item -Path "$PAYLOAD/arm/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/arm64/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/x64/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/runtimes/win-arm64/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/runtimes/win-x64/" -Recurse -Force -ErrorAction Ignore
+ # The Avalonia and MSAL binaries are already included in the $PAYLOAD directory directly
+ Remove-Item -Path "$PAYLOAD/x86/libSkiaSharp.dll" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/x86/libHarfBuzzSharp.dll" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/runtimes/win-x86/native/msalruntime_x86.dll" -Force -ErrorAction Ignore
+ }
+ "win-x64" {
+ Remove-Item -Path "$PAYLOAD/arm/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/arm64/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/x86/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/runtimes/win-arm64/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/runtimes/win-x86/" -Recurse -Force -ErrorAction Ignore
+ # The Avalonia and MSAL binaries are already included in the $PAYLOAD directory directly
+ Remove-Item -Path "$PAYLOAD/x64/libSkiaSharp.dll" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/x64/libHarfBuzzSharp.dll" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/x64/libSkiaSharp.so" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/x64/libHarfBuzzSharp.so" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/runtimes/win-x64/native/msalruntime.dll" -Force -ErrorAction Ignore
+ }
+ "win-arm64" {
+ Remove-Item -Path "$PAYLOAD/arm/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/x86/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/x64/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/runtimes/win-x86/" -Recurse -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/runtimes/win-x64/" -Recurse -Force -ErrorAction Ignore
+ # The Avalonia and MSAL binaries are already included in the $PAYLOAD directory directly
+ Remove-Item -Path "$PAYLOAD/arm64/libSkiaSharp.dll" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/arm64/libHarfBuzzSharp.dll" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/arm64/libSkiaSharp.so" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/arm64/libHarfBuzzSharp.so" -Force -ErrorAction Ignore
+ Remove-Item -Path "$PAYLOAD/runtimes/win-arm64/native/msalruntime_arm64.dll" -Force -ErrorAction Ignore
+ }
+}
# Delete localized resource assemblies - we don't localize the core GCM assembly anyway
-Get-ChildItem "$PAYLOAD" -Recurse -Include "*.resources.dll" | Remove-Item -Force
+Get-ChildItem "$PAYLOAD" -Recurse -Include "*.resources.dll" | Remove-Item -Force -ErrorAction Ignore
# Delete any empty directories
Get-ChildItem "$PAYLOAD" -Recurse -Directory `