Skip to content

Replace Microbuild with 1ES PT + ESRP#1972

Merged
mjcheetham merged 10 commits into
masterfrom
esrp
May 14, 2026
Merged

Replace Microbuild with 1ES PT + ESRP#1972
mjcheetham merged 10 commits into
masterfrom
esrp

Conversation

@mjcheetham
Copy link
Copy Markdown
Member

@mjcheetham mjcheetham commented May 14, 2026

Microbuild doesn't give us as much control as we'd like over our build images, pool admin, and signing process. Since moving to NativeAOT, the SignFiles target of Microbuild runs at the wrong time.

The fact we're using Microbuild is a holdover from the organisational parentage of the then GVFS team back in 2017/2018. The Git Client team also maintains Git Credential Manager and Microsoft Git, where we use regular 1ES PT builds and ESRP directly for signing. Let's standardise on this model.

Also, whilst we're at it, we can add automatic GitHub release publishing (as draft)! See an example of a run with this branch:

image

This PR is probably easiest to review commit by commit, and referencing microsoft/git and GCM's build pipelines for prior art.

Remove all MicroBuild.Core PackageReference declarations and the
associated <FilesToSign> ItemGroups from GVFS.Payload, GVFS.Installers,
and FastFetch, and remove MicroBuild.Core from Directory.Packages.props.

Also drop the BeforeTargets="SignFiles" hooks from the CreatePayload
and CreateInstaller targets — without MicroBuild.Core's targets the
SignFiles target no longer exists, and the hooks are unnecessary now
that signing is performed externally by the release pipeline.

This commit is a pure removal of the in-build signing wiring; signing
itself moves to ESRP-driven steps in the release pipeline in a follow-up
commit. The GitHub Actions CI / PR build never signed anything and is
unaffected.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add a Condition="'$(SkipCreateInstaller)' != 'true'" to the
CreateInstaller MSBuild target so callers can opt out of the Inno
Setup compile step.

This lets the release pipeline build all of the managed binaries
first (via Build.bat with the env var set), ESRP-sign them in place,
and only then invoke the Inno Setup compile to produce
SetupGVFS.<version>.exe — packaging the already-signed binaries in a
single, deterministic pass instead of building, deleting, and
rebuilding the installer.

Default behavior is unchanged: with the property unset (the case for
local builds and the GitHub Actions PR/CI workflow), CreateInstaller
fires as before during the regular Build target.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces MicroBuild-based release signing with a 1ES Pipeline Template + ESRP signing flow and adds draft GitHub Release publishing for release builds.

Changes:

  • Removes MicroBuild package/signing metadata from project files.
  • Adds ESRP signing template and Azure release pipeline stages for build, signing, artifact staging, and GitHub release creation.
  • Adds CI helper scripts for installing VS C++ workload and enabling ProjFS.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
scripts/Build.bat Fails the build when VS MSBuild is unavailable.
GVFS/GVFS.Payload/layout.bat Removes C++ code-analysis marker files from payload output.
GVFS/GVFS.Payload/GVFS.Payload.csproj Removes MicroBuild signing integration from payload generation.
GVFS/GVFS.Installers/GVFS.Installers.csproj Removes MicroBuild signing and allows installer creation to be skipped.
GVFS/FastFetch/FastFetch.csproj Removes MicroBuild signing integration.
Directory.Packages.props Removes the MicroBuild.Core package version.
.azure-pipelines/scripts/install-vs-cpp-workload.ps1 Adds VS C++ workload bootstrap/verification logic.
.azure-pipelines/scripts/enable-projfs.ps1 Adds ProjFS optional-feature enablement logic for CI.
.azure-pipelines/release.yml Replaces MicroBuild release pipeline with 1ES PT build/sign/release flow.
.azure-pipelines/esrp/sign.yml Adds reusable ESRP signing step template.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .azure-pipelines/scripts/install-vs-cpp-workload.ps1 Outdated
Comment thread .azure-pipelines/scripts/enable-projfs.ps1 Outdated
Comment thread .azure-pipelines/release.yml Outdated
Comment on lines +159 to +181
inlineOperation: |
[
{
"KeyCode": "CP-230012",
"OperationCode": "SigntoolSign",
"ToolName": "sign",
"ToolVersion": "1.0",
"Parameters": {
"OpusName": "Microsoft",
"OpusInfo": "https://www.microsoft.com",
"FileDigest": "/fd SHA256",
"PageHash": "/NPH",
"TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
}
},
{
"KeyCode": "CP-230012",
"OperationCode": "SigntoolVerify",
"ToolName": "sign",
"ToolVersion": "1.0",
"Parameters": {}
}
]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, but given we treat the ESRP inline op JSON blob as an opaque data structure (generated by an ESRP tool we use) I'd like to keep this per artifact. Especially if we ever decide to publish, say, a NuGet package in the future.. that uses different signing key codes and settings.

Comment on lines +274 to +280
inputs:
- input: pipelineArtifact
artifactName: Installer
targetPath: $(Pipeline.Workspace)/assets/Installer
- input: pipelineArtifact
artifactName: Symbols
targetPath: $(Pipeline.Workspace)/assets/Symbols
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that we've never shipped FastFetch through the public project before. But should we? Is this an opportunity to unify and clarify that tool's signing and publishing story?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I intentionally omitted publishing FastFetch since we've not published it externally (at least not in the past 6 years!) If we find teams or others want FastFetch published 'officially' then we can add this easily to the GitHub releases later. For now it can be manually downloaded from the pipeline artifacts.

Comment thread .azure-pipelines/scripts/install-vs-cpp-workload.ps1 Outdated
- checkout: self
displayName: 'Checkout VFS for Git'
path: vfsforgit/src

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we even allow manual runs? Should the only trigger be a push to the release branch (gated by PR)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have allowed manual runs in the past to allow for testing and debugging of the release workflow against non-release branches. Pipeline queue permissions are still restricted to members of the Git Client team, so only a restricted few of us can make releases still.

I'd like to keep manual as an option for now until we have put this pipeline through more real-world usage :-)

mjcheetham and others added 8 commits May 14, 2026 14:13
… ESRP

Rewrite the release pipeline to extend the 1ES Pipeline Templates
directly and to perform code signing via inline ADO tasks, replacing
the existing MicroBuild.1ES.Official.yml@MicroBuildTemplate +
MicroBuild.Core <FilesToSign> mechanism.

This new pipeline matches the pattern already used by microsoft/git
and git-ecosystem/git-credential-manager.

We build the Payload and FastFetch with SkipCreateInstaller=true so
the Inno Setup compile is deferred until after ESRP signing, then
build the installer in a dedicated dotnet build step
(--no-dependencies prevents the Payload layout from re-running and
overwriting the freshly signed binaries).

We also add a release stage with a 1ES releaseJob that publishes a
draft GitHub Release on microsoft/VFSForGit attaching the installer
asset.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Download the Symbols pipeline artifact alongside the Installer in the
release stage, zip it up via ArchiveFiles@2, and attach it to the
draft GitHub Release so consumers (and crash-dump triage) can grab
the matching .pdb / native debug symbols for the binaries shipped in
SetupGVFS.<ver>.exe.

Both downloads now land under $(Pipeline.Workspace)/_final so the
GitHubRelease@1 'assets' glob picks up SetupGVFS.*.exe and
Symbols.zip from the same staging directory.

Also enable the auto-generated change log on the release while we're
already in the GitHubRelease@1 step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add a UseDotNet@2 task with useGlobalJson: true before the build step
so the agent uses exactly the SDK version pinned in global.json

This matches the behavior of the GitHub Actions build workflow, which
already uses actions/setup-dotnet with global-json-file.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Previously Build.bat warned and continued when it could not locate VS
MSBuild (via either PATH or vswhere), silently skipping the native
C++ projects (GitHooksLoader, GVFS.NativeTests, GVFS.PostIndexChangedHook,
GVFS.ReadObjectHook, GVFS.VirtualFileSystemHook). That meant a build
on a machine without the C++ workload would happily produce an
installer with the native binaries missing.

VS MSBuild and the C++ workload are not optional for any GVFS build
that's expected to run, so make a missing toolchain a hard error
instead. Anyone hitting it should install Visual Studio with the
'Desktop development with C++' workload (or VS Build Tools with
VCTools).

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
GVFS sets PublishAot=true in Directory.Build.props for every managed
project, which means publishing GVFS.exe and friends needs the native
C++ toolchain (link.exe, the Windows SDK, etc.) at build time. The
1ES win-x86_64-ado1es image used by the new release pipeline has no VS
installed so the Native AOT publish step fails with:

    error : Platform linker not found.

..per the documented NativeAOT prerequisites
(https://aka.ms/nativeaot-prerequisites), which call for VS 2022 with
the 'Desktop development with C++' workload.

Add a PowerShell script that ensures the C++ workload is present
before Build.bat runs, and wire it into release.yml. The script:

  * Bootstraps vswhere.exe from its GitHub release if the standard
    Program Files location does not have it (so it works on minimal
    images too).

  * Uses 'vswhere -requires NativeDesktop VCTools -requiresAny' to
    decide whether the workload is already installed on any existing
    Visual Studio install. If yes, exit 0 and let the build run.

  * If a Visual Studio install exists but lacks the workload, run
    its setup.exe modify to add the appropriate workload ID
    (NativeDesktop for full VS, VCTools for Build Tools).

  * If no Visual Studio is installed at all, download the VS Build
    Tools bootstrapper from aka.ms and install it with the VCTools
    workload from scratch.

  * Treat exit codes 0 and 3010 (success, reboot needed) as success
    for both modify and fresh install.

  * Re-run the same vswhere requirement check post-install to
    verify.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
VFS for Git's runtime and several of its unit tests P/Invoke into
ProjectedFSLib.dll (e.g. Microsoft.Windows.ProjFS.ProjFSNative
.PrjDoesNameContainWildCards via ActiveEnumeration). That DLL is only
present on disk when the 'Client-ProjFS' Windows optional feature is
enabled.

The 1ES win-x86_64-ado1es image used by the new release pipeline does
not have that feature enabled, so RunUnitTests.bat fails with:

    System.DllNotFoundException : Unable to load DLL
    'ProjectedFSLib.dll' or one of its dependencies. (0x8007007E)
       at Microsoft.Windows.ProjFS.ProjFSNative.PrjDoesNameContainWildCards(...)
       at GVFS.Platform.Windows.ActiveEnumeration.SaveFilter(...)
       at GVFS.UnitTests.Windows.Virtualization.ActiveEnumerationTests
          .CannotSetMoreThanOneFilter()

Add a small PowerShell helper that enables the feature, and run it as
a build prereq. The script is a no-op when the feature is already
enabled, so it's safe to keep across image refreshes.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Visual Studio's MSBuild for native C++ projects emits a
<binary>.exe.lastcodeanalysissucceeded marker file next to each
produced executable to record that PREfast / code analysis ran
successfully on it. Those marker files are pure build-time
bookkeeping with no runtime purpose, but the GVFS.Payload layout
step xcopies the entire native bin\x64\<Config>\ folder for each
of GitHooksLoader, GVFS.PostIndexChangedHook, GVFS.ReadObjectHook
and GVFS.VirtualFileSystemHook -- so the markers end up in the
Payload, get packaged into SetupGVFS.exe, and finally land on
end-user machines as e.g. GitHooksLoader.exe.lastcodeanalysissucceeded
in 'C:\Program Files\VFS for Git\'.

Extend the existing layout.bat cleanup block (which already strips
*.runtimeconfig.json, *.deps.json, and orphaned managed PDBs) to
also recursively delete *.lastcodeanalysissucceeded from the
output directory so they're never shipped.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Refactor the three EsrpCodeSigning@6 invocations in release.yml
(Payload binaries, FastFetch, installer) to use a shared
.azure-pipelines/esrp/sign.yml step template, modeled on the same
template in microsoft/git.

The template:

  * Forwards the per-call inputs (displayName, folderPath, pattern,
    inlineOperation) to EsrpCodeSigning@6.

  * Provides defaults for the ESRP connection parameters that point
    at the standard pipeline variables ($(esrpAppConnectionName),
    $(esrpClientId), etc.), so callers don't repeat them.

  * Runs an inline PowerShell@2 step right after each signing
    operation that removes the CodeSignSummary-<guid>.md report
    ESRP CLI drops into the signing folder. Without this, those
    .md files would otherwise end up packaged into SetupGVFS.exe
    (Payload), or uploaded as part of the FastFetch and Installer
    pipeline artifacts.

Net effect on release.yml is a small reduction in line count and,
more importantly, cleanup is no longer something a future caller
can forget to wire up.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
@mjcheetham
Copy link
Copy Markdown
Member Author

Updated following Copilot's comments.. here's the range-diff:

 1:  b20b5552 =  1:  b20b5552 Build: drop MicroBuild signing wiring
 2:  21364aeb =  2:  21364aeb GVFS.Installers: gate CreateInstaller on $(SkipCreateInstaller)
 3:  f4b46619 !  3:  f3af9cd7 .azure-pipelines: migrate release pipeline from MicroBuild to 1ESPT + ESRP
    @@ .azure-pipelines/release.yml
     +      - stage: release
     +        displayName: 'Release'
     +        dependsOn: [build]
    -+        condition: and(succeeded(), eq('${{ parameters.github }}', true))
    ++        # Only publish a draft GitHub release when ESRP signing was enabled in
    ++        # this run -- otherwise we would risk uploading unsigned installer
    ++        # binaries to the public release workflow.
    ++        condition: and(succeeded(), eq('${{ parameters.github }}', true), eq('${{ parameters.esrp }}', true))
     +        jobs:
     +          - job: github
     +            displayName: 'Publish GitHub release'
 4:  58460323 =  4:  2fab771a .azure-pipelines: publish debug symbols with GitHub Release
 5:  64fc64e0 =  5:  11a41476 .azure-pipelines: install pinned .NET SDK from global.json
 6:  56aec454 =  6:  d7a11ae7 Build.bat: fail when VS MSBuild is missing
 7:  b57f278d !  7:  8721e68c .azure-pipelines: install VS C++ workload before build
    @@ .azure-pipelines/release.yml: extends:
      ## .azure-pipelines/scripts/install-vs-cpp-workload.ps1 (new) ##
     @@
     +#
    -+# Ensure the Visual Studio "Desktop development with C++" workload is
    -+# installed on the build agent.
    ++# Ensure a Visual Studio 2022 (or newer) install with the "Desktop
    ++# development with C++" workload is present on the build agent.
     +#
    -+# .NET NativeAOT publishing (used by every managed VFS for Git project via
    -+# PublishAot=true in Directory.Build.props) requires the C++ build tools
    -+# from this workload at publish time.
    ++# .NET NativeAOT publishing (used by every product-facing managed VFS for
    ++# Git project via PublishAot=true in Directory.Build.props) requires the
    ++# C++ build tools from this workload at publish time. The native VFS
    ++# projects also build against the v143 toolset, which ships with VS 2022.
     +#
     +# This script handles three situations:
    -+#   1. The C++ workload is already present -> exit early.
    -+#   2. Visual Studio (any product) is installed but the C++ workload is
    -+#      missing -> modify the install to add it.
    -+#   3. No Visual Studio at all -> install VS Build Tools 2022 with the
    -+#      VC tools workload.
    ++#   1. A VS 2022+ install with the C++ workload is already present
    ++#      -> exit early.
    ++#   2. A VS 2022+ install (any product) is present but the C++ workload
    ++#      is missing -> modify that install to add it.
    ++#   3. No VS 2022+ install at all -> install VS Build Tools 2022 with
    ++#      the VC tools workload. (An older VS install, e.g. VS 2019, is
    ++#      ignored here -- we leave it alone and install VS 2022 alongside.)
     +#
     +# vswhere.exe is bootstrapped from GitHub if not already on disk.
     +#
    @@ .azure-pipelines/scripts/install-vs-cpp-workload.ps1 (new)
     +$vswhereDownloadUrl    = 'https://github.com/microsoft/vswhere/releases/latest/download/vswhere.exe'
     +$buildToolsDownloadUrl = 'https://aka.ms/vs/17/release/vs_BuildTools.exe'
     +
    ++# The native VFS projects build against the v143 toolset, which ships with
    ++# Visual Studio 2022 (product line 17.x). VS 2019 (16.x) carries v142 and
    ++# is not sufficient -- so all vswhere queries below are scoped to 17.0+.
    ++$minVsVersion = '[17.0,)'
    ++
     +# Either of these workloads provides the C++ build tools we need.
     +# Microsoft.VisualStudio.Workload.NativeDesktop = "Desktop development with C++" (Community/Pro/Enterprise).
     +# Microsoft.VisualStudio.Workload.VCTools       = "C++ build tools" (Build Tools).
    @@ .azure-pipelines/scripts/install-vs-cpp-workload.ps1 (new)
     +        [Parameter(Mandatory = $true)] [string]   $VswhereExe,
     +        [string[]]                                $RequiredWorkloads
     +    )
    -+    $vswhereArgs = @('-latest', '-prerelease', '-products', '*', '-format', 'json')
    ++    $vswhereArgs = @('-latest', '-prerelease', '-products', '*', '-version', $script:minVsVersion, '-format', 'json')
     +    if ($RequiredWorkloads -and $RequiredWorkloads.Count -gt 0) {
     +        $vswhereArgs += '-requires'
     +        $vswhereArgs += $RequiredWorkloads
    @@ .azure-pipelines/scripts/install-vs-cpp-workload.ps1 (new)
     +# --- Find any VS install (regardless of workloads) ---
     +$install = Find-VsInstall -VswhereExe $vswhereExe
     +
    -+# --- If no VS at all, install VS Build Tools with the VC workload ---
    ++# --- If no VS 2022+ install at all, install VS Build Tools 2022 with the VC workload ---
     +if (-not $install) {
    -+    Write-Host "No Visual Studio installation found; installing VS Build Tools 2022 with the C++ workload..."
    ++    Write-Host "No Visual Studio 2022 (or newer) installation found; installing VS Build Tools 2022 with the C++ workload..."
     +    $bootstrapper = Join-Path $env:TEMP 'vs_BuildTools.exe'
     +    Write-Host "Downloading VS Build Tools bootstrapper from $buildToolsDownloadUrl..."
     +    Invoke-WebRequest -Uri $buildToolsDownloadUrl -OutFile $bootstrapper -UseBasicParsing
 8:  c859c6fc !  8:  0f30e3db .azure-pipelines: enable Projected File System for unit tests
    @@ .azure-pipelines/scripts/enable-projfs.ps1 (new)
     +$result = Enable-WindowsOptionalFeature -Online -FeatureName $featureName -NoRestart -ErrorAction Stop
     +
     +if ($result.RestartNeeded) {
    -+    Write-Warning "Windows optional feature '$featureName' was enabled but a restart is required to take effect."
    ++    # The pipeline runs unit tests immediately after this script which P/Invoke
    ++    # into ProjectedFSLib.dll. If the OS reports a reboot is required to make
    ++    # the feature usable, the build agent is in an inconsistent state and the
    ++    # tests will fail unpredictably -- so fail fast here instead.
    ++    throw "Windows optional feature '$featureName' was enabled but a restart is required to take effect; failing the build."
     +} else {
     +    Write-Host "INFO: Windows optional feature '$featureName' is now enabled."
     +}
 9:  51e58dc9 =  9:  c21ed310 GVFS.Payload: drop VS code-analysis marker files from payload
10:  fe60e1c3 = 10:  6eaea92f .azure-pipelines: introduce esrp/sign.yml template

@mjcheetham
Copy link
Copy Markdown
Member Author

mjcheetham commented May 14, 2026

Run with the latest changes: https://dev.azure.com/mseng/1ES/_build/results?buildId=31501969

All green! I specifically ran with GitHub=true and ESRP=false to trigger the new condition we have added on release publishing.. it worked - step was skipped.

Comment thread .azure-pipelines/release.yml Outdated
Comment on lines +283 to +284
image: ubuntu-x86_64-ado1es
os: linux
image: win-x86_64-ado1es
os: windows
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change intentional? Windows runners tend to be slower than Linux runners...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to re-archive our PDB symbols since the artifact that gets uploaded/downloaded between jobs gets the _manifest/ and internal SBOM stuff added. We need to keep release jobs separate since 1ES policies restrict certain job types from being 'build' vs 'release'.

We want to publish as a ZIP and the ArchiveFiles@2 task requires zip, which isn't on the Ubuntu images (nor can we install it mid-pipeline for reasons above).

@mjcheetham mjcheetham merged commit a44f237 into master May 14, 2026
54 checks passed
@mjcheetham mjcheetham deleted the esrp branch May 14, 2026 15:14
@mjcheetham mjcheetham mentioned this pull request May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants