diff --git a/.azure-pipelines/esrp/sign.yml b/.azure-pipelines/esrp/sign.yml new file mode 100644 index 00000000000000..b4d14d2713ee8f --- /dev/null +++ b/.azure-pipelines/esrp/sign.yml @@ -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 }} diff --git a/.azure-pipelines/esrp/windows/esrpsign.sh b/.azure-pipelines/esrp/windows/esrpsign.sh new file mode 100755 index 00000000000000..a3bf1bc66ea4f8 --- /dev/null +++ b/.azure-pipelines/esrp/windows/esrpsign.sh @@ -0,0 +1,198 @@ +#!/bin/bash +# +# Sign Windows files using the ESRP client (Authenticode). +# Usage: esrpsign.sh [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 ...]" >&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)" + +# 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 + +# Convert an MSYS2 path to Windows format for ESRPClient.exe. +to_windows_path () { + # Prefer cygpath if available (full Git for Windows) + if command -v cygpath >/dev/null 2>&1; then + cygpath -w "$1" + return + fi + case "$1" in + /[a-zA-Z]/*) + # Drive path: /d/path -> D:\path + drive=$(echo "$1" | cut -c2 | tr 'a-z' 'A-Z') + rest=$(echo "$1" | cut -c3-) + echo "${drive}:${rest}" | sed 's|/|\\|g' + ;; + /*) + # Absolute path under MSYS2 root + root=$(cd / && pwd -W) + echo "${root}${1}" | sed 's|/|\\|g' + ;; + # Relative or already-Windows path: just flip slashes + *) + echo "$1" | sed 's|/|\\|g' + ;; + esac +} + +# 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" diff --git a/.azure-pipelines/esrp/windows/setup.yml b/.azure-pipelines/esrp/windows/setup.yml new file mode 100644 index 00000000000000..0653b868d0a728 --- /dev/null +++ b/.azure-pipelines/esrp/windows/setup.yml @@ -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) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 0e0ce01ac71978..f0e79b31926149 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -10,6 +10,10 @@ resources: ref: refs/tags/release parameters: + - name: 'esrp' + type: boolean + default: false # TODO: change default to true after testing + displayName: 'Enable ESRP code signing' - name: 'github' type: boolean default: false # TODO: change default to true after testing @@ -26,6 +30,7 @@ parameters: - id: windows_x64 jobName: 'Windows (x64)' pool: GitClientPME-1ESHostedPool-intel-pc + poolArch: amd64 image: win-x86_64-ado1es os: windows toolchain: x86_64 @@ -34,13 +39,20 @@ parameters: - id: windows_arm64 jobName: 'Windows (ARM64)' pool: GitClientPME-1ESHostedPool-arm64-pc + poolArch: arm64 image: win-arm64-ado1es os: windows toolchain: clang-aarch64 mingwprefix: clangarm64 - # No matrix for macOS as we build both x64 and ARM64 in the same job - # and produce a universal binary. + - name: macos_matrix + type: object + default: + - id: macos_universal + jobName: 'macOS (x64 + ARM64)' + pool: 'Azure Pipelines' + image: macOS-latest + os: macos - name: linux_matrix type: object @@ -48,6 +60,7 @@ parameters: - id: linux_x64 jobName: 'Linux (x64)' pool: GitClientPME-1ESHostedPool-intel-pc + poolArch: amd64 image: ubuntu-x86_64-ado1es os: linux cc_arch: x86_64 @@ -56,14 +69,24 @@ parameters: - id: linux_arm64 jobName: 'Linux (ARM64)' pool: GitClientPME-1ESHostedPool-arm64-pc + poolArch: arm64 image: ubuntu-arm64-ado1es os: linux cc_arch: aarch64 deb_arch: arm64 variables: + - name: 'esrpAppConnectionName' + value: '1ESGitClient-ESRP-App' - name: 'githubConnectionName' value: 'GitHub-MicrosoftGit' + # ESRP signing variables set in the pipeline settings: + # - esrpEndpointUrl + # - esrpMI + # - esrpClientId + # - esrpTenantId + # - esrpKeyVaultName + # - esrpSignReqCertName extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelines @@ -85,20 +108,26 @@ extends: image: ubuntu-x86_64-ado1es os: linux steps: + - checkout: self + fetchDepth: 0 + fetchTags: true - task: Bash@3 displayName: 'Resolve version and tag information' name: info inputs: targetType: inline script: | - # TODO: determine git_version, tag_name, and tag_sha - # TODO: error if the current commit is not an annotated tag - git_version=TODO_GITVER - tag_name=TODO_TAGNAME - tag_sha=TODO_TAGSHA + set -euo pipefail + tag_name="v9.99.99.vfs.0.0" + tag_sha="$(git rev-parse HEAD)" + git_version="9.99.99.vfs.0.0" + echo "Git version: $git_version" + echo "Tag name: $tag_name" + echo "Tag SHA: $tag_sha" echo "##vso[task.setvariable variable=git_version;isOutput=true;isReadOnly=true]$git_version" echo "##vso[task.setvariable variable=tag_name;isOutput=true;isReadOnly=true]$tag_name" echo "##vso[task.setvariable variable=tag_sha;isOutput=true;isReadOnly=true]$tag_sha" + echo "##vso[build.updatebuildnumber]${tag_name} ($(Build.BuildNumber))" - stage: build displayName: 'Build' @@ -114,6 +143,7 @@ extends: name: ${{ dim.pool }} image: ${{ dim.image }} os: ${{ dim.os }} + hostArchitecture: ${{ dim.poolArch }} variables: tag_name: $[stageDependencies.prereqs.prebuild.outputs['info.tag_name']] tag_sha: $[stageDependencies.prereqs.prebuild.outputs['info.tag_sha']] @@ -127,44 +157,119 @@ extends: artifactName: '${{ dim.id }}' steps: - checkout: self + # Add Git Bash to the PATH so Bash tasks can find it + - task: BatchScript@1 + displayName: 'Add Git Bash to PATH' + inputs: + filename: ./.azure-pipelines/scripts/windows/setup-git-bash.cmd + # Install Azure CLI on arm64 (not pre-installed on these agents) + - ${{ if eq(dim.poolArch, 'arm64') }}: + - powershell: | + $ProgressPreference = 'SilentlyContinue' + $msi = "$env:TEMP\AzureCLI.msi" + Write-Host "Downloading Azure CLI (x64)..." + Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile $msi + Write-Host "Installing Azure CLI..." + Start-Process msiexec.exe -ArgumentList "/i", $msi, "/quiet", "/norestart" -Wait + $azPath = "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin" + Write-Host "##vso[task.prependpath]$azPath" + Write-Host "Azure CLI installed." + displayName: 'Install Azure CLI (arm64)' + # Setup ESRP code signing for Windows (sets ESRP_TOOL, ESRP_AUTH) + - ${{ if eq(parameters.esrp, true) }}: + - template: .azure-pipelines/esrp/windows/setup.yml@self + parameters: + serviceConnectionName: $(esrpAppConnectionName) + esrpClientId: $(esrpClientId) + keyVaultName: $(esrpKeyVaultName) + signCertName: $(esrpSignReqCertName) # TODO: add tasks to set up Git for Windows SDK # TODO: add tasks to build Git and installers - script: | echo $(mingwprefix) echo $(toolchain) + mkdir $(Build.ArtifactStagingDirectory)\app + copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example1.exe + copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example2.exe + copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example3.exe displayName: 'Dummy build' + # + # To sign Windows binaries with ESRP, call esrpsign.sh + # with the files to sign as arguments. Requires the + # following environment variables to be set: + # ESRP_TOOL - set by the setup template above + # ESRP_AUTH - set by the setup template above + # SYSTEM_ACCESSTOKEN - $(System.AccessToken) + # + - ${{ if eq(parameters.esrp, true) }}: + - bash: | + .azure-pipelines/esrp/windows/esrpsign.sh \ + "$BUILD_ARTIFACTSTAGINGDIRECTORY/app/example1.exe" \ + "$BUILD_ARTIFACTSTAGINGDIRECTORY/app/example2.exe" \ + "$BUILD_ARTIFACTSTAGINGDIRECTORY/app/example3.exe" + displayName: 'Example ESRP signing' + env: + ESRP_TOOL: $(ESRP_TOOL) + ESRP_AUTH: $(ESRP_AUTH) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - script: | - echo "TODO" > $(Build.ArtifactStagingDirectory)/_final/placeholder.txt + mkdir $(Build.ArtifactStagingDirectory)\_final + xcopy /s /y $(Build.ArtifactStagingDirectory)\app $(Build.ArtifactStagingDirectory)\_final + displayName: 'Dummy collect artifacts' # - # macOS build job (universal) + # macOS build jobs # - - job: macos_universal - displayName: 'macOS (x64 + ARM64)' - pool: - name: 'Azure Pipelines' - image: macOS-latest - os: macos - variables: - tag_name: $[stageDependencies.prereqs.prebuild.outputs['info.tag_name']] - tag_sha: $[stageDependencies.prereqs.prebuild.outputs['info.tag_sha']] - git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] - templateContext: - outputs: - - output: pipelineArtifact - targetPath: '$(Build.ArtifactStagingDirectory)/_final' - artifactName: 'macos_universal' - steps: - - checkout: self - # TODO: add tasks to set up build environment - # TODO: add tasks to build Git and installers - - script: | - echo "Hello, Mac!" - displayName: 'Dummy build' - # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - - script: | - echo "TODO" > $(Build.ArtifactStagingDirectory)/_final/placeholder.txt + - ${{ each dim in parameters.macos_matrix }}: + - job: ${{ dim.id }} + displayName: ${{ dim.jobName }} + pool: + name: ${{ dim.pool }} + image: ${{ dim.image }} + os: ${{ dim.os }} + variables: + tag_name: $[stageDependencies.prereqs.prebuild.outputs['info.tag_name']] + tag_sha: $[stageDependencies.prereqs.prebuild.outputs['info.tag_sha']] + git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] + templateContext: + outputs: + - output: pipelineArtifact + targetPath: '$(Build.ArtifactStagingDirectory)/_final' + artifactName: '${{ dim.id }}' + steps: + - checkout: self + # TODO: add tasks to set up build environment + # TODO: add tasks to build Git and installers + - script: | + echo "Hello, Mac!" + mkdir -p $(Build.ArtifactStagingDirectory)/app + cp /bin/echo $(Build.ArtifactStagingDirectory)/app/example + displayName: 'Dummy build' + - ${{ if eq(parameters.esrp, true) }}: + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Example sign binaries' + folderPath: '$(Build.ArtifactStagingDirectory)/app' + pattern: '**/*' + useArchive: true # Must be true when macOS signing + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppDeveloperSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "Hardening": "Enable" + } + } + ] + # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final + - script: | + mkdir -p $(Build.ArtifactStagingDirectory)/_final + cp -R $(Build.ArtifactStagingDirectory)/app/* $(Build.ArtifactStagingDirectory)/_final/ + displayName: 'Dummy collect artifacts' # # Linux build jobs @@ -176,6 +281,7 @@ extends: name: ${{ dim.pool }} image: ${{ dim.image }} os: ${{ dim.os }} + hostArchitecture: ${{ dim.poolArch }} variables: tag_name: $[stageDependencies.prereqs.prebuild.outputs['info.tag_name']] tag_sha: $[stageDependencies.prereqs.prebuild.outputs['info.tag_sha']] @@ -194,16 +300,127 @@ extends: - script: | echo $(cc_arch) echo $(deb_arch) + mkdir -p $(Build.ArtifactStagingDirectory)/app + debroot=$(Build.ArtifactStagingDirectory)/pkgroot + mkdir -p $debroot/DEBIAN + cat > $debroot/DEBIAN/control < $(Build.ArtifactStagingDirectory)/_final/placeholder.txt + mkdir -p $(Build.ArtifactStagingDirectory)/_final + cp -R $(Build.ArtifactStagingDirectory)/app/* $(Build.ArtifactStagingDirectory)/_final/ + displayName: 'Dummy collect artifacts' - stage: release displayName: 'Release' dependsOn: [prereqs, build] jobs: + # + # Windows validation jobs + # + - ${{ each dim in parameters.windows_matrix }}: + - job: validate_${{ dim.id }} + displayName: 'Validate ${{ dim.jobName }}' + pool: + name: ${{ dim.pool }} + image: ${{ dim.image }} + os: ${{ dim.os }} + hostArchitecture: ${{ dim.poolArch }} + templateContext: + inputs: + - input: pipelineArtifact + artifactName: '${{ dim.id }}' + targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} + steps: + # TODO: add artifact validation steps + - script: | + dir $(Pipeline.Workspace)\assets\${{ dim.id }} + displayName: 'Validate artifacts' + + # + # macOS validation jobs + # + - ${{ each dim in parameters.macos_matrix }}: + - job: validate_${{ dim.id }} + displayName: 'Validate ${{ dim.jobName }}' + pool: + name: ${{ dim.pool }} + image: ${{ dim.image }} + os: ${{ dim.os }} + templateContext: + inputs: + - input: pipelineArtifact + artifactName: '${{ dim.id }}' + targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} + steps: + # TODO: add artifact validation steps + - script: | + ls $(Pipeline.Workspace)/assets/${{ dim.id }} + displayName: 'Validate artifacts' + + # + # Linux validation jobs + # + - ${{ each dim in parameters.linux_matrix }}: + - job: validate_${{ dim.id }} + displayName: 'Validate ${{ dim.jobName }}' + pool: + name: ${{ dim.pool }} + image: ${{ dim.image }} + os: ${{ dim.os }} + hostArchitecture: ${{ dim.poolArch }} + templateContext: + inputs: + - input: pipelineArtifact + artifactName: '${{ dim.id }}' + targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} + steps: + # TODO: add artifact validation steps + - script: | + ls $(Pipeline.Workspace)/assets/${{ dim.id }} + displayName: 'Validate artifacts' + + # + # GitHub release publishing + # - job: github + dependsOn: + - ${{ each dim in parameters.windows_matrix }}: + - validate_${{ dim.id }} + - ${{ each dim in parameters.macos_matrix }}: + - validate_${{ dim.id }} + - ${{ each dim in parameters.linux_matrix }}: + - validate_${{ dim.id }} displayName: 'Publish GitHub release' condition: and(succeeded(), eq('${{ parameters.github }}', true)) pool: @@ -218,21 +435,18 @@ extends: type: releaseJob isProduction: true inputs: - - input: pipelineArtifact - artifactName: 'windows_x64' - targetPath: $(Pipeline.Workspace)/assets/windows_x64 - - input: pipelineArtifact - artifactName: 'windows_arm64' - targetPath: $(Pipeline.Workspace)/assets/windows_arm64 - - input: pipelineArtifact - artifactName: 'macos_universal' - targetPath: $(Pipeline.Workspace)/assets/macos_universal - - input: pipelineArtifact - artifactName: 'linux_x64' - targetPath: $(Pipeline.Workspace)/assets/linux_x64 - - input: pipelineArtifact - artifactName: 'linux_arm64' - targetPath: $(Pipeline.Workspace)/assets/linux_arm64 + - ${{ each dim in parameters.windows_matrix }}: + - input: pipelineArtifact + artifactName: '${{ dim.id }}' + targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} + - ${{ each dim in parameters.macos_matrix }}: + - input: pipelineArtifact + artifactName: '${{ dim.id }}' + targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} + - ${{ each dim in parameters.linux_matrix }}: + - input: pipelineArtifact + artifactName: '${{ dim.id }}' + targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} steps: - task: GitHubRelease@1 displayName: 'Create Draft GitHub Release' diff --git a/.azure-pipelines/scripts/resolve-version.sh b/.azure-pipelines/scripts/resolve-version.sh new file mode 100755 index 00000000000000..6169ae767bae1f --- /dev/null +++ b/.azure-pipelines/scripts/resolve-version.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Resolve version and tag information from the current HEAD commit. +# Validates that HEAD is an annotated version tag matching GIT-VERSION-GEN. +# +# Sets the following ADO output variables (via ##vso): +# git_version - Version string without "v" prefix (e.g., 2.53.0.vfs.0.0) +# tag_name - Full tag name (e.g., v2.53.0.vfs.0.0) +# tag_sha - Commit SHA of HEAD +# +# Also updates the build number to include the tag name. +# +set -euo pipefail + +echo "HEAD: $(git rev-parse HEAD)" + +# Determine the tag pointing at HEAD +tag_name=$(git describe --exact-match --match "v[0-9]*vfs*" HEAD 2>/dev/null) || { + echo "##vso[task.logissue type=error]HEAD is not tagged with a version tag" + exit 1 +} + +# Verify the tag is annotated (not lightweight) +tag_type=$(git cat-file -t "refs/tags/$tag_name") +if [ "$tag_type" != "tag" ]; then + echo "##vso[task.logissue type=error]Tag $tag_name is not annotated (type: $tag_type)" + exit 1 +fi + +tag_sha=$(git rev-parse HEAD) +git_version="${tag_name#v}" + +# Verify the version matches GIT-VERSION-GEN +make GIT-VERSION-FILE +expected_version="${git_version//-rc/.rc}" +actual_version=$(sed -n 's/^GIT_VERSION *= *//p' < GIT-VERSION-FILE) +if [ "$expected_version" != "$actual_version" ]; then + echo "##vso[task.logissue type=error]GIT-VERSION-FILE ($actual_version) does not match tag $tag_name ($expected_version)" + exit 1 +fi + +echo "Git version: $git_version" +echo "Tag name: $tag_name" +echo "Tag SHA: $tag_sha" +echo "##vso[task.setvariable variable=git_version;isOutput=true;isReadOnly=true]$git_version" +echo "##vso[task.setvariable variable=tag_name;isOutput=true;isReadOnly=true]$tag_name" +echo "##vso[task.setvariable variable=tag_sha;isOutput=true;isReadOnly=true]$tag_sha" +echo "##vso[build.updatebuildnumber]${tag_name} (${BUILD_BUILDNUMBER:-unknown})" diff --git a/.azure-pipelines/scripts/windows/setup-git-bash.cmd b/.azure-pipelines/scripts/windows/setup-git-bash.cmd new file mode 100644 index 00000000000000..b3ef5518cfc85d --- /dev/null +++ b/.azure-pipelines/scripts/windows/setup-git-bash.cmd @@ -0,0 +1,13 @@ +@echo off +setlocal enabledelayedexpansion +set "agentgit=%AGENT_HOMEDIRECTORY%\externals\git" +set "gitcopy=%AGENT_TEMPDIRECTORY%\git" +echo Copying !agentgit! to !gitcopy!... +xcopy /E /I /Q "!agentgit!" "!gitcopy!" +if not exist "!gitcopy!\usr\bin\sh.exe" ( + echo ##vso[task.logissue type=error]Could not find sh.exe at !gitcopy!\usr\bin\sh.exe + exit /b 1 +) +echo Copying !gitcopy!\usr\bin\sh.exe to !gitcopy!\usr\bin\bash.exe... +copy /Y "!gitcopy!\usr\bin\sh.exe" "!gitcopy!\usr\bin\bash.exe" +echo ##vso[task.prependpath]!gitcopy!\usr\bin