Skip to content

Install the macOS docker CLI from a Homebrew bottle instead of source-building it - #4661

Merged
paulmedynski merged 3 commits into
mainfrom
dev/paul/macos-bottles
Sep 9, 2026
Merged

Install the macOS docker CLI from a Homebrew bottle instead of source-building it#4661
paulmedynski merged 3 commits into
mainfrom
dev/paul/macos-bottles

Conversation

@paulmedynski

@paulmedynski paulmedynski commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Description

Homebrew has stopped publishing Intel macOS bottles, and the Azure Pipelines hosted macOS agents are Intel. Any formula whose current version has lost its Intel bottle is therefore compiled from source on our CI agents. This PR removes two such source builds from the macOS SQL Server setup step and bounds the step properly.

Scope note: this started as a Docker-only change. CI evidence (below) showed a second, larger instance of the same root cause in the same step, so the scope grew to cover it. The commits are separable if reviewers prefer them split.

1. Docker CLI — installed from a bottle instead of source-built

brew install docker compiles the CLI, and builds Go in order to do it. The sibling microsoft/mssql-rs repo measured this across 147 runs of the equivalent step: bottled path 29s median, 1% failure; source-build path 441s median (774s max), 36% failure. See microsoft/mssql-rs#500, which this borrows the approach from.

eng/pipelines/scripts/Install-DockerCli.macos.ps1 (new) installs the newest docker version that is bottled for Intel macOS, read from Homebrew's own OCI registry on ghcr.io:

  • Anonymous pull token → paginated tag list → newest version whose index carries a sequoia/sonoma/ventura bottle.
  • Bottles are content-addressed, so the download is verified against the SHA-256 digest the registry advertises rather than a checksum pinned here.
  • Extraction names the single member it wants (docker/<version>/bin/docker), so nothing else from an anonymous download reaches disk.
  • Nothing is fetched from outside Homebrew; the result is what brew install would have produced, with no hand-pinned version.

This replaces the brew install rather than acting as a fallback to it. A fallback that only runs during a Homebrew outage is untested precisely when it is needed; running it every time means a break surfaces immediately. The tradeoff is that we own this install path permanently.

The version search walks the entire tag list newest-first rather than a fixed window. homebrew/core/docker has 113 version tags and the newest Intel-bottled one is already at index 1 (29.7.2-1), because 29.8.0 ships only arm64_* and x86_64_linux. A manifest request costs ~0.22s, so a full walk is ~25s and only occurs when no Intel bottle exists at all.

2. sqlcmd — go-sqlcmd instead of mssql-tools18

The first CI run on this branch timed out, and the docker CLI was not the reason — it installed in 4.7s. The dominant cost was brew install mssql-tools18 at 750s (12.5 min): the microsoft/mssql-release tap pins an openssl@3 formula with no Intel bottle, so Homebrew compiled openssl (7,630 files, built in 12 minutes). That is the same root cause as the docker problem, in the same step.

mssql-tools18 is replaced by sqlcmd (go-sqlcmd) from homebrew-core — a single bottled Go binary with no openssl dependency. This also removes brew tap, brew trust, and brew update, the last of which the in-line comment justified solely as a prerequisite for brew trust.

The readiness probe deliberately stays on the host rather than moving into the container via docker exec: the tests connect from the host, so the probe must traverse the Colima port-forward and the container port mapping to prove what the tests actually depend on.

3. Step timeout

The step previously had no timeoutInMinutes at all and was bounded only by the 90-minute job timeout — which is why a slow install surfaced as an opaque job timeout with no diagnostics. It is now 40 minutes, sized from measurement: excluding the openssl build, the observed run still needed ~18 minutes to reach docker run (Colima boot 5.6 min; SQL image pull 9.6 min, 7 of which is extraction inside the VM), and SQL readiness polling can add up to 6 more.

Issues

None — proactive fix for a latent CI failure.

Testing

Ran CI-SqlClient to confirm macOS job succeeds.

…-building it

Homebrew ships no Intel macOS bottle for the current docker formula, so
'brew install docker' compiles the CLI and builds Go in order to do it. That
takes minutes and can exhaust the step budget. The macOS agents in the Azure
Pipelines hosted pool are Intel, so this affects every macOS test job.

Install-DockerCli.macos.ps1 installs the newest docker version that is bottled
for Intel macOS, read from Homebrew's own OCI registry on ghcr.io. Bottles are
content-addressed, so the download is verified against the digest the registry
advertises rather than a checksum pinned here. Nothing is fetched from outside
Homebrew, and the result is what 'brew install' would have produced.

This replaces the brew install rather than acting as a fallback to it, so the
path runs on every macOS job and a break surfaces immediately instead of only
during the next Homebrew outage.

colima still installs through brew: its bottle carries payloads outside bin/
and it depends on lima at runtime, so it cannot be installed by lifting a
single binary.

Also bounds the setup step with timeoutInMinutes, which it previously lacked
entirely - it was limited only by the 90 minute job timeout.
Copilot AI balanced review requested due to automatic review settings September 8, 2026 10:39
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Sep 8, 2026
@paulmedynski paulmedynski added this to the 7.1.0 milestone Sep 8, 2026
@paulmedynski paulmedynski added the Area\Engineering Use this for issues that are targeted for changes in the 'eng' folder or build systems. label Sep 8, 2026
@paulmedynski paulmedynski moved this from To triage to In progress in SqlClient Board Sep 8, 2026

Copilot AI left a comment

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.

🟡 Changes recommended

The fixed 15-version search window will eventually exclude the last available Intel bottle.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Replaces source-building Docker CLI on Intel macOS agents with direct installation from a verified Homebrew bottle.

Changes:

  • Adds OCI bottle discovery, verification, and extraction.
  • Integrates the installer with a 30-minute setup timeout.
  • Adds Pester coverage and test documentation.
File summaries
File Description
eng/pipelines/scripts/Install-DockerCli.macos.ps1 Implements bottle installation.
eng/pipelines/scripts/tests/Install-DockerCli.macos.Tests.ps1 Tests selection and integrity rules.
eng/pipelines/scripts/tests/README.md Documents the new tests.
eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml Uses the installer and adds a timeout.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread eng/pipelines/scripts/Install-DockerCli.macos.ps1 Outdated
The microsoft/mssql-release tap pins an openssl@3 formula that has no Intel
macOS bottle, so 'brew install mssql-tools18' compiled openssl from source:
7,630 files, built in 12 minutes. That was the single largest cost in the
macOS SQL setup step and, together with the Colima boot and the SQL image
pull, pushed the step past its timeout.

go-sqlcmd is a single bottled Go binary in homebrew-core with no openssl
dependency. Verified against v1.10.0 that it parses the flags this step
already uses (-S, -U, -P, -Q, -C, -No, -l), so the sqlcmd invocations are
unchanged.

Removing the tap also removes 'brew tap' and 'brew trust', and with them
'brew update', which the in-line comment justified solely as a prerequisite
for making 'brew trust' available.

The readiness check deliberately stays on the host rather than moving into
the container: the tests connect from the host, so the probe has to traverse
the Colima port-forward and the container port mapping to prove what the
tests actually depend on.

Raises the step timeout to 40 minutes. Excluding the openssl build, the
observed run still needed ~18 minutes before 'docker run', and SQL readiness
polling can add up to 6 more.
Copilot AI review requested due to automatic review settings September 8, 2026 11:37

Copilot AI left a comment

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.

🟡 Changes recommended

The bottle search will eventually age out, and the sqlcmd and timeout changes contradict the stated scope.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

eng/pipelines/scripts/Install-DockerCli.macos.ps1:49

  • This fixed 15-version window makes the installer expire if Homebrew continues publishing Docker releases without Intel bottles: after 15 newer releases, the last usable bottle still exists in the registry but is never inspected, so every macOS setup fails. Scan version tags until a matching Intel bottle is found (or use another durable lookup that cannot age out) rather than relying on the usable version always remaining in the newest few.
# Each version costs a registry round-trip and the answer is always in the
# newest few.
$MaxVersionsScanned = 15
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Addresses review feedback on PR #4661.

The 15-version scan window would have expired the workaround. Homebrew has
stopped publishing Intel macOS bottles, so ARM-only tags accumulate at the
head of the version list and the newest usable version sinks further down
with every docker release. Once it passed index 15 the script would have
failed even though a usable bottle remained.

Verified against the live registry: homebrew/core/docker has 113 version
tags, and the newest Intel-bottled one is already at index 1 (29.7.2-1)
because 29.8.0 publishes only arm64 macOS and linux bottles. At the observed
release cadence - 33 tags between 29.0.0 and 29.8.0 - the window would have
closed in roughly three months.

A manifest request costs ~0.2s, so walking the whole list is ~25s, and that
only happens when no Intel bottle exists at all, which is already a broken
state. The realistic degraded case costs a few seconds.

Adds a test covering a long run of arm64-only versions ahead of a usable one.
Confirmed load-bearing by reintroducing the cap: that test, and only that
test, fails.
Copilot AI review requested due to automatic review settings September 8, 2026 11:45

Copilot AI left a comment

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.

🔵 Needs a closer look

Registry pagination can construct invalid URLs, and the SQLCMD and timeout changes contradict the declared scope.

Review details

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

eng/pipelines/scripts/Install-DockerCli.macos.ps1:82

  • The pagination target is always prefixed with $Registry, but a registry Link target may already be absolute. In that case this produces a URL such as https://ghcr.iohttps://ghcr.io/..., so pagination fails and older bottled versions are never considered. Resolve the target against the current request URI so both relative and absolute links work; the existing tests currently provide no Link header and do not catch this path.

eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml:53

  • This also replaces mssql-tools18 with the Go implementation, while the PR description explicitly says mssql-tools18 remains installed through Homebrew and that the rest of setup is untouched. That is a separate toolchain change with compatibility implications; either keep the existing SQLCMD installation or update the stated scope and document/validate the migration.
      brew install sqlcmd

eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml:251

  • The PR description says this step will be limited to 30 minutes, but the implementation configures 40 minutes. Please align the configured timeout with the documented behavior (or update the description if 40 minutes is intentional).
    timeoutInMinutes: 40
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@paulmedynski
paulmedynski marked this pull request as ready for review September 8, 2026 13:29
@paulmedynski
paulmedynski requested a review from a team as a code owner September 8, 2026 13:29
@paulmedynski
paulmedynski enabled auto-merge (squash) September 8, 2026 13:29
@paulmedynski paulmedynski moved this from In progress to In review in SqlClient Board Sep 8, 2026
@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.45%. Comparing base (7054399) to head (59f9351).
⚠️ Report is 27 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4661      +/-   ##
==========================================
- Coverage   65.92%   64.45%   -1.47%     
==========================================
  Files         290      284       -6     
  Lines       44987    67980   +22993     
==========================================
+ Hits        29656    43817   +14161     
- Misses      15331    24163    +8832     
Flag Coverage Δ
CI-SqlClient ?
PR-SqlClient-Project 64.45% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cheenamalhotra

Copy link
Copy Markdown
Member

There's a failure in pipelines on macOS that may need attending to.

@paulmedynski

paulmedynski commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

The CI-SqlClient macOS failure is from SQL Server crashing inside the container. I've seen this a few times, so something to look into separately. The Ubuntu enclave failures (cancellations) are due to the requested image not existing in the AE pool - already fixed in #4515. These can be ignored here.

@paulmedynski
paulmedynski merged commit ce9f01b into main Sep 9, 2026
416 of 420 checks passed
@paulmedynski
paulmedynski deleted the dev/paul/macos-bottles branch September 9, 2026 00:36
@github-project-automation github-project-automation Bot moved this from In review to Done in SqlClient Board Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area\Engineering Use this for issues that are targeted for changes in the 'eng' folder or build systems.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants