Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
27c1938
azure-pipelines: flesh out release pipeline infrastructure
mjcheetham Apr 30, 2026
478f190
azure-pipelines: add ESRP code signing
mjcheetham Apr 30, 2026
ca8b3e1
azure-pipelines: install build dependencies for Linux build
dscho Apr 30, 2026
318ea04
azure-pipelines: build microsoft-git Debian package for Linux
dscho Apr 30, 2026
c0a9265
azure-pipelines: stage signed Debian package for upload
dscho Apr 30, 2026
6296dbb
azure-pipelines: install macOS build dependencies
dscho Apr 30, 2026
02997b8
azure-pipelines: configure universal macOS build
dscho Apr 30, 2026
39138dd
azure-pipelines: build payload via macos-installer Makefile
dscho Apr 30, 2026
06a2023
azure-pipelines: ESRP-sign macOS payload binaries
dscho Apr 30, 2026
2488948
azure-pipelines: build unsigned macOS installer pkg
dscho Apr 30, 2026
648d0e3
azure-pipelines: ESRP-sign macOS installer pkg
dscho Apr 30, 2026
54e9eab
azure-pipelines: ESRP-notarize macOS installer pkg
dscho Apr 30, 2026
7664c83
azure-pipelines: build macOS DMG and stage artifacts for upload
dscho Apr 30, 2026
6870ac3
azure-pipelines: add Windows path conversion utilities
mjcheetham May 1, 2026
e181dba
azure-pipelines: bootstrap Git for Windows SDK on Windows agents
dscho May 1, 2026
1add977
azure-pipelines: install Azure CLI on Windows arm64 agents
mjcheetham Apr 30, 2026
2427baf
azure-pipelines: clone build-extra into Windows SDK
dscho May 1, 2026
29d76fa
azure-pipelines: build mingw-w64-git package on Windows
dscho May 1, 2026
ffbbe68
azure-pipelines: build Windows installer and portable Git
dscho May 1, 2026
76f6847
azure-pipelines: customise the Windows installer via patches
mjcheetham May 1, 2026
407f744
azure-pipelines: stage Windows installer artifacts for upload
dscho May 1, 2026
87014fe
azure-pipelines: ESRP-sign Windows installer artifacts via signtool a…
mjcheetham May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .azure-pipelines/esrp/sign.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Reusable step template for ESRP code signing via EsrpCodeSigning@6.
#
# For macOS, ESRP requires files to be submitted as a zip archive.
# Set 'useArchive: true' to automatically handle the
# copy → zip → sign → extract cycle. For Windows/Linux where ESRP
# can sign files directly in a folder, leave it as false (default).
#
parameters:
- name: displayName
type: string
- name: folderPath
type: string
- name: pattern
type: string
- name: inlineOperation
type: string
# When true, matching files are copied to a staging dir, zipped,
# signed, and extracted back to folderPath.
- name: useArchive
type: boolean
default: false
# ESRP connection parameters (defaults use pipeline variables)
- name: connectedServiceName
type: string
default: $(esrpAppConnectionName)
- name: appRegistrationClientId
type: string
default: $(esrpClientId)
- name: appRegistrationTenantId
type: string
default: $(esrpTenantId)
- name: authAkvName
type: string
default: $(esrpKeyVaultName)
- name: authSignCertName
type: string
default: $(esrpSignReqCertName)
- name: serviceEndpointUrl
type: string
default: $(esrpEndpointUrl)

steps:
- ${{ if eq(parameters.useArchive, true) }}:
- task: DeleteFiles@1
displayName: 'Clean staging dir for ${{ parameters.displayName }}'
inputs:
SourceFolder: '$(Agent.TempDirectory)/esrp-staging'
Contents: '*'
RemoveSourceFolder: true
- task: CopyFiles@2
displayName: 'Collect files for ${{ parameters.displayName }}'
inputs:
SourceFolder: '${{ parameters.folderPath }}'
Contents: '${{ parameters.pattern }}'
TargetFolder: '$(Agent.TempDirectory)/esrp-staging/contents'
- task: ArchiveFiles@2
displayName: 'Archive files for ${{ parameters.displayName }}'
inputs:
rootFolderOrFile: '$(Agent.TempDirectory)/esrp-staging/contents'
includeRootFolder: false
archiveType: zip
archiveFile: '$(Agent.TempDirectory)/esrp-staging/archive.zip'
- task: EsrpCodeSigning@6
displayName: '${{ parameters.displayName }}'
inputs:
connectedServiceName: '${{ parameters.connectedServiceName }}'
useMSIAuthentication: true
appRegistrationClientId: '${{ parameters.appRegistrationClientId }}'
appRegistrationTenantId: '${{ parameters.appRegistrationTenantId }}'
authAkvName: '${{ parameters.authAkvName }}'
authSignCertName: '${{ parameters.authSignCertName }}'
serviceEndpointUrl: '${{ parameters.serviceEndpointUrl }}'
folderPath: '$(Agent.TempDirectory)/esrp-staging'
pattern: 'archive.zip'
useMinimatch: true
signConfigType: inlineSignParams
inlineOperation: ${{ parameters.inlineOperation }}
- task: ExtractFiles@1
displayName: 'Extract signed files for ${{ parameters.displayName }}'
inputs:
archiveFilePatterns: '$(Agent.TempDirectory)/esrp-staging/archive.zip'
destinationFolder: '${{ parameters.folderPath }}'
overwriteExistingFiles: true
- task: DeleteFiles@1
displayName: 'Clean up staging dir for ${{ parameters.displayName }}'
condition: always()
inputs:
SourceFolder: '$(Agent.TempDirectory)/esrp-staging'
Contents: '*'
RemoveSourceFolder: true
- ${{ else }}:
- task: EsrpCodeSigning@6
displayName: '${{ parameters.displayName }}'
inputs:
connectedServiceName: '${{ parameters.connectedServiceName }}'
useMSIAuthentication: true
appRegistrationClientId: '${{ parameters.appRegistrationClientId }}'
appRegistrationTenantId: '${{ parameters.appRegistrationTenantId }}'
authAkvName: '${{ parameters.authAkvName }}'
authSignCertName: '${{ parameters.authSignCertName }}'
serviceEndpointUrl: '${{ parameters.serviceEndpointUrl }}'
folderPath: '${{ parameters.folderPath }}'
pattern: '${{ parameters.pattern }}'
useMinimatch: true
signConfigType: inlineSignParams
inlineOperation: ${{ parameters.inlineOperation }}
173 changes: 173 additions & 0 deletions .azure-pipelines/esrp/windows/esrpsign.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#!/bin/bash
#
# Sign Windows files using the ESRP client (Authenticode).
# Usage: esrpsign.sh <file1> [file2 ...]
#
# Required environment variables:
# ESRP_TOOL - Path to ESRPClient.exe
# ESRP_AUTH - Path to the ESRP auth JSON file
# SYSTEM_ACCESSTOKEN - ADO system access token (OAuth bearer)
#
# Optional environment variables:
# ESRP_KEYCODE - Signing key code (default: CP-231522)
#
# The script generates the auth and input JSON files and sets the
# following ESRP client environment variables automatically:
# ESRP_AUTH_CONFIG - Path to the auth JSON file
# ESRP_POLICY_CONFIG - Path to the policy JSON file
# ESRP_SESSION_CONFIG - Not set; ESRP client defaults are used
#
set -euo pipefail

if [ $# -lt 1 ]; then
echo "usage: esrpsign.sh <file> [file ...]" >&2
exit 1
fi

if [ -z "${ESRP_TOOL:-}" ]; then
echo "error: ESRP_TOOL environment variable must be set" >&2
exit 1
fi
if [ -z "${ESRP_AUTH:-}" ]; then
echo "error: ESRP_AUTH environment variable must be set" >&2
exit 1
fi
if [ -z "${SYSTEM_ACCESSTOKEN:-}" ]; then
echo "error: SYSTEM_ACCESSTOKEN environment variable must be set" >&2
exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/../../scripts/windows/utils.sh"

# Check for overriden key code, otherwise use default (Microsoft Third-Party/OSS)
ESRP_KEYCODE="${ESRP_KEYCODE:-CP-231522}"

# Create work dir and resolve its Windows path by cd-ing into it.
WORK_DIR="$(mktemp -d)"
WORK_DIR_WIN="$(cd "$WORK_DIR" && pwd -W | sed 's|/|\\|g')"

echo "==> ESRP signing tool: $ESRP_TOOL"
echo "==> Working directory: $WORK_DIR"

if [ ! -f "$ESRP_TOOL" ]; then
echo "error: ESRPClient.exe not found at $ESRP_TOOL" >&2
exit 1
fi

# Build the SignRequestFiles JSON array
echo "==> Preparing files for signing ($# file(s))..."
files_json=""
for file in "$@"; do
if [ ! -f "$file" ]; then
echo "error: file not found: $file" >&2
exit 1
fi

abs_path="$(cd "$(dirname "$file")" && pwd)/$(basename "$file")"
win_path="$(to_windows_path "$abs_path")"
# Escape backslashes for JSON
win_path_escaped="${win_path//\\/\\\\}"
echo " - $win_path"

if [ -n "$files_json" ]; then
files_json+=","
fi
files_json+="
{
\"SourceLocation\": \"$win_path_escaped\",
\"DestinationLocation\": \"$win_path_escaped\"
}"
done

# Generate the input JSON
input_json="$WORK_DIR/input.json"
output_json="$WORK_DIR/output.json"

echo "==> Generating input JSON: $input_json"
cat > "$input_json" <<-EOF
{
"Version": "1.0.0",
"SignBatches": [
{
"SourceLocationType": "UNC",
"DestinationLocationType": "UNC",
"SignRequestFiles": [$files_json
],
"SigningInfo": {
"Operations": [
{
"KeyCode": "$ESRP_KEYCODE",
"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": "$ESRP_KEYCODE",
"OperationCode": "SigntoolVerify",
"ToolName": "sign",
"ToolVersion": "1.0",
"Parameters": {}
}
]
}
}
]
}
EOF

# Generate policy JSON
echo "==> Generating policy JSON..."
policy_json="$WORK_DIR/policy.json"
cat > "$policy_json" <<-EOF
{
"Version": "1.0.0",
"Intent": "ProductRelease",
"ContentType": "Binaries",
"ContentOrigin": "1stParty",
"ProductState": "Current",
"Audience": "ExternalBroad"
}
EOF

# Use auth JSON from ESRP_AUTH
export ESRP_AUTH_CONFIG="$(to_windows_path "$ESRP_AUTH")"
export ESRP_POLICY_CONFIG="$WORK_DIR_WIN\\policy.json"

# The ADO system access token is referenced in the auth JSON via the environment
# variable - export this so the ESRP client can pick it up when it runs.
export SYSTEM_ACCESSTOKEN

# Print generated JSON files for debugging
echo "==> Auth JSON:"
cat "$ESRP_AUTH"
echo ""
echo "==> Policy JSON:"
cat "$policy_json"
echo ""
echo "==> Input JSON:"
cat "$input_json"
echo ""

# Sign the files
esrp_tool_win="$(to_windows_path "$ESRP_TOOL")"
input_json_win="$WORK_DIR_WIN\\input.json"
output_json_win="$WORK_DIR_WIN\\output.json"

echo "==> ESRP_AUTH_CONFIG=$ESRP_AUTH_CONFIG"
echo "==> ESRP_POLICY_CONFIG=$ESRP_POLICY_CONFIG"
echo "==> Running: $esrp_tool_win sign -i $input_json_win -o $output_json_win"
"$esrp_tool_win" sign \
-i "$input_json_win" \
-o "$output_json_win"

echo "==> Signing complete."
echo "==> Output JSON:"
cat "$output_json"
69 changes: 69 additions & 0 deletions .azure-pipelines/esrp/windows/setup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
parameters:
- name: serviceConnectionName
type: string
- name: esrpClientId
type: string
- name: keyVaultName
type: string
- name: signCertName
type: string

steps:
- task: EsrpClientTool@4
name: esrpinstall
displayName: 'Install ESRP client'
- task: AzureCLI@2
displayName: 'Set up ESRP environment'
inputs:
azureSubscription: ${{ parameters.serviceConnectionName }}
addSpnToEnvironment: true
scriptType: ps
scriptLocation: inlineScript
inlineScript: |
# Resolve ESRP client tool path (passed via env to avoid PS subexpression issues)
$esrpTool = "$env:ESRPCLIENT_TOOLPATH\$env:ESRPCLIENT_TOOLNAME"
if (-not (Test-Path $esrpTool)) { Write-Error "ESRPClient.exe not found at $esrpTool"; exit 1 }
Write-Host "Found ESRP client: $esrpTool"
Write-Host "##vso[task.setvariable variable=ESRP_TOOL]$esrpTool"

# Derive the service connection GUID from the ENDPOINT_URL_* env vars
# that the agent emits for the bound connection. Filter out the
# built-in SystemVssConnection which is always present.
$scId = (Get-ChildItem env:ENDPOINT_URL_*).Name `
-replace '^ENDPOINT_URL_','' |
Where-Object { $_ -ne 'SYSTEMVSSCONNECTION' }
if (-not $scId) { Write-Error "Could not derive service connection GUID"; exit 1 }
Write-Host "Resolved service connection GUID: $scId"

# servicePrincipalId and tenantId are provided by addSpnToEnvironment
$authJson = @{
Version = "1.0.0"
AuthenticationType = "AAD_MSI_WIF"
EsrpClientId = "${{ parameters.esrpClientId }}"
ClientId = $env:servicePrincipalId
TenantId = $env:tenantId
AADAuthorityBaseUri = "https://login.microsoftonline.com/"
FederatedTokenData = @{
JobId = "$(System.JobId)"
PlanId = "$(System.PlanId)"
ProjectId = "$(System.TeamProjectId)"
Hub = "$(System.HostType)"
Uri = "$(System.CollectionUri)"
ServiceConnectionId = $scId
SystemAccessToken = "SYSTEM_ACCESSTOKEN"
}
RequestSigningCert = @{
GetCertFromKeyVault = $true
KeyVaultName = "${{ parameters.keyVaultName }}"
KeyVaultCertName = "${{ parameters.signCertName }}"
}
} | ConvertTo-Json -Depth 4

$authPath = "$(Agent.TempDirectory)\esrp-auth.json"
$authJson | Set-Content -Path $authPath -Encoding UTF8
Write-Host "Generated ESRP auth JSON: $authPath"
Write-Host "##vso[task.setvariable variable=ESRP_AUTH]$authPath"
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
ESRPCLIENT_TOOLPATH: $(esrpinstall.esrpclient.toolpath)
ESRPCLIENT_TOOLNAME: $(esrpinstall.esrpclient.toolname)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/installer/install.iss b/installer/install.iss
index 70787b7..137f660 100644
--- a/installer/install.iss
+++ b/installer/install.iss
@@ -65,7 +65,7 @@ SignTool=signtool
; Installer-related
AllowNoIcons=yes
AppName={#APP_NAME}
-AppPublisher=The Git Development Community
+AppPublisher=The Git Client Team at Microsoft
AppPublisherURL={#APP_URL}
AppSupportURL={#APP_CONTACT_URL}
AppVersion={#APP_VERSION}
Loading
Loading