From 4989d33f39d51ac200b81a05a00127b5d74651a4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 11 May 2026 17:39:17 +0200 Subject: [PATCH 01/14] azure-pipelines: make the Windows mingw-w64-git build honour git_version When building on Windows, the version comes from an annotated tag at HEAD, falling back to a `git describe + timestamp` string when no such tag exists. Real release runs already have the tag in place via the tag-push trigger; manual debug runs do not. Create the `v$(git_version)` tag ourselves so the artifact name matches the pipeline-resolved version. The tag alone is not enough: microsoft/git's GIT-VERSION-GEN refuses any version not based on `DEF_VER`, so debug tags using a different vfs prefix would still abort the build. GIT-VERSION-GEN also reads a `version` file at the source-tree root in preference to running `git describe`, and the prefix check only fires on the describe path, so planting that file side-steps both. The file has to live in the directory the build compiles from. Make that directory a worktree of the agent's checkout so refs and objects are shared without duplicating the source, and override `remote.origin.url` per-worktree (via `extensions.worktreeConfig`) so the build's unconditional `git fetch` stays on local disk rather than hitting the Azure-supplied remote. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 107 +++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index b1da29c2494041..ef2a6f34710e96 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -262,6 +262,10 @@ extends: # reason (see actions/runner ProcessInvoker.cs). exec &2 + exit 1 + } + else + git tag -a -m "v$(git_version)" "v$(git_version)" HEAD + fi + + # Pre-create the worktree that makepkg-mingw will + # actually compile, instead of letting please.sh's + # `clone --reference $bare https://github.com/git-for-windows/git` + # do it. The PKGBUILD's `build()` -> `make` invokes + # microsoft/git's GIT-VERSION-GEN, which `git + # describe`s in $srcdir/git and rejects any version + # whose `${VN%%.vfs.*}` does not match the hard-coded + # `${DEF_VER%%.vfs.*}` (`v2.53.0`). For real release + # tags the two match, but for any debug tag (in + # particular the v9.99.99.vfs.0.0 the TO-DROP commit + # produces) the build aborts with "Found version + # v9.99.99.vfs.0.0, which is not based on + # v2.53.0.vfs.0.0". GIT-VERSION-GEN reads a `version` + # file at the source-tree root in preference to + # running git describe, and the validation only + # fires on the describe path; so plant that file + # ahead of the build. + # + # please.sh would clone /usr/src/MINGW-packages + # itself if missing; we do the same clone first so + # the package directory exists and please.sh skips + # its own clone (line 838-840 of please.sh). + test -d /usr/src/MINGW-packages || + git clone --depth 1 --single-branch -b main \ + https://github.com/git-for-windows/MINGW-packages \ + /usr/src/MINGW-packages + + # mingw-w64-git/src/git is what makepkg's extract_git + # `git fetch`s and `git checkout --force --no-track + # -B makepkg `s into. Make it a worktree of this + # agent's checkout so the v$VERSION tag we just + # created is already visible there (worktrees share + # refs and objects with the main repo) and we don't + # have to duplicate the source. + # + # Worktrees also share `.git/config` with the main + # repo, so origin would point at the Azure-supplied + # remote URL and extract_git's `git fetch` would go + # online (and abort the build if it failed). Enable + # `extensions.worktreeConfig` and override origin to + # the local checkout via `git config --worktree` so + # the fetch stays on local disk and is effectively a + # no-op. Mirror please.sh's `core.autoCRLF=false` + # under the same per-worktree namespace so the + # checksums calc_checksum_git() computes against + # this tree are reproducible regardless of the main + # repo's autocrlf setting. + mkdir -p /usr/src/MINGW-packages/mingw-w64-git/src + test -d /usr/src/MINGW-packages/mingw-w64-git/src/git || { + git -C "$BUILD_SOURCESDIRECTORY" \ + config extensions.worktreeConfig true + git -C "$BUILD_SOURCESDIRECTORY" worktree add \ + /usr/src/MINGW-packages/mingw-w64-git/src/git HEAD + git -C /usr/src/MINGW-packages/mingw-w64-git/src/git \ + config --worktree remote.origin.url \ + "$BUILD_SOURCESDIRECTORY" + git -C /usr/src/MINGW-packages/mingw-w64-git/src/git \ + config --worktree core.autoCRLF false + } + + # The actual `version` file write that side-steps + # GIT-VERSION-GEN's validation. The file is untracked, + # so makepkg's `git checkout --force --no-track -B + # makepkg ` does not remove it. + echo "$BUILD_VERSION" \ + >/usr/src/MINGW-packages/mingw-w64-git/src/git/version + sh -x /usr/src/build-extra/please.sh build-mingw-w64-git \ --only-"$(cpu_arch)" \ --build-src-pkg \ From ce9965654c37b876ec334f4b54deceaf8acdabad Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 11 May 2026 17:56:27 +0200 Subject: [PATCH 02/14] azure-pipelines: skip building the unused mingw-w64-git source package on Windows `--build-src-pkg` makes please.sh produce a source tarball alongside the binary mingw-w64-git packages. Nothing downstream consumes it: the tarball is not bundled into the installer, not signed, not published as an artifact, and not attached to the GitHub release. Drop the flag. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index ef2a6f34710e96..b6a3d1be256c56 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -384,7 +384,6 @@ extends: sh -x /usr/src/build-extra/please.sh build-mingw-w64-git \ --only-"$(cpu_arch)" \ - --build-src-pkg \ -o artifacts \ HEAD From 65b5e74729587513e274d31106977cf94826a920 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 9 May 2026 15:30:21 +0200 Subject: [PATCH 03/14] azure-pipelines: validate Windows installer in the release stage Fill in the `validate_windows_*` jobs in the release stage, still TODO-stubbed since the initial port of the build-git-installers workflow. Mirror the legacy workflow's check: silently install the .exe and assert that `git --version` matches the tag. Validation only consumes the pre-built artifact, so skip the implicit `checkout: self` to avoid cloning the entire Git history just to throw the working tree away. The macOS and Linux validate jobs are filled in by subsequent commits. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 42 ++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index b6a3d1be256c56..bafe8860490a97 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -996,16 +996,50 @@ extends: image: ${{ dim.image }} os: ${{ dim.os }} hostArchitecture: ${{ dim.poolArch }} + variables: + git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] 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' + # The validation only consumes the pre-built installer + # artifact and queries the freshly installed git.exe; + # nothing references the source tree, so skip the + # implicit `checkout: self`. + - checkout: none + # Silently install the Inno Setup installer (Git-*.exe), + # then assert that `git --version` reports the version + # we built. Mirrors the validate-installers job in the + # legacy build-git-installers GitHub Actions workflow. + - powershell: | + $exe = Get-ChildItem -Path "$(Pipeline.Workspace)\assets\${{ dim.id }}\Git-*.exe" | + Where-Object { $_.Name -notlike 'PortableGit-*' } | + Select-Object -First 1 -ExpandProperty FullName + if (-not $exe) { + Write-Error "No Git-*.exe installer found in assets\${{ dim.id }}" + exit 1 + } + Write-Host "Installing $exe" + $p = Start-Process -Wait -PassThru -FilePath "$exe" ` + -ArgumentList "/SILENT","/VERYSILENT","/NORESTART","/SUPPRESSMSGBOXES","/ALLOWDOWNGRADE=1" + if ($p.ExitCode -ne 0) { + Write-Error "Installer exited with code $($p.ExitCode)" + exit $p.ExitCode + } + displayName: 'Install Git' + - powershell: | + $raw = & "$env:PROGRAMFILES\Git\cmd\git.exe" --version + $actual = ($raw -replace '^git version ', '').Trim() + $expect = ('$(git_version)' -replace '-rc', '.rc').Trim() + Write-Host "Expected: $expect" + Write-Host "Actual: $actual" + if ($actual -ne $expect) { + Write-Error "Version mismatch: expected '$expect', got '$actual'" + exit 1 + } + displayName: 'Validate installed version' # # macOS validation jobs From 4bfceb00cb3c87df40841d41729811d775ad4908 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 10 May 2026 09:54:38 +0200 Subject: [PATCH 04/14] azure-pipelines: validate macOS installer in the release stage Continue the validation port started in the previous commit. Mirror the legacy build-git-installers workflow on macOS: install the .pkg and assert that `git --version` matches the tag. Two extras vs. the Windows port: on Apple Silicon agents, Homebrew's `/opt/homebrew/bin/git` appears earlier on PATH than the pkg-installed `/usr/local/bin/git`, so `brew uninstall git` before the install is needed for the subsequent `git --version` to actually exercise our artifact; and check that `git version --build-options` reports `cpu: $(uname -m)` so the universal binary runs natively rather than under Rosetta. The Linux validate jobs are filled in by the next commit. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 43 ++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index bafe8860490a97..6d4962f29455db 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -1051,16 +1051,51 @@ extends: name: ${{ dim.pool }} image: ${{ dim.image }} os: ${{ dim.os }} + variables: + git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] 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' + # The validation only consumes the pre-built installer + # artifact and queries the freshly installed git; + # nothing references the source tree, so skip the + # implicit `checkout: self`. + - checkout: none + # On Apple Silicon, Homebrew's git in /opt/homebrew/bin + # shadows /usr/local/bin/git that our pkg installs into, + # so uninstall it first. Then install the universal pkg + # and assert version + universal-binary cpu architecture. + - bash: | + set -e + if [ "$(uname -m)" = arm64 ] && command -v brew >/dev/null; then + brew uninstall git || true + fi + pkg=$(find "$(Pipeline.Workspace)/assets/${{ dim.id }}" \ + -name 'git-*-universal.pkg' -type f | head -1) + if [ -z "$pkg" ]; then + echo "No git-*-universal.pkg found in assets/${{ dim.id }}" >&2 + exit 1 + fi + echo "Installing $pkg" + sudo installer -pkg "$pkg" -target / + displayName: 'Install Git' + - bash: | + set -e + actual=$(git --version | sed 's/^git version //') + expect=$(echo "$(git_version)" | sed 's/-rc/.rc/g') + echo "Expected: $expect" + echo "Actual: $actual" + test "$actual" = "$expect" + displayName: 'Validate installed version' + - bash: | + set -ex + git version --build-options >actual + cat actual + grep "cpu: $(uname -m)" actual + displayName: 'Validate universal binary CPU architecture' # # Linux validation jobs From dd8ae585946caa78b003c94d99b4e37f9314c0f0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 10 May 2026 09:58:34 +0200 Subject: [PATCH 05/14] azure-pipelines: validate Linux .deb in the release stage Finish the validation port started two commits ago. Mirror the legacy build-git-installers workflow on Linux: install the .deb and assert that `git --version` matches the tag. Use `apt-get install -y` rather than the legacy workflow's `apt install`, which would otherwise pause for the y/N prompt; run `apt-get update` first so dependency resolution sees the agent's current package index. With this commit all three OS validate jobs run real artifact checks before the GitHub draft release publishing job downstream of them fires, completing the build-git-installers workflow migration. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 6d4962f29455db..d002254608c223 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -1108,16 +1108,38 @@ extends: image: ${{ dim.image }} os: ${{ dim.os }} hostArchitecture: ${{ dim.poolArch }} + variables: + git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] 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' + # The validation only consumes the pre-built .deb and + # queries the freshly installed git; nothing references + # the source tree, so skip the implicit `checkout: self`. + - checkout: none + - bash: | + set -e + deb=$(find "$(Pipeline.Workspace)/assets/${{ dim.id }}" \ + -name 'microsoft-git_*.deb' -type f | head -1) + if [ -z "$deb" ]; then + echo "No microsoft-git_*.deb found in assets/${{ dim.id }}" >&2 + exit 1 + fi + echo "Installing $deb" + sudo apt-get update + sudo apt-get install -y "$deb" + displayName: 'Install Git' + - bash: | + set -e + actual=$(git --version | sed 's/^git version //') + expect=$(echo "$(git_version)" | sed 's/-rc/.rc/g') + echo "Expected: $expect" + echo "Actual: $actual" + test "$actual" = "$expect" + displayName: 'Validate installed version' # # GitHub release publishing From 782e75f578c8a97dd8ec48dc814e45155239c77a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 May 2026 17:32:57 +0200 Subject: [PATCH 06/14] azure-pipelines: enable on tag push, default ESRP and GitHub release on With build, signing, notarization, validation, and draft-release publishing all in place, the Azure Pipeline is ready to take over from the GitHub Actions build-git-installers workflow. Switch `trigger: none` to a tag-only trigger matching the same `v[0-9]*vfs*` pattern the GitHub workflow used (and that resolve-version.sh validates against), and explicitly exclude all branches so the pipeline does not fire on every topic-branch push. Flip the `esrp` and `github` parameter defaults from false to true. The GitHub release job still uses `isDraft: true`, so a maintainer inspects and publishes the release manually; manual runs in the ADO UI can still uncheck either box for a dry run. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index d002254608c223..e07443d285d763 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -1,5 +1,11 @@ name: $(Date:yyyyMMdd)$(Rev:.r) -trigger: none +trigger: + branches: + exclude: + - '*' + tags: + include: + - v[0-9]*vfs* pr: none resources: @@ -12,11 +18,11 @@ resources: parameters: - name: 'esrp' type: boolean - default: false # TODO: change default to true after testing + default: true displayName: 'Enable ESRP code signing' - name: 'github' type: boolean - default: false # TODO: change default to true after testing + default: true displayName: 'Enable GitHub release publishing' # From a770fe47633a8637120b38400c8cd6a8a678c604 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 May 2026 17:37:50 +0200 Subject: [PATCH 07/14] build-git-installers: demote to workflow_dispatch only The Azure Pipeline at .azure-pipelines/release.yml now builds and signs the official microsoft/git installers on every `v[0-9]*vfs*` tag push. Two release pipelines firing on the same tag would race each other uploading assets to the same draft GitHub release, so this workflow has to step out of the way. We also no longer have access to a code-signing certificate this workflow could use, so even on its own it would attach unsigned (or improperly signed) installers, which we must not ship. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .github/workflows/build-git-installers.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-git-installers.yml b/.github/workflows/build-git-installers.yml index 5985dc6968df9a..cf4c66c1122d0a 100644 --- a/.github/workflows/build-git-installers.yml +++ b/.github/workflows/build-git-installers.yml @@ -1,9 +1,17 @@ name: build-git-installers +# This workflow used to run automatically on every `v*vfs*` tag push to +# build and publish the official microsoft/git installers. The signed +# release builds have moved to the Azure Pipeline in +# `.azure-pipelines/release.yml`, which is what now runs on tag push. +# +# We no longer have access to a permissible code signing certificate +# from this workflow, so the artifacts it produces would not be properly +# code-signed and must not be published as releases. Keep the workflow +# around behind a `workflow_dispatch:` trigger only, so it can still be +# invoked manually for debugging or comparison purposes. on: - push: - tags: - - 'v[0-9]*vfs*' # matches "vvfs" + workflow_dispatch: permissions: id-token: write # required for Azure login via OIDC From 3e69e110aeeeb1d36dfc69b09eabdde110c6c6d6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 May 2026 09:23:58 +0200 Subject: [PATCH 08/14] azure-pipelines: parallelise the Windows SDK sparse checkout Initialising the Git for Windows SDK sparse-checks out thousands of small files (the MinGW toolchain plus the MSYS2 userland). The work is firmly I/O-bound, not CPU-bound, so the default of one checkout worker leaves most of the agent's I/O subsystem idle. Raise the worker count to 56, matching what git-for-windows/git-sdk-64's own CI uses for the same operation; deliberately well above the agent's CPU count for the I/O-bound reason above. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index e07443d285d763..1e8052f6631abb 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -197,6 +197,14 @@ extends: arguments: '$(sdk_repo) $(mingwprefix) "$(Agent.TempDirectory)\gitsdk"' env: BOOTSTRAP_DIR: '$(Build.SourcesDirectory)' + # please.sh's `create-sdk-artifact` step does the + # final sparse-checkout of the build-installers + # SDK subset, which is I/O-bound (lots of small + # writes), not CPU-bound. The default + # checkout.workers=1 leaves the agent's I/O + # subsystem mostly idle; bumping it well beyond + # the core count gives a substantial speedup. + GIT_CONFIG_PARAMETERS: "'checkout.workers=56'" - task: Bash@3 displayName: 'Clone build-extra into SDK' inputs: From a63284d885a0474f1dace0a0056dd59d974987b3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 May 2026 09:25:02 +0200 Subject: [PATCH 09/14] azure-pipelines: parallelise the Windows mingw-w64-git build The mingw-w64-git build invokes `make` without an explicit `-j`, leaving the bulk of the per-file work (perl scripts, doc tree, contrib targets, per-.c compilation) serial on a single core. As with the SDK sparse checkout in the previous commit, most of the wall-clock time is spent shuffling small files in and out of the SDK's pacman cache and the build tree rather than in the compiler itself; the work is predominantly I/O-bound, so the right factor is well above the agent's core count rather than `$(nproc)`. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 1e8052f6631abb..8dc963dd0d52f1 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -254,8 +254,16 @@ extends: bash "$apply" "$patches/git-sdk" "/$(mingwprefix)" - task: Bash@3 displayName: 'Build mingw-w64-git package' - ${{ if eq(parameters.esrp, true) }}: - env: + env: + # The mingw-w64-git build is heavy on parallel work + # that the underlying compile (and especially the + # contrib + doc + i18n + perl-script generation + # stages) can soak up far more aggressively than the + # core count would suggest, since most of it is I/O + # against the SDK's pacman cache and the make rules + # have very few real serialisation points. + MAKEFLAGS: -j15 + ${{ if eq(parameters.esrp, true) }}: ESRP_TOOL: $(ESRP_TOOL) ESRP_AUTH: $(ESRP_AUTH) SYSTEM_ACCESSTOKEN: $(System.AccessToken) From fa7ee7b0bb4b051ec32ecd751c044d599521e896 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 May 2026 15:33:38 +0200 Subject: [PATCH 10/14] azure-pipelines: wait for the dpkg lock when installing Linux deps The 1ES Ubuntu agents come up with `unattended-upgrades` already running, which holds `/var/lib/dpkg/lock-frontend` for the first few minutes after boot. The Linux build and validate jobs' `apt-get` invocations therefore intermittently fail with E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process (unattended-upgr) on what is genuinely just a timing race. Tell apt to poll for the lock with a generous timeout rather than failing immediately, turning the race into a backoff and avoiding wrapping each apt invocation in an ad-hoc retry loop. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 8dc963dd0d52f1..e25b6016c1c11a 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -918,8 +918,16 @@ extends: targetType: inline script: | set -euo pipefail - sudo apt-get update -q - sudo apt-get install -y -q --no-install-recommends \ + # The 1ES Ubuntu agents come up with `unattended- + # upgrades` running, which holds the dpkg + # frontend lock for the first few minutes after + # boot. `apt-get` releases earlier than 2.1 + # would have failed immediately with + # E: Could not get lock /var/lib/dpkg/lock-frontend + # Pass `DPkg::Lock::Timeout=600` so apt waits up + # to 10 minutes for the lock instead. + sudo apt-get -o DPkg::Lock::Timeout=600 update -q + sudo apt-get -o DPkg::Lock::Timeout=600 install -y -q --no-install-recommends \ build-essential \ tcl tk gettext asciidoc xmlto \ libcurl4-gnutls-dev libpcre2-dev zlib1g-dev libexpat-dev \ @@ -1151,8 +1159,11 @@ extends: exit 1 fi echo "Installing $deb" - sudo apt-get update - sudo apt-get install -y "$deb" + # Wait up to 10 minutes for unattended-upgrades to + # release the dpkg lock; see comment on the build + # job's 'Install build dependencies' step. + sudo apt-get -o DPkg::Lock::Timeout=600 update + sudo apt-get -o DPkg::Lock::Timeout=600 install -y "$deb" displayName: 'Install Git' - bash: | set -e From be7c1b7001a052b848812431bb771407fd32c6e4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 May 2026 18:02:59 +0200 Subject: [PATCH 11/14] azure-pipelines: validate Windows installer against ProgramW6432 The Windows validate job looked up the installed `git.exe` under `%PROGRAMFILES%\Git\cmd\git.exe`. On the ARM64 1ES agent that resolved to `C:\Program Files (x86)\Git\cmd\git.exe`, where the installer never puts anything, and the step failed. `%PROGRAMFILES%` is per-process: Windows remaps it to `C:\Program Files (x86)` for any 32-bit-emulated process, and the ARM64 hosted agent's classic PowerShell evidently runs under emulation. `%ProgramW6432%` is defined on every 64-bit Windows and always points at the native `C:\Program Files`, regardless of the calling process's bitness. The Inno Setup installer installs into `{pf}` -> `C:\Program Files\Git`, so the swap works on both x64 and ARM64. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index e25b6016c1c11a..1ab69adddff3bd 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -1060,7 +1060,7 @@ extends: } displayName: 'Install Git' - powershell: | - $raw = & "$env:PROGRAMFILES\Git\cmd\git.exe" --version + $raw = & "$env:ProgramW6432\Git\cmd\git.exe" --version $actual = ($raw -replace '^git version ', '').Trim() $expect = ('$(git_version)' -replace '-rc', '.rc').Trim() Write-Host "Expected: $expect" From 727194ca6147a747d2b4b36d0148c873fc0982e7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 May 2026 19:57:43 +0200 Subject: [PATCH 12/14] azure-pipelines: fold Windows validation into the build job 1ES pipeline templates add substantial fixed overhead per job (image acquisition, SDL gating, artifact-staging plumbing, job tear-down). For the Windows validate job that runs only a few seconds of PowerShell against the installer it just built, that overhead dwarfs the work itself. Unlike GitHub Actions, Azure Pipelines provides little benefit from re-running an individual failed job in isolation, so the per-OS separation between build and validate buys nothing here. Fold the install + version-check steps into the corresponding build job and drop the separate validate job. The macOS and Linux validate jobs are folded in subsequent commits. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 90 +++++++++++++----------------------- 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 1ab69adddff3bd..6b4b0965e936d5 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -508,6 +508,38 @@ extends: artifacts/PortableGit-*.exe \ artifacts/sha-256.txt \ "$(Build.ArtifactStagingDirectory)/_final/" + # Validate the freshly built installer in-place: silently + # install Git-*.exe and assert that `git --version` reports + # the version we resolved at the prereqs stage. Folded into + # the build job so it runs on the same agent without the + # 1ES job-startup overhead a separate validate job carries. + - powershell: | + $exe = Get-ChildItem -Path "$(Build.ArtifactStagingDirectory)\_final\Git-*.exe" | + Where-Object { $_.Name -notlike 'PortableGit-*' } | + Select-Object -First 1 -ExpandProperty FullName + if (-not $exe) { + Write-Error "No Git-*.exe installer found in _final" + exit 1 + } + Write-Host "Installing $exe" + $p = Start-Process -Wait -PassThru -FilePath "$exe" ` + -ArgumentList "/SILENT","/VERYSILENT","/NORESTART","/SUPPRESSMSGBOXES","/ALLOWDOWNGRADE=1" + if ($p.ExitCode -ne 0) { + Write-Error "Installer exited with code $($p.ExitCode)" + exit $p.ExitCode + } + displayName: 'Install Git' + - powershell: | + $raw = & "$env:ProgramW6432\Git\cmd\git.exe" --version + $actual = ($raw -replace '^git version ', '').Trim() + $expect = ('$(git_version)' -replace '-rc', '.rc').Trim() + Write-Host "Expected: $expect" + Write-Host "Actual: $actual" + if ($actual -ne $expect) { + Write-Error "Version mismatch: expected '$expect', got '$actual'" + exit 1 + } + displayName: 'Validate installed version' # # macOS build jobs @@ -1015,62 +1047,6 @@ extends: 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 }} - variables: - git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] - templateContext: - inputs: - - input: pipelineArtifact - artifactName: '${{ dim.id }}' - targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} - steps: - # The validation only consumes the pre-built installer - # artifact and queries the freshly installed git.exe; - # nothing references the source tree, so skip the - # implicit `checkout: self`. - - checkout: none - # Silently install the Inno Setup installer (Git-*.exe), - # then assert that `git --version` reports the version - # we built. Mirrors the validate-installers job in the - # legacy build-git-installers GitHub Actions workflow. - - powershell: | - $exe = Get-ChildItem -Path "$(Pipeline.Workspace)\assets\${{ dim.id }}\Git-*.exe" | - Where-Object { $_.Name -notlike 'PortableGit-*' } | - Select-Object -First 1 -ExpandProperty FullName - if (-not $exe) { - Write-Error "No Git-*.exe installer found in assets\${{ dim.id }}" - exit 1 - } - Write-Host "Installing $exe" - $p = Start-Process -Wait -PassThru -FilePath "$exe" ` - -ArgumentList "/SILENT","/VERYSILENT","/NORESTART","/SUPPRESSMSGBOXES","/ALLOWDOWNGRADE=1" - if ($p.ExitCode -ne 0) { - Write-Error "Installer exited with code $($p.ExitCode)" - exit $p.ExitCode - } - displayName: 'Install Git' - - powershell: | - $raw = & "$env:ProgramW6432\Git\cmd\git.exe" --version - $actual = ($raw -replace '^git version ', '').Trim() - $expect = ('$(git_version)' -replace '-rc', '.rc').Trim() - Write-Host "Expected: $expect" - Write-Host "Actual: $actual" - if ($actual -ne $expect) { - Write-Error "Version mismatch: expected '$expect', got '$actual'" - exit 1 - } - displayName: 'Validate installed version' - # # macOS validation jobs # @@ -1179,8 +1155,6 @@ extends: # - 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 }}: From 6fac9977f6f8d854967f04551426247a46fc972a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 May 2026 19:59:58 +0200 Subject: [PATCH 13/14] azure-pipelines: fold macOS validation into the build job For the same reason as the Windows fold in the previous commit, the per-job 1ES overhead is not worth paying for the few seconds it takes to install the .pkg, check the reported version, and confirm the universal binary's CPU. Fold the validation steps into the macOS build job and drop the separate validate job. The Linux validate jobs are folded in the next commit. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 92 +++++++++++++----------------------- 1 file changed, 34 insertions(+), 58 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 6b4b0965e936d5..189d258cf3e8d2 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -911,6 +911,40 @@ extends: mv .github/macos-installer/git-*-universal.dmg \ "$pkg" \ "$(Build.ArtifactStagingDirectory)/_final/" + # Validate the freshly built pkg in-place: install it, + # assert `git --version`, and confirm the universal binary + # actually runs natively as the host architecture. Folded + # into the build job so it runs on the same agent without + # the 1ES job-startup overhead a separate validate job + # carries. + - bash: | + set -e + if [ "$(uname -m)" = arm64 ] && command -v brew >/dev/null; then + brew uninstall git || true + fi + pkg=$(find "$(Build.ArtifactStagingDirectory)/_final" \ + -name 'git-*-universal.pkg' -type f | head -1) + if [ -z "$pkg" ]; then + echo "No git-*-universal.pkg found in _final" >&2 + exit 1 + fi + echo "Installing $pkg" + sudo installer -pkg "$pkg" -target / + displayName: 'Install Git' + - bash: | + set -e + actual=$(git --version | sed 's/^git version //') + expect=$(echo "$(git_version)" | sed 's/-rc/.rc/g') + echo "Expected: $expect" + echo "Actual: $actual" + test "$actual" = "$expect" + displayName: 'Validate installed version' + - bash: | + set -ex + git version --build-options >actual + cat actual + grep "cpu: $(uname -m)" actual + displayName: 'Validate universal binary CPU architecture' # # Linux build jobs @@ -1047,62 +1081,6 @@ extends: displayName: 'Release' dependsOn: [prereqs, build] jobs: - # - # 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 }} - variables: - git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] - templateContext: - inputs: - - input: pipelineArtifact - artifactName: '${{ dim.id }}' - targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} - steps: - # The validation only consumes the pre-built installer - # artifact and queries the freshly installed git; - # nothing references the source tree, so skip the - # implicit `checkout: self`. - - checkout: none - # On Apple Silicon, Homebrew's git in /opt/homebrew/bin - # shadows /usr/local/bin/git that our pkg installs into, - # so uninstall it first. Then install the universal pkg - # and assert version + universal-binary cpu architecture. - - bash: | - set -e - if [ "$(uname -m)" = arm64 ] && command -v brew >/dev/null; then - brew uninstall git || true - fi - pkg=$(find "$(Pipeline.Workspace)/assets/${{ dim.id }}" \ - -name 'git-*-universal.pkg' -type f | head -1) - if [ -z "$pkg" ]; then - echo "No git-*-universal.pkg found in assets/${{ dim.id }}" >&2 - exit 1 - fi - echo "Installing $pkg" - sudo installer -pkg "$pkg" -target / - displayName: 'Install Git' - - bash: | - set -e - actual=$(git --version | sed 's/^git version //') - expect=$(echo "$(git_version)" | sed 's/-rc/.rc/g') - echo "Expected: $expect" - echo "Actual: $actual" - test "$actual" = "$expect" - displayName: 'Validate installed version' - - bash: | - set -ex - git version --build-options >actual - cat actual - grep "cpu: $(uname -m)" actual - displayName: 'Validate universal binary CPU architecture' - # # Linux validation jobs # @@ -1155,8 +1133,6 @@ extends: # - job: github dependsOn: - - ${{ each dim in parameters.macos_matrix }}: - - validate_${{ dim.id }} - ${{ each dim in parameters.linux_matrix }}: - validate_${{ dim.id }} displayName: 'Publish GitHub release' From 967ec9627fced582936c14974fb3106fac1cf5f7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 May 2026 20:05:10 +0200 Subject: [PATCH 14/14] azure-pipelines: fold Linux validation into the build job For the same reason as the Windows and macOS folds in the previous two commits, the per-job 1ES overhead is not worth paying for the few seconds it takes to install the .deb and check the reported version. Fold the validation steps into the Linux build job and drop the separate validate job; the fold reuses the dpkg-lock work-around the build-deps step already needs. The release stage's only remaining job after this is `github`, whose ordering is already provided by the release stage's own dependsOn entry on the build stage. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 47 +++++++++--------------------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 189d258cf3e8d2..d867a322cea510 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -1076,46 +1076,22 @@ extends: mkdir -p "$(Build.ArtifactStagingDirectory)/_final" mv "$(Build.ArtifactStagingDirectory)/app/microsoft-git_$(git_version)_$(deb_arch).deb" \ "$(Build.ArtifactStagingDirectory)/_final/" - - - stage: release - displayName: 'Release' - dependsOn: [prereqs, build] - jobs: - # - # 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 }} - variables: - git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] - templateContext: - inputs: - - input: pipelineArtifact - artifactName: '${{ dim.id }}' - targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} - steps: - # The validation only consumes the pre-built .deb and - # queries the freshly installed git; nothing references - # the source tree, so skip the implicit `checkout: self`. - - checkout: none + # Validate the freshly built .deb in-place: install it + # and assert `git --version`. Folded into the build job + # so it runs on the same agent without the 1ES job- + # startup overhead a separate validate job carries. - bash: | set -e - deb=$(find "$(Pipeline.Workspace)/assets/${{ dim.id }}" \ + deb=$(find "$(Build.ArtifactStagingDirectory)/_final" \ -name 'microsoft-git_*.deb' -type f | head -1) if [ -z "$deb" ]; then - echo "No microsoft-git_*.deb found in assets/${{ dim.id }}" >&2 + echo "No microsoft-git_*.deb found in _final" >&2 exit 1 fi echo "Installing $deb" # Wait up to 10 minutes for unattended-upgrades to - # release the dpkg lock; see comment on the build - # job's 'Install build dependencies' step. + # release the dpkg lock; see comment on this job's + # 'Install build dependencies' step. sudo apt-get -o DPkg::Lock::Timeout=600 update sudo apt-get -o DPkg::Lock::Timeout=600 install -y "$deb" displayName: 'Install Git' @@ -1128,13 +1104,14 @@ extends: test "$actual" = "$expect" displayName: 'Validate installed version' + - stage: release + displayName: 'Release' + dependsOn: [prereqs, build] + jobs: # # GitHub release publishing # - job: github - dependsOn: - - ${{ each dim in parameters.linux_matrix }}: - - validate_${{ dim.id }} displayName: 'Publish GitHub release' condition: and(succeeded(), eq('${{ parameters.github }}', true)) pool: