Skip to content

Commit 9c1b401

Browse files
joperezraskpt
andauthored
Fix deprecated Homebrew cask DSL usage (#19965) (#20119)
Co-authored-by: James Newton-King <james@newtonking.com> (cherry picked from commit 12847d4) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> # Conflicts: # .github/workflows/homebrew-validate-release.yml # docs/release-process.md # eng/homebrew/README.md Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com>
1 parent bd4a8e2 commit 9c1b401

7 files changed

Lines changed: 96 additions & 39 deletions

File tree

‎docs/release-process.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The Aspire release process uses these main automation components:
1313
- Publishes Aspire CLI npm packages through ESRP/MicroBuild.
1414
- Promotes the build to the GA channel via darc.
1515
- Submits WinGet manifest PRs.
16-
- Optionally validates the Homebrew cask against the live GitHub release when `SkipHomebrewValidation=false` (cask version bumps themselves are submitted by upstream autobump; see [Installer channels](#installer-channels)).
16+
- Optionally validates the Homebrew cask against the live GitHub release when `SkipHomebrewValidation=false`, running `brew audit --cask --online`, explicit binary notarization verification, and a real `brew install`/`brew uninstall` cycle (cask version bumps themselves are submitted by upstream autobump; see [Installer channels](#installer-channels)).
1717
- Optionally publishes the signed VS Code extension to the Visual Studio Marketplace.
1818
- Dispatches the GitHub Actions workflow below as the `aspire-repo-bot` GitHub App and waits for it to complete.
1919
- Uploads `aspire-cli-*` archives from the source build's `BlobArtifacts` onto the GitHub Release as the `aspire-repo-bot`.
@@ -372,6 +372,7 @@ Azure DevOps release-publish-nuget.yml
372372
-> upload aspire-cli-* assets to the GitHub release
373373
-> dispatch update-nix-cli-flake.yml as aspire-repo-bot (stable releases)
374374
-> validate Homebrew cask against the live release (only when SkipHomebrewValidation=false)
375+
-> brew audit --cask --online + binary notarization verification + brew install/uninstall
375376
376377
GitHub release-github-tasks.yml
377378
-> create tag

‎eng/homebrew/README.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ cask URL points at validation time:
129129

130130
| Mode | Cask URL resolves? | Audit args | Used by |
131131
|---|---|---|---|
132-
| `LiveRelease` | Yes — points at a live GitHub release | `brew audit --cask --online --signing` + `brew install`/`brew uninstall` | `release-publish-nuget.yml` `HomebrewValidateJob`, after `PublishReleaseAssetsJob` uploads the archives |
133-
| `LiveArchives` | Not yet — release for `v#{version}` hasn't been published | `brew audit --cask --no-signing` (no `--online`) | `azure-pipelines.yml` Homebrew Cask job; `.github/workflows/tests.yml`; `dogfood.sh` PR validation |
132+
| `LiveRelease` | Yes — points at a live GitHub release | `brew audit --cask --online` + binary notarization verification + `brew install`/`brew uninstall` | `release-publish-nuget.yml` `HomebrewValidateJob`, after `PublishReleaseAssetsJob` uploads the archives |
133+
| `LiveArchives` | Not yet — release for `v#{version}` hasn't been published | `brew audit --cask` (no `--online`) | `azure-pipelines.yml` Homebrew Cask job; `.github/workflows/tests.yml`; `dogfood.sh` PR validation |
134134

135135
Common to both modes:
136136

@@ -146,8 +146,7 @@ audit methods (`audit_download`, `audit_signing`, `audit_rosetta`,
146146
release that doesn't exist yet at source-build time. Excluding them
147147
individually with `--except` is brittle — any new `--online`-gated audit
148148
method that touches the archive in a future brew release would silently
149-
start failing. `--no-signing` is also used because PR-build /
150-
source-build archives are unsigned CI artifacts.
149+
start failing.
151150

152151
The price is that LiveArchives doesn't run the `--online`-only checks:
153152
github/gitlab repo probes, homepage redirect/404 detection, livecheck

‎eng/homebrew/aspire.rb.template‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ cask "aspire" do
55
sha256 arm: "${SHA256_OSX_ARM64}",
66
intel: "${SHA256_OSX_X64}"
77

8-
url "https://github.com/microsoft/aspire/releases/download/v#{version}/aspire-cli-osx-#{arch}-#{version}.tar.gz",
9-
verified: "github.com/microsoft/aspire/"
8+
url "https://github.com/microsoft/aspire/releases/download/v#{version}/aspire-cli-osx-#{arch}-#{version}.tar.gz"
109
name "Aspire CLI"
1110
desc "CLI for building observable, production-ready distributed applications"
1211
homepage "https://aspire.dev/"
@@ -31,8 +30,8 @@ cask "aspire" do
3130
# The sidecar tells the CLI which install channel placed this binary, so
3231
# behaviors like self-update can route through `brew` instead of overwriting
3332
# the cask payload. See docs/specs/install-routes.md.
34-
postflight do
35-
File.write("#{staged_path}/.aspire-install.json", %Q({"source":"brew"}\n))
33+
postflight_steps do
34+
write_file ".aspire-install.json", "{\"source\":\"brew\"}\n", base: :staged_path
3635
end
3736

3837
# Empty by design. `~/.aspire/` is shared user state across all Aspire CLI

‎eng/homebrew/dogfood.sh‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,23 +134,16 @@ local_url_prefix = ARGV[1]
134134
lines = File.readlines(cask_path, chomp: true)
135135
rewritten = []
136136
137-
# The cask has a 2-line url stanza:
138-
# url "https://github.com/microsoft/aspire/...",
139-
# verified: "github.com/microsoft/aspire/"
140-
# For dogfood install, replace the url with a file:// path and drop the
141-
# trailing verified line. file:// URLs are exempt from `audit_missing_verified`
142-
# (brew's `file_url?` short-circuits the check), and dogfood.sh only runs
143-
# `brew install` — not `brew audit` — so audit_no_match never fires either.
137+
# For dogfood install, replace the release URL with a file:// path. The cask's
138+
# default URL verification behavior is sufficient for release downloads, and
139+
# file:// URLs do not require additional verification metadata.
144140
i = 0
145141
while i < lines.length
146142
line = lines[i]
147143
stripped = line.strip
148144
if stripped.start_with?('url "https://github.com/microsoft/aspire/releases/download/')
149145
rewritten << " url \"#{local_url_prefix}/aspire-cli-osx-\#{arch}-\#{version}.tar.gz\""
150146
i += 1
151-
if i < lines.length && lines[i].strip.start_with?('verified:')
152-
i += 1
153-
end
154147
next
155148
end
156149

‎eng/homebrew/validate-cask-artifact.sh‎

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ set -euo pipefail
77
#
88
# Validation modes:
99
# * LiveRelease — Validates against the live GitHub release for the cask's
10-
# version. Runs the full `brew audit --cask --online
11-
# --signing` (the same gauntlet Homebrew/homebrew-cask's
12-
# per-cask CI runs for a bump PR) plus a real
10+
# version. Runs the full `brew audit --cask --online`
11+
# plus explicit binary notarization verification (matching
12+
# Homebrew/homebrew-cask's signing audit) and a real
1313
# `brew install`/`brew uninstall` cycle. Used by the
1414
# release pipeline after `PublishReleaseAssetsJob` has
1515
# uploaded the aspire-cli-osx-* archives to the GitHub
1616
# release.
1717
# * LiveArchives — Validates against the cask file alone, without depending
1818
# on the cask URL resolving (the GitHub release for the
1919
# version-being-built doesn't exist yet at source-build
20-
# time). Runs `brew audit --cask --no-signing` — all
21-
# structural checks (style, syntax, naming, verified-vs-url
20+
# time). Runs `brew audit --cask` — all structural checks
21+
# (style, syntax, naming, verified-vs-url
2222
# consistency, version format, conflicts, depends_on
2323
# rules). Drops `--online` because several `audit_*`
2424
# methods (download, signing, rosetta, min_os) try to
@@ -160,9 +160,9 @@ echo "brew test-bot --only-tap-syntax succeeded."
160160

161161
echo ""
162162
# Match the audit arg set that Homebrew/homebrew-cask CI runs for an existing
163-
# cask bump: `brew audit --cask --online --signing <cask>`. (`--new` is added
164-
# upstream only when the cask is being submitted for the first time, which is
165-
# a human-driven path not covered by this pipeline.)
163+
# cask bump: `brew audit --cask --online <cask>`. (`--new` is added upstream
164+
# only when the cask is being submitted for the first time, which is a
165+
# human-driven path not covered by this pipeline.)
166166
#
167167
# In LiveRelease mode `--online` is brew's depth flag that enables the
168168
# network-requiring audit methods on top of the structural ones: archive
@@ -177,9 +177,6 @@ echo ""
177177
# fetches the cask URL). Excluding them individually with `--except` is
178178
# brittle because any new `--online`-gated audit method that touches the
179179
# archive in a future brew release would silently start failing.
180-
# `--no-signing` is also used because PR-build / source-build archives are
181-
# unsigned/ad-hoc-signed CI artifacts, not notarized release assets — even
182-
# if we could fetch them, `audit_signing` would reject them.
183180
#
184181
# What we lose in LiveArchives by dropping `--online`:
185182
# * github/gitlab repo probes (e.g. "is microsoft/aspire archived?")
@@ -188,9 +185,9 @@ echo ""
188185
# All of these run in LiveRelease in `HomebrewValidateJob` for every
189186
# released version, so a regression in any of them surfaces there.
190187
echo "Auditing cask via local tap..."
191-
audit_args=(--cask --online --signing)
188+
audit_args=(--cask --online)
192189
if [[ "$VALIDATION_MODE" == "LiveArchives" ]]; then
193-
audit_args=(--cask --no-signing)
190+
audit_args=(--cask)
194191
fi
195192
audit_args+=("$TAP_NAME/$CASK_NAME")
196193
audit_command="brew audit ${audit_args[*]}"
@@ -232,6 +229,24 @@ if [[ "$VALIDATION_MODE" == "LiveRelease" ]]; then
232229
exit 1
233230
fi
234231

232+
cask_version="$(awk -F'"' '/^[[:space:]]*version[[:space:]]+"/ { print $2; exit }' "$CASK_FILE")"
233+
installed_binary="$(brew --prefix)/Caskroom/$CASK_NAME/$cask_version/aspire"
234+
if [[ ! -f "$installed_binary" ]]; then
235+
echo "Error: installed Aspire binary not found at $installed_binary." >&2
236+
exit 1
237+
fi
238+
239+
if ! command -v codesign >/dev/null 2>&1; then
240+
echo "Error: codesign is required for LiveRelease binary notarization validation." >&2
241+
exit 1
242+
fi
243+
244+
# Homebrew no longer exposes --signing for `brew audit`. Because this cask
245+
# is installed from a temporary third-party tap, audit_signing does not run
246+
# automatically as it does for homebrew/cask. Preserve that release gate by
247+
# applying the same notarization requirement directly to the installed binary.
248+
codesign --verify -R=notarized --check-notarization "$installed_binary"
249+
235250
echo " Path: $(command -v aspire)"
236251
aspire_version="$(aspire --version 2>&1)"
237252
echo " Version: $aspire_version"

‎eng/pipelines/release-publish-nuget.yml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2369,7 +2369,8 @@ extends:
23692369
# defer_baseline_pr=true; PR creation is handled here).
23702370
#
23712371
# 4. HomebrewValidateJob — runs the full upstream-equivalent cask
2372-
# validation (`brew audit --cask --online --signing` + real
2372+
# validation (`brew audit --cask --online` + explicit binary
2373+
# notarization verification + real
23732374
# `brew install`/`brew uninstall`) against the cask generated by
23742375
# the source build, with the live GitHub release URL it points
23752376
# at. This is the moment when the URL is actually reachable —
@@ -2718,7 +2719,8 @@ extends:
27182719
DISPATCHED_RUN_URL: $(DispatchedRunUrl)
27192720
27202721
# Validate the generated Homebrew cask against the live GitHub
2721-
# release. Runs `brew audit --cask --online --signing` and a real
2722+
# release. Runs `brew audit --cask --online`, explicit binary
2723+
# notarization verification, and a real
27222724
# `brew install`/`brew uninstall` against the cask file produced by
27232725
# the source build. The cask URL points at
27242726
# github.com/microsoft/aspire/releases/download/v#{version}/..., so

‎tests/Aspire.Acquisition.Tests/Scripts/PRScriptInstallerModeTests.cs‎

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,13 @@ private static async Task<string> CreateWinGetInstallerArtifactAsync(string root
106106
return (manifestDir, archiveRoot);
107107
}
108108

109-
private static async Task<string> CreateMockHomebrewBinAsync(TestEnvironment env, int aspireExitCode)
109+
private static async Task<string> CreateMockHomebrewBinAsync(TestEnvironment env, int aspireExitCode, int codesignExitCode = 0)
110110
{
111111
var mockBinDir = Path.Combine(env.TempDirectory, "mock-homebrew-bin");
112112
var brewRepository = Path.Combine(env.TempDirectory, "brew-repository");
113113
var brewPrefix = Path.Combine(env.TempDirectory, "brew-prefix");
114114
var brewLog = Path.Combine(env.TempDirectory, "brew.log");
115+
var codesignLog = Path.Combine(env.TempDirectory, "codesign.log");
115116

116117
Directory.CreateDirectory(mockBinDir);
117118
Directory.CreateDirectory(brewRepository);
@@ -130,6 +131,8 @@ await File.WriteAllTextAsync(brewPath, $$"""
130131
exit {{aspireExitCode}}
131132
ASPIRE
132133
chmod +x "{{mockBinDir}}/aspire"
134+
mkdir -p "{{brewPrefix}}/Caskroom/aspire/13.3.0"
135+
cp "{{mockBinDir}}/aspire" "{{brewPrefix}}/Caskroom/aspire/13.3.0/aspire"
133136
}
134137
135138
case "${1:-}" in
@@ -179,6 +182,7 @@ exit 0
179182
;;
180183
uninstall)
181184
rm -f "{{mockBinDir}}/aspire"
185+
rm -rf "{{brewPrefix}}/Caskroom/aspire"
182186
exit 0
183187
;;
184188
untap)
@@ -199,6 +203,14 @@ exit 0
199203
""");
200204
FileHelper.MakeExecutable(curlPath);
201205

206+
var codesignPath = Path.Combine(mockBinDir, "codesign");
207+
await File.WriteAllTextAsync(codesignPath, $$"""
208+
#!/usr/bin/env bash
209+
echo "$*" >> "{{codesignLog}}"
210+
exit {{codesignExitCode}}
211+
""");
212+
FileHelper.MakeExecutable(codesignPath);
213+
202214
return mockBinDir;
203215
}
204216

@@ -614,6 +626,38 @@ public async Task Bash_PrepareHomebrewCask_FailedVerification_UninstallsCask()
614626

615627
Assert.NotEqual(0, result.ExitCode);
616628
var brewLog = await File.ReadAllTextAsync(Path.Combine(env.TempDirectory, "brew.log"));
629+
Assert.Contains("audit --cask --online local/aspire/aspire", brewLog);
630+
Assert.DoesNotContain("--signing", brewLog);
631+
Assert.DoesNotContain("--no-signing", brewLog);
632+
Assert.Contains("uninstall --cask local/aspire-test/aspire", brewLog);
633+
var codesignLog = await File.ReadAllTextAsync(Path.Combine(env.TempDirectory, "codesign.log"));
634+
Assert.Contains("--verify -R=notarized --check-notarization", codesignLog);
635+
Assert.Contains("Caskroom/aspire/13.3.0/aspire", codesignLog);
636+
}
637+
638+
[Fact]
639+
[RequiresTools(["ruby"])]
640+
[SkipOnPlatform(TestPlatforms.Windows, "Bash script tests require bash shell")]
641+
public async Task Bash_PrepareHomebrewCask_FailedSigningValidation_UninstallsCask()
642+
{
643+
using var env = new TestEnvironment();
644+
var archiveRoot = Path.Combine(env.TempDirectory, "archives");
645+
await CreateFakeHomebrewArchivesAsync(archiveRoot);
646+
var mockBinDir = await CreateMockHomebrewBinAsync(env, aspireExitCode: 0, codesignExitCode: 42);
647+
using var cmd = new ScriptToolCommand("eng/homebrew/prepare-cask-artifact.sh", env, _testOutput);
648+
cmd.WithEnvironmentVariable("PATH", $"{mockBinDir}{Path.PathSeparator}/usr/bin:/bin:/usr/sbin:/sbin");
649+
650+
var result = await cmd.ExecuteAsync(
651+
"--version", "13.3.0",
652+
"--channel", "stable",
653+
"--archive-root", archiveRoot,
654+
"--output-dir", Path.Combine(env.TempDirectory, "homebrew-output"),
655+
"--validation-mode", "LiveRelease");
656+
657+
Assert.NotEqual(0, result.ExitCode);
658+
var codesignLog = await File.ReadAllTextAsync(Path.Combine(env.TempDirectory, "codesign.log"));
659+
Assert.Contains("--verify -R=notarized --check-notarization", codesignLog);
660+
var brewLog = await File.ReadAllTextAsync(Path.Combine(env.TempDirectory, "brew.log"));
617661
Assert.Contains("uninstall --cask local/aspire-test/aspire", brewLog);
618662
}
619663

@@ -642,22 +686,26 @@ public async Task Bash_PrepareHomebrewCask_LiveArchives_GeneratesCaskWithArchive
642686
var cask = await File.ReadAllTextAsync(Path.Combine(outputDir, "aspire.rb"));
643687
Assert.Contains("version \"13.3.0\"", cask);
644688
Assert.Contains("https://github.com/microsoft/aspire/releases/download/v#{version}/aspire-cli-osx-#{arch}-#{version}.tar.gz", cask);
645-
Assert.Contains("verified: \"github.com/microsoft/aspire/\"", cask);
689+
Assert.DoesNotContain("verified:", cask);
690+
Assert.Contains("postflight_steps do", cask);
691+
Assert.Contains("write_file \".aspire-install.json\", \"{\\\"source\\\":\\\"brew\\\"}\\n\", base: :staged_path", cask);
646692
Assert.Contains((await GetSha256HexAsync(Path.Combine(archiveRoot, "aspire-cli-osx-arm64-13.3.0.tar.gz"))).ToLowerInvariant(), cask);
647693
Assert.Contains((await GetSha256HexAsync(Path.Combine(archiveRoot, "aspire-cli-osx-x64-13.3.0.tar.gz"))).ToLowerInvariant(), cask);
648694
Assert.DoesNotContain("${", cask);
649695
Assert.True(File.Exists(Path.Combine(outputDir, "dogfood.sh")));
650696

651-
// LiveArchives mode must drop `--online` and add `--no-signing` to `brew audit`,
652-
// because the cask URL points at a github.com/microsoft/aspire release that does
697+
// LiveArchives mode must drop `--online` from `brew audit`, because the cask URL
698+
// points at a github.com/microsoft/aspire release that does
653699
// not exist yet at source-build time. See the `audit_args` block in
654700
// eng/homebrew/validate-cask-artifact.sh for the full rationale; this assertion
655701
// locks the contract so a regression in the mode selection surfaces here rather
656702
// than silently failing (or worse, silently succeeding) in the source-build
657703
// prepare stage.
658704
var brewLog = await File.ReadAllTextAsync(Path.Combine(env.TempDirectory, "brew.log"));
659-
Assert.Contains("audit --cask --no-signing local/aspire/aspire", brewLog);
705+
Assert.Contains("audit --cask local/aspire/aspire", brewLog);
660706
Assert.DoesNotContain("audit --cask --online", brewLog);
707+
Assert.DoesNotContain("--signing", brewLog);
708+
Assert.DoesNotContain("--no-signing", brewLog);
661709
}
662710

663711
[Fact]

0 commit comments

Comments
 (0)