From 39b31328c917f18fe167b94f1a56003d47203c0d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 15:37:56 +0200 Subject: [PATCH 01/16] azure-pipelines: install build dependencies for Linux build Port the build-dependency setup from the GitHub workflow's create-linux-unsigned-artifacts job (.github/workflows/build-git-installers.yml). The package list matches one for one: build-essential for the C toolchain, tcl tk for git-gui, gettext for i18n, asciidoc and xmlto for documentation, libcurl4-gnutls-dev / libpcre2-dev / zlib1g-dev / libexpat-dev for the Git build flags this pipeline will use, and curl / ca-certificates for any in-job downloads. The GitHub workflow runs all of this inside an ubuntu:20.04 / ubuntu:22.04 container, both to pin the resulting .deb's glibc ABI floor and to give apt-get a root-owned filesystem. The 1ES pool images we run on (GitClientPME-1ESHostedPool-{intel,arm64}-pc) silently ignore a job-level `container:` directive, so the build executes on the bare Ubuntu host VM as the unprivileged agent user. Run apt-get via `sudo`, and to at least pin the .deb's glibc floor to something an audit can read back, log the running Ubuntu version, kernel, and effective UID at the start of the job. Two pieces of the workflow's setup are intentionally left out: The DEBIAN_FRONTEND=noninteractive / TZ=Etc/UTC env vars exist only to keep `tzdata` from prompting interactively when it gets pulled in inside the container (see 842cfa4aaaae0 (fixup! release: build unsigned Ubuntu .deb package, 2025-02-13)); the bare 1ES image already has tzdata configured and they would have no effect. The Node.js workaround exists to satisfy GitHub Actions' Node-based shim, which Azure Pipelines does not need. Re-introducing a real container later (whether via 1ES's container option, a custom container job, or a build inside docker invoked from a step) is a separate question. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index f0e79b31926149..c08b3a4ea09199 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -295,7 +295,26 @@ extends: artifactName: '${{ dim.id }}' steps: - checkout: self - # TODO: add tasks to set up build environment + - task: Bash@3 + displayName: 'Log build environment' + inputs: + targetType: inline + script: | + lsb_release -a || true + uname -a + id + - task: Bash@3 + displayName: 'Install build dependencies' + inputs: + targetType: inline + script: | + set -euo pipefail + sudo apt-get update -q + sudo apt-get install -y -q --no-install-recommends \ + build-essential \ + tcl tk gettext asciidoc xmlto \ + libcurl4-gnutls-dev libpcre2-dev zlib1g-dev libexpat-dev \ + curl ca-certificates # TODO: add tasks to build Git and installers - script: | echo $(cc_arch) From 2a194fc3240382ed00759e1df9395c3a9a7cf24d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 16:28:32 +0200 Subject: [PATCH 02/16] azure-pipelines: build microsoft-git Debian package for Linux Replace the Linux dummy build with the real package build, ported from the create-linux-unsigned-artifacts job in .github/workflows/build-git-installers.yml. The pipeline already wires $(git_version) from the prereqs stage, so the version no longer comes from a tag_version job output. $(deb_arch) is taken straight from the matrix entry, dropping the GitHub workflow's runtime dpkg-architecture round-trip; the matrix already names amd64 and arm64 explicitly. Parallelism switches from the workflow's hard-coded -j5 (a runner-specific holdover) to -j$(nproc), which is the common default and adapts to whatever the 1ES pool gives us. The shell prologue switches from `set -ex` to `set -euo pipefail` so an unbound variable or a failed step in a pipeline aborts the job rather than silently producing a broken .deb. The make recipe and DEBIAN/control body match the workflow byte for byte: same DESTDIR layout, same Make flags (USE_LIBPCRE, USE_CURL_FOR_IMAP_SEND, NO_OPENSSL, NO_CROSS_DIRECTORY_HARDLINKS, ASCIIDOC8, ASCIIDOC_NO_ROFF, ASCIIDOC='TZ=UTC asciidoc'), same prefix and gitexecdir/libexecdir/htmldir, same install targets, and the same Depends list and Description text. The only intentional content change is the package version and architecture fields, which now come from $(git_version) and $(deb_arch). The sed 's/-rc/.rc/g' substitution carried over because Git's GIT-VERSION-GEN expects rc tags spelled with a dot. Output goes under $(Build.ArtifactStagingDirectory)/app/, where the existing ESRP signing template will pick it up via its '**/*.deb' pattern. The collect step below still uses the dummy 'cp -R app/* _final/' shape; tightening that is the next commit. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 62 ++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index c08b3a4ea09199..31d1f3cbf61a48 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -315,22 +315,52 @@ extends: tcl tk gettext asciidoc xmlto \ libcurl4-gnutls-dev libpcre2-dev zlib1g-dev libexpat-dev \ curl ca-certificates - # TODO: add tasks to build Git and installers - - script: | - echo $(cc_arch) - echo $(deb_arch) - mkdir -p $(Build.ArtifactStagingDirectory)/app - debroot=$(Build.ArtifactStagingDirectory)/pkgroot - mkdir -p $debroot/DEBIAN - cat > $debroot/DEBIAN/control <version + make GIT-VERSION-FILE + + PKGNAME="microsoft-git_${VERSION}_$(deb_arch)" + PKGDIR="$(Build.ArtifactStagingDirectory)/pkgroot" + rm -rf "$PKGDIR" + mkdir -p "$PKGDIR/DEBIAN" + + DESTDIR="$PKGDIR" make -j"$(nproc)" V=1 DEVELOPER=1 \ + USE_LIBPCRE=1 \ + USE_CURL_FOR_IMAP_SEND=1 NO_OPENSSL=1 \ + NO_CROSS_DIRECTORY_HARDLINKS=1 \ + ASCIIDOC8=1 ASCIIDOC_NO_ROFF=1 \ + ASCIIDOC='TZ=UTC asciidoc' \ + prefix=/usr/local \ + gitexecdir=/usr/local/lib/git-core \ + libexecdir=/usr/local/lib/git-core \ + htmldir=/usr/local/share/doc/git/html \ + install install-doc install-html + + # Based on https://packages.ubuntu.com/xenial/vcs/git + cat >"$PKGDIR/DEBIAN/control" < + Description: Git client built from the https://github.com/microsoft/git repository, + specialized in supporting monorepo scenarios. Includes the Scalar CLI. + CTRL + + mkdir -p "$(Build.ArtifactStagingDirectory)/app" + dpkg-deb -Zxz --build "$PKGDIR" \ + "$(Build.ArtifactStagingDirectory)/app/$PKGNAME.deb" - ${{ if eq(parameters.esrp, true) }}: # ESRP ADO tasks require .NET, so we install it here since the # Linux images do not have it by default. From 0804fa59e81c53d091030d41143c7dce7b089484 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 16:31:50 +0200 Subject: [PATCH 03/16] azure-pipelines: stage signed Debian package for upload Replace the Linux dummy collect step with a focused move of just the signed microsoft-git__.deb into $(Build.ArtifactStagingDirectory)/_final/, which the existing templateContext.outputs.pipelineArtifact already publishes as the linux_x64 / linux_arm64 artifact. The dummy version copied everything under app/* into _final/, which worked in isolation but would have started silently uploading any intermediate files (including the pkgroot/ staging tree the build step now writes alongside the .deb). Naming the file precisely also turns "ESRP signed something else" into a missing-file error rather than a silent wrong-artifact upload. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 31d1f3cbf61a48..ea098b47a47807 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -384,11 +384,15 @@ extends: "Parameters": {} } ] - # 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' + - task: Bash@3 + displayName: 'Stage Debian package for upload' + inputs: + targetType: inline + script: | + set -euo pipefail + mkdir -p "$(Build.ArtifactStagingDirectory)/_final" + mv "$(Build.ArtifactStagingDirectory)/app/microsoft-git_$(git_version)_$(deb_arch).deb" \ + "$(Build.ArtifactStagingDirectory)/_final/" - stage: release displayName: 'Release' From 2e22dec901795ff44c46b5fb913aaf8ed637557c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 17:09:27 +0200 Subject: [PATCH 04/16] azure-pipelines: install macOS build dependencies Port the dual-architecture Homebrew install from the GitHub workflow's create-macos-artifacts job (.github/workflows/build-git-installers.yml). The native Homebrew on the macOS-latest pool image is arm64 and lives at /opt/homebrew. To produce a universal binary we additionally need the x86_64 build of gettext/libintl, so install a separate x86_64 Homebrew under /usr/local via the upstream installer running under Rosetta and pull gettext from there as well. The native arm64 brew installs the rest of the build chain: automake, asciidoc, xmlto, and docbook (the same set the workflow installs). The two arch-specific libintl.a copies are then combined with lipo into a universal archive at the workspace root, where the upcoming config.mak's LDFLAGS = -L"$(pwd)" will find it. libintl depends on iconv, but the system /usr/lib/libiconv.dylib is already universal and exports the _iconv* symbols Homebrew's gettext was built against; Homebrew's own libiconv exports _libiconv* and would not link, which is why the comment from the workflow is preserved here. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index ea098b47a47807..a44e1f5d41d913 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -239,7 +239,37 @@ extends: artifactName: '${{ dim.id }}' steps: - checkout: self - # TODO: add tasks to set up build environment + - task: Bash@3 + displayName: 'Install build dependencies' + inputs: + targetType: inline + script: | + set -euo pipefail + + # The agent's native arm64 Homebrew lives at + # /opt/homebrew. Install a separate x86_64 Homebrew + # under /usr/local so we can fetch the x86_64 build + # of gettext/libintl alongside the arm64 one. + arch -x86_64 /bin/bash -c \ + "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + arch -x86_64 /usr/local/bin/brew install gettext + + # Native (arm64) build dependencies. + brew install automake asciidoc xmlto docbook + brew link --force gettext + + # Make a universal libintl.a out of the two + # arch-specific copies, dropped at the workspace + # root so the build's LDFLAGS = -L"$(pwd)" can find + # it. libintl depends on iconv, but we deliberately + # use the system's universal /usr/lib/libiconv.dylib + # rather than Homebrew's libiconv (which exports + # _libiconv* symbols, while Homebrew's gettext was + # built against system iconv with _iconv* symbols). + lipo -create \ + -output libintl.a \ + /usr/local/opt/gettext/lib/libintl.a \ + /opt/homebrew/opt/gettext/lib/libintl.a # TODO: add tasks to build Git and installers - script: | echo "Hello, Mac!" From 2f8c9cf9297c11b9cfafeca35870bbad3ca00cb6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 17:12:42 +0200 Subject: [PATCH 05/16] azure-pipelines: configure universal macOS build Port the config.mak generation from the GitHub workflow's create-macos-artifacts job. With the universal libintl.a from the previous step in place, the build needs the Make flags that turn on the dual-arch compile and that route around several macOS quirks: HOST_CPU=universal, dual -arch CFLAGS (the actual universal-binary driver), -DNO_OPENSSL for contrib Makefiles that do not see the main Makefile's NO_OPENSSL handling, and explicit USE_HOMEBREW_LIBICONV/ICONVDIR overrides so we link against the universal /usr/lib/libiconv.dylib (whose unprefixed _iconv* symbols are what Homebrew's gettext was built against, unlike Homebrew's own libiconv with its prefixed _libiconv* symbols). CFLAGS adds the gettext include dirs from both Homebrew prefixes; LDFLAGS = -L"$(pwd)" so the linker finds the universal libintl.a in the workspace root. CURL_LDFLAGS / CURL_CONFIG pin against the OS-supplied libcurl rather than a Homebrew copy. Finally, SKIP_DASHED_BUILT_INS disables the dashed built-ins; on macOS the hard-link optimisation does not kick in for the staging tree and the resulting full copies would bloat the eventual .dmg. Compared with the GitHub workflow, the differences are mechanical: the source tree is at $(Build.SourcesDirectory) (no actions/checkout "path: git" subdir), so config.mak and the version file land at the worktree root; $(git_version) replaces tag_version; and the script prologue is set -euo pipefail rather than set -ex. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index a44e1f5d41d913..ed3fdce59150d3 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -270,6 +270,64 @@ extends: -output libintl.a \ /usr/local/opt/gettext/lib/libintl.a \ /opt/homebrew/opt/gettext/lib/libintl.a + - task: Bash@3 + displayName: 'Configure universal build' + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + # Git's GIT-VERSION-GEN expects .rc rather than -rc + BUILD_VERSION="$(echo "$VERSION" | sed 's/-rc/.rc/g')" + echo "$BUILD_VERSION" >version + + # HOST_CPU is a bit of a lie and is only used in + # 'git version --build-options'; we'll fix that in + # code. The two -arch flags are what actually drive + # the universal build. + cat >config.mak <>config.mak <>config.mak <>config.mak # TODO: add tasks to build Git and installers - script: | echo "Hello, Mac!" From aa4f1e3f5da43ee179a86d71f31f0bcb407e062c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 17:16:43 +0200 Subject: [PATCH 06/16] azure-pipelines: build payload via macos-installer Makefile Port the build sequence from the GitHub workflow's create-macos-artifacts step. Runs `make GIT-VERSION-FILE dist dist-doc` in the source tree, recovers the original commit OID from the resulting source tarball with `git get-tar-commit-id` (this becomes GIT_BUILT_FROM_COMMIT, which the macos-installer Makefile bakes into `git version --build-options`), then extracts the source and manpage tarballs into payload/ and manpages/, drops a copy of the worktree config.mak inside the extracted source so the universal-build flags apply during the real compile, and finally runs `make -C .github/macos-installer payload` to produce the universal binary tree. The macos-installer Makefile derives BUILD_DIR from $(GITHUB_WORKSPACE), which is unset under ADO. Setting GITHUB_WORKSPACE=$(Build.SourcesDirectory) in the task env block gives the Makefile the same anchor it had under GitHub Actions, so payload/git-$VERSION/ ends up where the targets expect it. XML_CATALOG_FILES is exported to the catalogs from the Homebrew docbook installed in the dependencies step, so asciidoc/xmlto can resolve their DTDs during dist-doc. VERSION is exported because the macos-installer Makefile reads it directly. The .rc/-rc rewrite stays consistent with the config-generation step: the on-disk version file has the .rc form (Git's GIT-VERSION-GEN convention), while the package and artifact filenames keep the original tag spelling (which is what the Makefile's ORIGINAL_VERSION tracks). Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index ed3fdce59150d3..db578fd8794ace 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -328,6 +328,45 @@ extends: # they end up as full copies instead and bloat the # .dmg indecently. Skip them entirely. echo 'SKIP_DASHED_BUILT_INS = YabbaDabbaDoo' >>config.mak + - task: Bash@3 + displayName: 'Build payload via macos-installer' + env: + # The macos-installer Makefile derives BUILD_DIR from + # $(GITHUB_WORKSPACE), which is unset in ADO. Point it + # at the worktree root. + GITHUB_WORKSPACE: $(Build.SourcesDirectory) + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + BUILD_VERSION="$(echo "$VERSION" | sed 's/-rc/.rc/g')" + + # The asciidoc/xmlto build steps need the catalogs + # from Homebrew docbook. + export XML_CATALOG_FILES="$(brew --prefix)/etc/xml/catalog" + + make -j"$(sysctl -n hw.physicalcpu)" GIT-VERSION-FILE dist dist-doc + + # Recover the source-tree commit OID from the dist + # tarball; the macos-installer Makefile bakes it + # into 'git version --build-options'. + GIT_BUILT_FROM_COMMIT="$(gunzip -c "git-$BUILD_VERSION.tar.gz" \ + | git get-tar-commit-id)" + export GIT_BUILT_FROM_COMMIT + export VERSION + + mkdir payload manpages + tar -xf "git-$BUILD_VERSION.tar.gz" -C payload + tar -xf "git-manpages-$BUILD_VERSION.tar.gz" -C manpages + + # The actual compile happens inside the extracted + # tree, against a copy of the config.mak we wrote + # at the worktree root in the previous step. + cp config.mak "payload/git-$BUILD_VERSION/config.mak" + + make -C .github/macos-installer V=1 payload # TODO: add tasks to build Git and installers - script: | echo "Hello, Mac!" From c2fc33b8f1fe7b838af03f543b07f63f3cf5bfaf Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 17:22:27 +0200 Subject: [PATCH 07/16] azure-pipelines: ESRP-sign macOS payload binaries Replace the dummy "Hello, Mac!" build placeholder and the dummy ESRP signing block with a real signing flow against the universal payload built in the previous step. The pattern follows git-credential-manager/.azure-pipelines/release.yml: pre-filter the payload to just the Mach-O files (using `file --mime` matching `mach`, the same heuristic the existing .github/scripts/codesign.sh uses), copy that subset into a staging directory under $(Build.ArtifactStagingDirectory)/macos-tosign/ preserving relative paths, hand the staging dir off to the existing .azure-pipelines/esrp/sign.yml template (which zips, signs via EsrpCodeSigning@6 with KeyCode CP-401337-Apple + OperationCode MacAppDeveloperSign + Hardening enabled, and extracts back into the staging dir), then copy the signed binaries back over the payload tree. The pre-filter is necessary because the existing template's CopyFiles@2 step uses minimatch globs and the only reliable way to pick out Mach-O files is by file content. Signing the entire payload tree would either fail on non-binary files or sign things that should not be signed (shell scripts, perl, manpages, templates, the uninstall.sh). UseDotNet@2 (8.x) installs the .NET SDK that EsrpCodeSigning@6 depends on; the macOS-latest pool image does not provide it. The whole block is gated on the `esrp` pipeline parameter, matching the existing convention in the file. This commit replaces both the dummy build (which produced $(Build.ArtifactStagingDirectory)/app/example) and the dummy ESRP call (which signed that fake artifact) with a real signing flow against the payload we now build for real. The dummy collect step remains for now and is replaced when the .pkg/.dmg flow lands. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 62 ++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index db578fd8794ace..ff3d6d3c2dc839 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -367,19 +367,52 @@ extends: cp config.mak "payload/git-$BUILD_VERSION/config.mak" make -C .github/macos-installer V=1 payload - # 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' + # ESRP-sign the universal Mach-O binaries inside the + # payload tree. The existing esrp/sign.yml template's + # CopyFiles@2 step uses minimatch globs and the only + # reliable way to detect Mach-O is by file content + # (file --mime), so we pre-filter into a staging dir, + # let the template zip/sign/extract that staging dir, + # then copy the signed binaries back over the payload. - ${{ if eq(parameters.esrp, true) }}: + # ESRP ADO tasks require .NET, which the macOS pool + # image does not provide by default. + - task: UseDotNet@2 + displayName: 'Install .NET for ESRP' + inputs: + packageType: sdk + version: '8.x' + - task: Bash@3 + displayName: 'Stage Mach-O binaries for signing' + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + BUILD_VERSION="$(echo "$VERSION" | sed 's/-rc/.rc/g')" + payload_dir="payload/git-$BUILD_VERSION" + stage_dir="$(Build.ArtifactStagingDirectory)/macos-tosign/binaries" + + rm -rf "$stage_dir" + mkdir -p "$stage_dir" + + pushd "$payload_dir" + find . -type f -exec file --mime {} + \ + | sed -n '/mach/s/: .*//p' \ + | while IFS= read -r f; do + rel="${f#./}" + tgt="$stage_dir/$rel" + mkdir -p "$(dirname "$tgt")" + cp -- "$f" "$tgt" + done + popd - template: .azure-pipelines/esrp/sign.yml@self parameters: - displayName: 'Example sign binaries' - folderPath: '$(Build.ArtifactStagingDirectory)/app' + displayName: 'ESRP-sign Mach-O binaries' + folderPath: '$(Build.ArtifactStagingDirectory)/macos-tosign/binaries' pattern: '**/*' - useArchive: true # Must be true when macOS signing + useArchive: true # Required for macOS signing inlineOperation: | [ { @@ -392,6 +425,17 @@ extends: } } ] + - task: Bash@3 + displayName: 'Copy signed binaries back to payload' + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + BUILD_VERSION="$(echo "$VERSION" | sed 's/-rc/.rc/g')" + cp -R "$(Build.ArtifactStagingDirectory)/macos-tosign/binaries"/* \ + "payload/git-$BUILD_VERSION/" # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - script: | mkdir -p $(Build.ArtifactStagingDirectory)/_final From 76cd46ed28efbedb28ef79a5cd5762efabf3b266 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 17:35:54 +0200 Subject: [PATCH 08/16] azure-pipelines: build unsigned macOS installer pkg Run the macos-installer Makefile's `pkg` target to produce .github/macos-installer/disk-image/git--universal.pkg from the signed payload tree built and ESRP-signed in the preceding steps. Crucially we leave APPLE_INSTALLER_IDENTITY undefined. The Makefile's pkg_cmd has an `ifdef APPLE_INSTALLER_IDENTITY` branch that adds `--sign ""` to the pkgbuild invocation; with the variable unset, pkgbuild produces an unsigned .pkg, which ESRP then signs in the next commit. This is the whole point of the migration: replace the `productsign`-via-pkgbuild path with ESRP signing of the resulting .pkg. GITHUB_WORKSPACE and VERSION are exported for the same reason as in the payload step: the Makefile reads them directly and there is no GitHub Actions runtime under ADO to set them automatically. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index ff3d6d3c2dc839..f227a05f880824 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -436,6 +436,24 @@ extends: BUILD_VERSION="$(echo "$VERSION" | sed 's/-rc/.rc/g')" cp -R "$(Build.ArtifactStagingDirectory)/macos-tosign/binaries"/* \ "payload/git-$BUILD_VERSION/" + - task: Bash@3 + displayName: 'Build unsigned installer pkg' + env: + GITHUB_WORKSPACE: $(Build.SourcesDirectory) + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + export VERSION + + # Leave APPLE_INSTALLER_IDENTITY undefined so the + # Makefile's `pkg` target produces an unsigned .pkg + # (the `ifdef APPLE_INSTALLER_IDENTITY` branch in + # pkg_cmd is skipped). ESRP signs it in the next + # step. + make -C .github/macos-installer V=1 pkg # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - script: | mkdir -p $(Build.ArtifactStagingDirectory)/_final From bc1baec417ce3695347de4dba00fd02db4bc581a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 17:39:37 +0200 Subject: [PATCH 09/16] azure-pipelines: ESRP-sign macOS installer pkg Sign the unsigned .pkg produced in the previous step. The same ESRP path used for the Mach-O binaries works here too: KeyCode CP-401337-Apple covers both Developer ID Application and Developer ID Installer certs in this account, so MacAppDeveloperSign on a .pkg is the productsign equivalent. git-credential-manager/.azure-pipelines/release.yml uses exactly this pattern (same KeyCode and OperationCode, just pointed at the .pkg instead of the binary tree), so the sign-then-extract cycle through the existing esrp/sign.yml template applies unchanged. folderPath is the macos-installer's disk-image/ directory, where the previous step deposited the unsigned .pkg; the template's ExtractFiles@1 writes the signed .pkg back over it. This replaces the productsign --sign branch in the macos-installer Makefile's pkg_cmd, which we deliberately did not exercise (we left APPLE_INSTALLER_IDENTITY undefined in the previous commit so pkgbuild produced an unsigned .pkg). Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index f227a05f880824..d0e89a97bbf41a 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -454,6 +454,25 @@ extends: # pkg_cmd is skipped). ESRP signs it in the next # step. make -C .github/macos-installer V=1 pkg + - ${{ if eq(parameters.esrp, true) }}: + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'ESRP-sign installer pkg' + folderPath: '.github/macos-installer/disk-image' + pattern: '*.pkg' + useArchive: true # Required for 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 From 88fb9d3df7fd09a03b48e62812b7de360cf9ccea Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 19:08:34 +0200 Subject: [PATCH 10/16] azure-pipelines: ESRP-notarize macOS installer pkg Submit the signed .pkg through ESRP for Apple notarization. Same KeyCode (CP-401337-Apple) and same template as the previous two ESRP calls, but a different OperationCode (MacAppNotarize) and a required BundleId parameter. The bundle identifier comes straight from the macos-installer Makefile's pkg_cmd, which invokes `pkgbuild --identifier com.git.pkg`. Reusing it here keeps the notarization request consistent with the actual identifier baked into the package. This replaces the `xcrun notarytool submit ... --wait` plus `xcrun stapler staple` flow that .github/scripts/notarize.sh performs in the GitHub workflow's `make notarize` target. The ESRP MacAppNotarize operation handles both the submission and the ticket stapling, returning the notarized .pkg back into disk-image/ via the same zip-extract template path the previous sign step used. git-credential-manager/.azure-pipelines/release.yml uses the same operation with its own BundleId; that pipeline is the working reference for this combination of KeyCode, OperationCode, and useArchive: true. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index d0e89a97bbf41a..5691a7244fe0a8 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -473,6 +473,24 @@ extends: } } ] + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'ESRP-notarize installer pkg' + folderPath: '.github/macos-installer/disk-image' + pattern: '*.pkg' + useArchive: true # Required for macOS notarization + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppNotarize", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "BundleId": "com.git.pkg" + } + } + ] # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - script: | mkdir -p $(Build.ArtifactStagingDirectory)/_final From 7d0955475f953cc9b04efd87d6bf15890f7a6524 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 19:17:43 +0200 Subject: [PATCH 11/16] azure-pipelines: build macOS DMG and stage artifacts for upload Run the macos-installer Makefile's `image` target to build .github/macos-installer/git--universal.dmg from the contents of disk-image/, which by this point contains the signed and notarized .pkg from the previous ESRP step. hdiutil's behaviour is unchanged from the GitHub workflow path; only the trigger is. The final stage step replaces the placeholder dummy collect step that pointed at the long-dead $(Build.ArtifactStagingDirectory)/app directory. The .dmg lands at the macos-installer root and the signed-and-notarized .pkg lives in disk-image/; we move both (named precisely, not globbed) into $(Build.ArtifactStagingDirectory)/_final/, which the job's templateContext.outputs already publishes as the macos_universal pipeline artifact. Naming the files instead of globbing turns "the build skipped one" into a missing-file error rather than a silent half-empty upload, matching the same defensive choice the Linux stage step makes. GITHUB_WORKSPACE and VERSION are exported for the same reason as the earlier macos-installer Makefile invocations: the Makefile reads them directly and ADO does not set them automatically. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 5691a7244fe0a8..b063aa3f2dda2a 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -491,11 +491,35 @@ extends: } } ] - # 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' + - task: Bash@3 + displayName: 'Build DMG' + env: + GITHUB_WORKSPACE: $(Build.SourcesDirectory) + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + export VERSION + + # Builds .github/macos-installer/git--universal.dmg + # from the contents of disk-image/, which now contains + # the signed and notarized .pkg. + make -C .github/macos-installer V=1 image + - task: Bash@3 + displayName: 'Stage installer artifacts for upload' + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + mkdir -p "$(Build.ArtifactStagingDirectory)/_final" + mv ".github/macos-installer/git-$VERSION-universal.dmg" \ + "$(Build.ArtifactStagingDirectory)/_final/" + mv ".github/macos-installer/disk-image/git-$VERSION-universal.pkg" \ + "$(Build.ArtifactStagingDirectory)/_final/" # # Linux build jobs From 6f7601f32b437d5ace34a0d40af39d6879d609bb Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 1 May 2026 09:55:23 +0200 Subject: [PATCH 12/16] azure-pipelines: bootstrap Git for Windows SDK on Windows agents GitHub Actions has the git-for-windows/setup-git-for-windows-sdk@v1 action that drops a full SDK onto the runner; ADO has no equivalent task, so the Windows job has to bootstrap the SDK by hand before it can run any of the bash-driven build steps the GitHub workflow relies on. The SDK is published as a build-installers.tar.zst snapshot in the `ci-artifacts` rolling release of git-for-windows/git-sdk-64 (and git-sdk-arm64 for ARM64). Add a `sdk_repo` field to each windows_matrix entry so the new download step can pick the right one, and surface it as a job-level variable. Decompressing zstd archives is the awkward bit: Windows 2022's built-in tar.exe (BSD tar) does not understand zstd; Git for Windows' GNU tar can but only if zstd.exe is on the PATH; and we cannot assume Git for Windows is even installed on the agent. The robust path, as suggested by Johannes Schindelin in the planning discussion for this PR, is to download tar.exe, zstd.exe, and msys-zstd-1.dll directly from https://github.com/git-for-windows/git-sdk-64/raw/HEAD/usr/bin into a tools dir, prepend that dir to PATH (in this task via $env:PATH so the extraction can use them, and via ##vso[task.prependpath] so subsequent tasks see them too), then extract the SDK archive with `tar --use-compress-program='zstd -d'` into C:\sdk. The downloaded MSYS2 binaries depend on msys-2.0.dll, which is satisfied either by the agent's bundled MinGit (which the prior setup-git-bash.cmd step puts on the PATH) or, after extraction, by the SDK's own usr/bin. Both paths are kept on PATH after the bootstrap so subsequent bash tasks find a working bash, and the matching MinGW toolchain (mingw64 for x64, clangarm64 for ARM64) is also surfaced. If 1ES network egress later turns out to forbid those raw and release downloads, the same bytes can be vendored via Secure Files or fetched via a partial clone of git-sdk-64; that switch is a follow-up. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index b063aa3f2dda2a..4ca6b51ba5cbc8 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -35,6 +35,9 @@ parameters: os: windows toolchain: x86_64 mingwprefix: mingw64 + # GitHub repo whose ci-artifacts release exposes a + # build-installers.tar.zst snapshot of the matching SDK. + sdk_repo: git-for-windows/git-sdk-64 - id: windows_arm64 jobName: 'Windows (ARM64)' @@ -44,6 +47,7 @@ parameters: os: windows toolchain: clang-aarch64 mingwprefix: clangarm64 + sdk_repo: git-for-windows/git-sdk-arm64 - name: macos_matrix type: object @@ -150,6 +154,7 @@ extends: git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] toolchain: ${{ dim.toolchain }} mingwprefix: ${{ dim.mingwprefix }} + sdk_repo: ${{ dim.sdk_repo }} templateContext: outputs: - output: pipelineArtifact @@ -175,6 +180,48 @@ extends: Write-Host "##vso[task.prependpath]$azPath" Write-Host "Azure CLI installed." displayName: 'Install Azure CLI (arm64)' + - task: PowerShell@2 + displayName: 'Bootstrap Git for Windows SDK' + inputs: + targetType: inline + script: | + $ErrorActionPreference = 'Stop' + $ProgressPreference = 'SilentlyContinue' + + # Win 2022's built-in tar.exe (BSD tar) does not + # decompress zstd archives, and the agent image is + # not guaranteed to ship Git for Windows. Download + # tar.exe + zstd.exe + msys-zstd-1.dll straight + # from git-sdk-64's HEAD and use those. + $tools = "$(Build.SourcesDirectory)\sdk-tools" + New-Item -ItemType Directory -Path $tools -Force | Out-Null + $base = 'https://github.com/git-for-windows/git-sdk-64/raw/HEAD/usr/bin' + foreach ($f in 'tar.exe', 'zstd.exe', 'msys-zstd-1.dll') { + Invoke-WebRequest "$base/$f" -OutFile "$tools\$f" + } + + # Make tar/zstd findable inside this task too; + # ##vso[task.prependpath] only affects subsequent + # tasks. + $env:PATH = "$tools;$env:PATH" + + # Download the SDK snapshot for the matching arch + # from its ci-artifacts rolling release. + $archive = "$(Build.SourcesDirectory)\build-installers.tar.zst" + Invoke-WebRequest ` + "https://github.com/$(sdk_repo)/releases/download/ci-artifacts/build-installers.tar.zst" ` + -OutFile $archive + + # Extract the SDK into C:\sdk. + $sdk = 'C:\sdk' + New-Item -ItemType Directory -Path $sdk -Force | Out-Null + & tar.exe --use-compress-program='zstd -d' -xf $archive -C $sdk + + # Make tar tools, the SDK's bash, and the matching + # MinGW toolchain available to subsequent tasks. + Write-Host "##vso[task.prependpath]$tools" + Write-Host "##vso[task.prependpath]$sdk\usr\bin" + Write-Host "##vso[task.prependpath]$sdk\$(mingwprefix)\bin" # Setup ESRP code signing for Windows (sets ESRP_TOOL, ESRP_AUTH) - ${{ if eq(parameters.esrp, true) }}: - template: .azure-pipelines/esrp/windows/setup.yml@self From bea7ceef32e7d1d090e17ffff7b3c9ce7e5fb6e4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 1 May 2026 10:00:42 +0200 Subject: [PATCH 13/16] azure-pipelines: clone build-extra into Windows SDK Git for Windows' build tooling (please.sh, signtool.sh, the installer .iss templates, and the MINGW-packages helpers) lives in git-for-windows/build-extra rather than in the SDK snapshot. The GitHub workflow's Windows job clones it into /usr/src/build-extra of the SDK before invoking please.sh; mirror that here. A partial clone (--filter=blob:none) plus --single-branch -b main is enough for everything please.sh needs and avoids pulling the full blob history; it matches the workflow's invocation byte for byte. The bash task picks up the SDK's bash from the PATH set up by the preceding bootstrap step, so /usr/src resolves into the SDK at C:\sdk\usr\src as expected. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 4ca6b51ba5cbc8..dacd058b3ba006 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -222,6 +222,18 @@ extends: Write-Host "##vso[task.prependpath]$tools" Write-Host "##vso[task.prependpath]$sdk\usr\bin" Write-Host "##vso[task.prependpath]$sdk\$(mingwprefix)\bin" + - task: Bash@3 + displayName: 'Clone build-extra into SDK' + inputs: + targetType: inline + script: | + set -euo pipefail + # The please.sh + signtool.sh scripts the build + # relies on live in build-extra; the SDK ships + # without them. Partial clone to keep this fast. + git clone --filter=blob:none --single-branch -b main \ + https://github.com/git-for-windows/build-extra \ + /usr/src/build-extra # Setup ESRP code signing for Windows (sets ESRP_TOOL, ESRP_AUTH) - ${{ if eq(parameters.esrp, true) }}: - template: .azure-pipelines/esrp/windows/setup.yml@self From 1dd22243ebc8d6f77622cfeb75d659624d3b9a00 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 1 May 2026 10:03:36 +0200 Subject: [PATCH 14/16] azure-pipelines: build mingw-w64-git package on Windows Drive please.sh build-mingw-w64-git from a Bash@3 task using the SDK's bash that the bootstrap step put on the PATH. Outputs land in $(Build.SourcesDirectory)/artifacts/ so the subsequent installer-build step can pass them to please.sh make_installers_from_mingw_w64_git via --pkg= flags. Three small adaptations from the GitHub workflow's source step: The /usr/bin/git trampoline that delegates to the matching MinGW-built git.exe is the same one the workflow writes by hand; makepkg-mingw shells out to plain `git`, and the SDK bash's git candidates would otherwise come from MinGit, not the toolchain we are building against. The user.name / user.email / PACKAGER values used to be the GitHub actor; on ADO there is no equivalent identity, so the initial port hardcodes a build-bot identity. If this needs to attribute to a specific human or service principal later, that is a one-line change here. please.sh's --only- flag takes the bare CPU name (x86_64 or aarch64), not the toolchain triple. windows_x64 already happens to match (toolchain=x86_64), but windows_arm64's toolchain is clang-aarch64, so a small case statement maps it to aarch64. Adding another arch later would extend the case rather than touch the matrix shape. Two pieces of the workflow's pkg-build step are intentionally not ported in this commit and noted in a comment for follow-up: the per-tarball GPG signing (replaceable with an ESRP PGP operation analogous to the Linux LinuxSign flow if needed downstream), and the MINGW-packages bundle creation, which depends on a PKGBUILD. snapshot that microsoft/git does not currently ship. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index dacd058b3ba006..0365dd3a3da56b 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -234,6 +234,48 @@ extends: git clone --filter=blob:none --single-branch -b main \ https://github.com/git-for-windows/build-extra \ /usr/src/build-extra + - task: Bash@3 + displayName: 'Build mingw-w64-git package' + inputs: + targetType: inline + script: | + set -euo pipefail + set -x + + # makepkg-mingw runs git internally and looks for a + # plain /usr/bin/git; trampoline it to the matching + # MinGW-built git.exe. + printf '#!/bin/sh\n\nexec /%s/bin/git.exe "$@"\n' \ + "$(mingwprefix)" >/usr/bin/git + chmod +x /usr/bin/git + + USER_NAME='microsoft-git-build' + USER_EMAIL='microsoft-git-build@users.noreply.github.com' + git config --global user.name "$USER_NAME" + git config --global user.email "$USER_EMAIL" + export PACKAGER="$USER_NAME <$USER_EMAIL>" + + # please.sh's --only- takes the bare CPU + # name (x86_64 / aarch64), not the toolchain + # triple ($(toolchain) here is x86_64 or + # clang-aarch64). + case "$(toolchain)" in + x86_64) cpu_arch=x86_64 ;; + clang-aarch64) cpu_arch=aarch64 ;; + *) echo "unknown toolchain: $(toolchain)" >&2; exit 1 ;; + esac + + sh -x /usr/src/build-extra/please.sh build-mingw-w64-git \ + --only-"$cpu_arch" \ + --build-src-pkg \ + -o artifacts \ + HEAD + + # NOTE: the GitHub workflow additionally GPG-signs + # each tarball and creates a MINGW-packages.bundle + # for downstream Pacman consumers; both are + # intentionally out of scope for the initial port + # and tracked as follow-ups. # Setup ESRP code signing for Windows (sets ESRP_TOOL, ESRP_AUTH) - ${{ if eq(parameters.esrp, true) }}: - template: .azure-pipelines/esrp/windows/setup.yml@self From 3c7b99327b7dd11dd33185c473736111f1ec4d8d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 1 May 2026 10:07:40 +0200 Subject: [PATCH 15/16] azure-pipelines: build Windows installer and portable Git Drive please.sh make_installers_from_mingw_w64_git for both installer and portable variants from a single bash task. The GitHub workflow runs these as separate matrix jobs (one per type/arch combination); the AzP version keeps both builds in the same job so the .pkg.tar.* artifacts produced in the previous step are available without an inter-job artifact passing trip. The PDB archive copy into build-extra/cached-source-packages is the same prerequisite that --include-pdbs needs in the GitHub workflow. Build-extra reads from there to embed PDBs into the installer; without the copy --include-pdbs is silently ineffective. The --pkg= filter that strips signatures and the optional archimport / cvs / p4 / gitweb / doc-man pieces matches the workflow's sed exactly so the resulting .exe sizes are comparable. The two .exe artifacts here are still unsigned: the GitHub workflow relies on Inno Setup invoking signtool.sh via a git signtool alias to sign the installer at build time, and a post-build git signtool to sign the portable. Neither path is wired here; the next commit applies ESRP signing to both .exe artifacts after the build, which is the migration target this PR series is moving towards. The SHA-256 sidecar the workflow emits follows in that same next commit, since its content depends on the signed bytes. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 0365dd3a3da56b..dc12d70f4fbe20 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -276,6 +276,38 @@ extends: # for downstream Pacman consumers; both are # intentionally out of scope for the initial port # and tracked as follow-ups. + - task: Bash@3 + displayName: 'Build installer and portable Git' + inputs: + targetType: inline + script: | + set -euo pipefail + set -x + + b=/usr/src/build-extra + + # please.sh make_installers_from_mingw_w64_git + # --include-pdbs reads PDB archives from + # cached-source-packages/. + mkdir -p "$b/cached-source-packages" + cp artifacts/*-pdb* "$b/cached-source-packages/" + + # The --pkg=... list excludes the optional pieces + # the workflow drops (signatures, archimport, cvs, + # p4, gitweb, doc-man); keep the same filter so + # the resulting .exe size is comparable. + pkg_args=$( + ls artifacts/mingw-w64-$(toolchain)-*.tar.* \ + | sed '/\.sig$/d;/archimport/d;/cvs/d;/p4/d;/gitweb/d;/doc-man/d;s/^/--pkg=/' \ + | tr '\n' ' ' + ) + + for type in installer portable; do + eval "$b"/please.sh make_installers_from_mingw_w64_git --include-pdbs \ + --version="$(git_version)" \ + -o artifacts --"$type" \ + $pkg_args + done # Setup ESRP code signing for Windows (sets ESRP_TOOL, ESRP_AUTH) - ${{ if eq(parameters.esrp, true) }}: - template: .azure-pipelines/esrp/windows/setup.yml@self From 7bdc59e2a67d061d2f9b9dfd6bd6c3057b7aa96a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 1 May 2026 10:14:25 +0200 Subject: [PATCH 16/16] azure-pipelines: ESRP-sign and stage Windows installer artifacts Replace the dummy ESRP-signing block (which copied calc.exe into example*.exe and signed those) and the dummy collect block (which xcopy'd the same fake artifacts into _final/) with the real post-build flow. The signing call hands artifacts/Git-*.exe and artifacts/PortableGit-*.exe to the existing .azure-pipelines/esrp/windows/esrpsign.sh wrapper that mjcheetham landed earlier. That script reads ESRP_TOOL and ESRP_AUTH from the env block (set by the setup.yml template above) and SYSTEM_ACCESSTOKEN from $(System.AccessToken). The wrapper script is the same one that a future `git signtool` alias is intended to delegate to per the comment on commit 406d385f6ec00, so signing the installer at Inno Setup time becomes a follow-up that does not require touching this step. The SHA-256 sidecar moves here from the build commit so the hashes reflect the bytes that ship: ESRP signing rewrites the .exe contents, so a SHA-256 computed before signing would mismatch the released artifact. When ESRP is disabled the openssl call still runs against the unsigned binaries, which is the right answer in that case. The final cp into $(Build.ArtifactStagingDirectory)/_final/ ships exactly Git-*.exe, PortableGit-*.exe, and sha-256.txt; the templateContext.outputs.pipelineArtifact already publishes that directory as the windows_x64 / windows_arm64 artifact. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 53 +++++++++++++++++------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index dc12d70f4fbe20..d5d34050862f39 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -316,40 +316,37 @@ extends: 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: | + set -euo pipefail .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' + artifacts/Git-*.exe \ + artifacts/PortableGit-*.exe + displayName: 'ESRP-sign installer and portable Git' env: ESRP_TOOL: $(ESRP_TOOL) ESRP_AUTH: $(ESRP_AUTH) SYSTEM_ACCESSTOKEN: $(System.AccessToken) - # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - - script: | - mkdir $(Build.ArtifactStagingDirectory)\_final - xcopy /s /y $(Build.ArtifactStagingDirectory)\app $(Build.ArtifactStagingDirectory)\_final - displayName: 'Dummy collect artifacts' + - task: Bash@3 + displayName: 'Stage installer artifacts for upload' + inputs: + targetType: inline + script: | + set -euo pipefail + + # Compute SHA-256 over the (possibly signed) + # binaries; if ESRP signing ran, this picks up + # the post-sign bytes, which is what we want to + # publish in the release notes. + openssl dgst -sha256 \ + artifacts/Git-*.exe \ + artifacts/PortableGit-*.exe \ + | sed 's/.* //' >artifacts/sha-256.txt + + mkdir -p "$(Build.ArtifactStagingDirectory)/_final" + cp artifacts/Git-*.exe \ + artifacts/PortableGit-*.exe \ + artifacts/sha-256.txt \ + "$(Build.ArtifactStagingDirectory)/_final/" # # macOS build jobs