From d1ba72f6a4399b7cf54a31ec32803dd93142185a Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Thu, 9 Jul 2026 22:54:39 +0200 Subject: [PATCH 01/12] ci(release): remove Homebrew tap dispatch job (tap now self-updates) The update-homebrew job sent a repository_dispatch to microsoft/homebrew-apm using a fine-grained PAT (GH_PKG_PAT). That token hit the enterprise policy forbidding PAT lifetimes > 8 days and deterministically broke the Homebrew step every release. The tap now self-updates via its own poll-upstream-release.yml: a daily job that reads the latest microsoft/apm release with the tap's built-in GITHUB_TOKEN and commits the formula bump directly -- no cross-repo trigger, no PAT, nothing to rotate. This removes the sender job entirely, retiring the push model. Note: GH_PKG_PAT remains referenced only by the update-scoop job, which is disabled (if: false), so the secret is not orphaned by this change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 65 ----------------------------- 1 file changed, 65 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 254a1f962..302f340ce 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -741,71 +741,6 @@ jobs: with: verbose: true - # Update Homebrew formula (only stable releases from public repo) - update-homebrew: - name: Update Homebrew Formula - runs-on: ubuntu-latest - needs: [build-and-test, build-and-validate-macos-intel, build-and-validate-macos-arm, integration-tests, release-validation, create-release, publish-pypi] - if: github.ref_type == 'tag' && needs.create-release.outputs.is_private_repo != 'true' && needs.create-release.outputs.is_prerelease != 'true' - permissions: - contents: read - - steps: - - name: Extract SHA256 checksums from GitHub release - id: checksums - run: | - # Download the SHA256 checksum files from the GitHub release - RELEASE_TAG="${{ github.ref_name }}" - - echo "Downloading checksums for release $RELEASE_TAG" - - # Download checksum files directly from the release - curl -L -o apm-darwin-arm64.tar.gz.sha256 \ - "https://github.com/${{ github.repository }}/releases/download/$RELEASE_TAG/apm-darwin-arm64.tar.gz.sha256" - curl -L -o apm-darwin-x86_64.tar.gz.sha256 \ - "https://github.com/${{ github.repository }}/releases/download/$RELEASE_TAG/apm-darwin-x86_64.tar.gz.sha256" - curl -L -o apm-linux-x86_64.tar.gz.sha256 \ - "https://github.com/${{ github.repository }}/releases/download/$RELEASE_TAG/apm-linux-x86_64.tar.gz.sha256" - curl -L -o apm-linux-arm64.tar.gz.sha256 \ - "https://github.com/${{ github.repository }}/releases/download/$RELEASE_TAG/apm-linux-arm64.tar.gz.sha256" - - # Extract SHA256 checksums - DARWIN_ARM64_SHA=$(cat apm-darwin-arm64.tar.gz.sha256 | cut -d' ' -f1) - DARWIN_X86_64_SHA=$(cat apm-darwin-x86_64.tar.gz.sha256 | cut -d' ' -f1) - LINUX_X86_64_SHA=$(cat apm-linux-x86_64.tar.gz.sha256 | cut -d' ' -f1) - LINUX_ARM64_SHA=$(cat apm-linux-arm64.tar.gz.sha256 | cut -d' ' -f1) - - echo "darwin-arm64-sha=$DARWIN_ARM64_SHA" >> $GITHUB_OUTPUT - echo "darwin-x86_64-sha=$DARWIN_X86_64_SHA" >> $GITHUB_OUTPUT - echo "linux-x86_64-sha=$LINUX_X86_64_SHA" >> $GITHUB_OUTPUT - echo "linux-arm64-sha=$LINUX_ARM64_SHA" >> $GITHUB_OUTPUT - - echo "Darwin ARM64 SHA: $DARWIN_ARM64_SHA" - echo "Darwin x86_64 SHA: $DARWIN_X86_64_SHA" - echo "Linux x86_64 SHA: $LINUX_X86_64_SHA" - echo "Linux ARM64 SHA: $LINUX_ARM64_SHA" - - - name: Trigger Homebrew tap repository update - uses: peter-evans/repository-dispatch@v3 - with: - token: ${{ secrets.GH_PKG_PAT }} - repository: microsoft/homebrew-apm - event-type: formula-update - client-payload: | - { - "release": { - "version": "${{ github.ref_name }}", - "tag": "${{ github.ref_name }}", - "repository": "${{ github.repository }}" - }, - "checksums": { - "darwin_arm64": "${{ steps.checksums.outputs.darwin-arm64-sha }}", - "darwin_x86_64": "${{ steps.checksums.outputs.darwin-x86_64-sha }}", - "linux_x86_64": "${{ steps.checksums.outputs.linux-x86_64-sha }}", - "linux_arm64": "${{ steps.checksums.outputs.linux-arm64-sha }}" - } - } - # Update Scoop bucket (only stable releases from public repo) update-scoop: name: Update Scoop Bucket From 4f70450e45f168f67ac078dab4b21663ea7fe7e7 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 00:10:10 +0200 Subject: [PATCH 02/12] docs(release): document Homebrew self-update flow Keep the release architecture and contributor guide aligned with the removal of the obsolete cross-repository dispatch. The tap now polls public releases using its repository-scoped GITHUB_TOKEN. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .apm/instructions/cicd.instructions.md | 3 ++- .github/instructions/cicd.instructions.md | 3 ++- docs/src/content/docs/contributing/integration-testing.md | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.apm/instructions/cicd.instructions.md b/.apm/instructions/cicd.instructions.md index d98c27884..e64c79f81 100644 --- a/.apm/instructions/cicd.instructions.md +++ b/.apm/instructions/cicd.instructions.md @@ -80,7 +80,8 @@ integration suite runs only at merge time via GitHub Merge Queue ## Release Flow Dependencies - **PR workflow**: Tier 1 only - ci.yml (build-and-test, Linux-only) provides fast feedback. Tier 2 does not run until enqueued. - **Merge queue workflow**: ci.yml (Tier 1 against tentative merge ref) + ci-integration.yml (Tier 2: build -> smoke-test -> integration-tests -> release-validation). Queue auto-merges on success; ejects on failure. -- **Push/Release workflow (Linux + Windows)**: build-and-test -> integration-tests -> release-validation -> create-release -> publish-pypi -> update-homebrew (gh-aw-compat runs in parallel, informational) +- **Push/Release workflow (Linux + Windows)**: build-and-test -> integration-tests -> release-validation -> create-release -> publish-pypi (gh-aw-compat runs in parallel, informational) +- **Homebrew distribution**: `microsoft/homebrew-apm` polls the latest public APM release and updates its formula with its own `GITHUB_TOKEN`; this workflow must not send a cross-repository dispatch or hold a tap credential. - **Push/Release workflow (macOS Intel)**: build-and-validate-macos-intel (root node: unit tests + build always + conditional integration/release-validation) -> create-release - **Push/Release workflow (macOS ARM)**: build-and-validate-macos-arm (root node, tag/schedule/dispatch only; all phases run) -> create-release - **Tag Triggers**: Only `v*.*.*` tags trigger full release pipeline diff --git a/.github/instructions/cicd.instructions.md b/.github/instructions/cicd.instructions.md index d98c27884..e64c79f81 100644 --- a/.github/instructions/cicd.instructions.md +++ b/.github/instructions/cicd.instructions.md @@ -80,7 +80,8 @@ integration suite runs only at merge time via GitHub Merge Queue ## Release Flow Dependencies - **PR workflow**: Tier 1 only - ci.yml (build-and-test, Linux-only) provides fast feedback. Tier 2 does not run until enqueued. - **Merge queue workflow**: ci.yml (Tier 1 against tentative merge ref) + ci-integration.yml (Tier 2: build -> smoke-test -> integration-tests -> release-validation). Queue auto-merges on success; ejects on failure. -- **Push/Release workflow (Linux + Windows)**: build-and-test -> integration-tests -> release-validation -> create-release -> publish-pypi -> update-homebrew (gh-aw-compat runs in parallel, informational) +- **Push/Release workflow (Linux + Windows)**: build-and-test -> integration-tests -> release-validation -> create-release -> publish-pypi (gh-aw-compat runs in parallel, informational) +- **Homebrew distribution**: `microsoft/homebrew-apm` polls the latest public APM release and updates its formula with its own `GITHUB_TOKEN`; this workflow must not send a cross-repository dispatch or hold a tap credential. - **Push/Release workflow (macOS Intel)**: build-and-validate-macos-intel (root node: unit tests + build always + conditional integration/release-validation) -> create-release - **Push/Release workflow (macOS ARM)**: build-and-validate-macos-arm (root node, tag/schedule/dispatch only; all phases run) -> create-release - **Tag Triggers**: Only `v*.*.*` tags trigger full release pipeline diff --git a/docs/src/content/docs/contributing/integration-testing.md b/docs/src/content/docs/contributing/integration-testing.md index f09d80d5c..80e921989 100644 --- a/docs/src/content/docs/contributing/integration-testing.md +++ b/docs/src/content/docs/contributing/integration-testing.md @@ -159,10 +159,11 @@ The workflow ensures quality gates at each step: 3. **release-validation** job - Final shipped-binary validation 4. **create-release** job - GitHub release creation 5. **publish-pypi** job - PyPI package publication -6. **update-homebrew** job - Homebrew formula update Each stage must succeed before proceeding to the next, ensuring only fully validated releases reach users. +The Homebrew tap updates independently: `microsoft/homebrew-apm` polls the latest public APM release and commits formula updates with its own repository-scoped `GITHUB_TOKEN`. The release pipeline does not hold a cross-repository Homebrew credential. + ### Test Matrix Promotion integration tests run on: From e8bd0ae21329eb2e410f06aa0ac7fc6d84270d5e Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 00:17:56 +0200 Subject: [PATCH 03/12] chore(lock): refresh CI instruction integrity Record the updated managed CI/CD instruction hash so apm audit --ci verifies the release documentation mirror. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apm.lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm.lock.yaml b/apm.lock.yaml index c168177a2..d077bc39e 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -683,7 +683,7 @@ local_deployed_file_hashes: .github/agents/spec-tag-architect.agent.md: sha256:82907265c5e7cf1ac61ad96866fa7c5683b69c8f09b7a4c5f3cc241acc9568ca .github/agents/supply-chain-security-expert.agent.md: sha256:8fb8cc426d6af17ba084a28b3f026c2b475b62e3ca63ed2f88b83bd823f877af .github/instructions/changelog.instructions.md: sha256:1e51ec4c74e847967962bd279dc4c6e582c5d3578490b3c28d5f3acd3e05f73e - .github/instructions/cicd.instructions.md: sha256:9c0fafc74f743aa97e5adba2168d66c9e3a327b135065e3b804bdbb5f04cda5d + .github/instructions/cicd.instructions.md: sha256:08d87b7d635761cb41deb8fc71d5d83f54678de463db484afb16d2d4f8713ecb .github/instructions/cli.instructions.md: sha256:8e39e8d5047ce88575cb02f87c2bcede584dfef258bd86f7466c7badf136541a .github/instructions/doc-sync.instructions.md: sha256:bb3816254f8df6bffc6faacd556871f36903e9d7f348982f1e2de0339384c696 .github/instructions/encoding.instructions.md: sha256:93db7377dc896f6efecf2c5d8c5d89255a555562f468d034d64c42edd5cf46d5 From 7a1d21d57aef93e04dc7dcb6f925cd5c029f52f3 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 02:47:28 +0200 Subject: [PATCH 04/12] test(release): guard Homebrew ownership boundary Execute a hermetic tap-update model against local release metadata and fail if the APM release workflow restores a Homebrew credential, repository dispatch, or PR-auth path. This addresses the review panel's empirical coverage and documentation follow-ups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .apm/instructions/cicd.instructions.md | 1 + .github/instructions/cicd.instructions.md | 1 + CHANGELOG.md | 5 + apm.lock.yaml | 2 +- .../docs/contributing/integration-testing.md | 5 +- .../release_homebrew_update_harness.py | 73 +++++++++++++ .../test_release_homebrew_update.py | 103 ++++++++++++++++++ 7 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 tests/integration/release_homebrew_update_harness.py create mode 100644 tests/integration/test_release_homebrew_update.py diff --git a/.apm/instructions/cicd.instructions.md b/.apm/instructions/cicd.instructions.md index e64c79f81..fa94c62c4 100644 --- a/.apm/instructions/cicd.instructions.md +++ b/.apm/instructions/cicd.instructions.md @@ -82,6 +82,7 @@ integration suite runs only at merge time via GitHub Merge Queue - **Merge queue workflow**: ci.yml (Tier 1 against tentative merge ref) + ci-integration.yml (Tier 2: build -> smoke-test -> integration-tests -> release-validation). Queue auto-merges on success; ejects on failure. - **Push/Release workflow (Linux + Windows)**: build-and-test -> integration-tests -> release-validation -> create-release -> publish-pypi (gh-aw-compat runs in parallel, informational) - **Homebrew distribution**: `microsoft/homebrew-apm` polls the latest public APM release and updates its formula with its own `GITHUB_TOKEN`; this workflow must not send a cross-repository dispatch or hold a tap credential. +- **Scoop placeholder**: `update-scoop` remains disabled pending downstream setup; it must not be enabled with the current cross-repository PAT dispatch design. - **Push/Release workflow (macOS Intel)**: build-and-validate-macos-intel (root node: unit tests + build always + conditional integration/release-validation) -> create-release - **Push/Release workflow (macOS ARM)**: build-and-validate-macos-arm (root node, tag/schedule/dispatch only; all phases run) -> create-release - **Tag Triggers**: Only `v*.*.*` tags trigger full release pipeline diff --git a/.github/instructions/cicd.instructions.md b/.github/instructions/cicd.instructions.md index e64c79f81..fa94c62c4 100644 --- a/.github/instructions/cicd.instructions.md +++ b/.github/instructions/cicd.instructions.md @@ -82,6 +82,7 @@ integration suite runs only at merge time via GitHub Merge Queue - **Merge queue workflow**: ci.yml (Tier 1 against tentative merge ref) + ci-integration.yml (Tier 2: build -> smoke-test -> integration-tests -> release-validation). Queue auto-merges on success; ejects on failure. - **Push/Release workflow (Linux + Windows)**: build-and-test -> integration-tests -> release-validation -> create-release -> publish-pypi (gh-aw-compat runs in parallel, informational) - **Homebrew distribution**: `microsoft/homebrew-apm` polls the latest public APM release and updates its formula with its own `GITHUB_TOKEN`; this workflow must not send a cross-repository dispatch or hold a tap credential. +- **Scoop placeholder**: `update-scoop` remains disabled pending downstream setup; it must not be enabled with the current cross-repository PAT dispatch design. - **Push/Release workflow (macOS Intel)**: build-and-validate-macos-intel (root node: unit tests + build always + conditional integration/release-validation) -> create-release - **Push/Release workflow (macOS ARM)**: build-and-validate-macos-arm (root node, tag/schedule/dispatch only; all phases run) -> create-release - **Tag Triggers**: Only `v*.*.*` tags trigger full release pipeline diff --git a/CHANGELOG.md b/CHANGELOG.md index d58656897..84abab735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Homebrew formula updates now run independently in the tap repository, removing + its cross-repository credential from the release pipeline. (#2088) + ### Fixed - `apm install host/org/repo/subpath#ref` on an unrecognised self-hosted FQDN diff --git a/apm.lock.yaml b/apm.lock.yaml index d077bc39e..017a9d1b2 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -683,7 +683,7 @@ local_deployed_file_hashes: .github/agents/spec-tag-architect.agent.md: sha256:82907265c5e7cf1ac61ad96866fa7c5683b69c8f09b7a4c5f3cc241acc9568ca .github/agents/supply-chain-security-expert.agent.md: sha256:8fb8cc426d6af17ba084a28b3f026c2b475b62e3ca63ed2f88b83bd823f877af .github/instructions/changelog.instructions.md: sha256:1e51ec4c74e847967962bd279dc4c6e582c5d3578490b3c28d5f3acd3e05f73e - .github/instructions/cicd.instructions.md: sha256:08d87b7d635761cb41deb8fc71d5d83f54678de463db484afb16d2d4f8713ecb + .github/instructions/cicd.instructions.md: sha256:ade86dfad284da18e82b8295859f1a61e759e588248ad163d42d534677f7a438 .github/instructions/cli.instructions.md: sha256:8e39e8d5047ce88575cb02f87c2bcede584dfef258bd86f7466c7badf136541a .github/instructions/doc-sync.instructions.md: sha256:bb3816254f8df6bffc6faacd556871f36903e9d7f348982f1e2de0339384c696 .github/instructions/encoding.instructions.md: sha256:93db7377dc896f6efecf2c5d8c5d89255a555562f468d034d64c42edd5cf46d5 diff --git a/docs/src/content/docs/contributing/integration-testing.md b/docs/src/content/docs/contributing/integration-testing.md index 80e921989..ee3113dee 100644 --- a/docs/src/content/docs/contributing/integration-testing.md +++ b/docs/src/content/docs/contributing/integration-testing.md @@ -129,7 +129,6 @@ environment end-to-end; for local iteration prefer the direct 3. **E2E golden scenario tests** (using built binaries) 4. Create GitHub Release 5. Publish to PyPI -6. Update Homebrew Formula **Manual workflow dispatch:** - Test builds (uploads as workflow artifacts) @@ -197,7 +196,7 @@ Promotion integration tests run on: - ✅ Parameter substitution works correctly - ✅ MCP integration functions (GitHub tools) - ✅ Binary artifacts work across platforms -- ✅ Release pipeline integrity (GitHub Release → PyPI → Homebrew) +- ✅ Release pipeline integrity (GitHub Release → PyPI) ## Benefits @@ -220,7 +219,7 @@ Promotion integration tests run on: - Guarantees shipped releases work end-to-end - Users can trust the README golden scenario - Cross-platform binary verification -- Automatic Homebrew formula updates +- Independent Homebrew formula updates through tap polling ## Debugging Test Failures diff --git a/tests/integration/release_homebrew_update_harness.py b/tests/integration/release_homebrew_update_harness.py new file mode 100644 index 000000000..ded209234 --- /dev/null +++ b/tests/integration/release_homebrew_update_harness.py @@ -0,0 +1,73 @@ +"""Structural release guard plus a hermetic model of the tap's local write.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from pathlib import Path + +import yaml + + +def _assert_release_workflow_has_no_homebrew_push(workflow_path: Path) -> None: + workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8")) + for job_name, job in workflow.get("jobs", {}).items(): + rendered = json.dumps(job) + if "homebrew" not in f"{job_name} {rendered}".lower(): + continue + forbidden = ("GH_PKG_PAT", "repository-dispatch", "repository_dispatch", "gh pr create") + found = [token for token in forbidden if token in rendered] + if re.search(r"(?:secrets\.[A-Z0-9_]+|github\.token)", rendered, re.IGNORECASE): + found.append("workflow token") + if found: + raise RuntimeError( + f"release workflow restores a Homebrew push/auth path: {', '.join(found)}" + ) + + +def _replace_assignment(formula: str, key: str, value: str) -> str: + updated, count = re.subn( + rf'(?m)^ {key} "[^"]*"$', + f' {key} "{value}"', + formula, + ) + if count != 1: + raise RuntimeError(f"formula must contain exactly one {key} assignment") + return updated + + +def update_tap(workflow_path: Path, release_path: Path, tap_repo: Path) -> None: + """Model the tap's repository-local formula commit from release metadata.""" + _assert_release_workflow_has_no_homebrew_push(workflow_path) + + release = json.loads(release_path.read_text(encoding="utf-8")) + formula_path = tap_repo / "Formula" / "apm.rb" + formula = formula_path.read_text(encoding="utf-8") + formula = _replace_assignment(formula, "version", release["tag_name"].removeprefix("v")) + formula = _replace_assignment(formula, "url", release["asset_url"]) + formula = _replace_assignment(formula, "sha256", release["sha256"]) + formula_path.write_text(formula, encoding="utf-8") + + subprocess.run(["git", "add", str(formula_path)], cwd=tap_repo, check=True) + subprocess.run( + ["git", "commit", "-m", f"Update APM to {release['tag_name']}"], + cwd=tap_repo, + check=True, + capture_output=True, + text=True, + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--workflow", type=Path, required=True) + parser.add_argument("--release", type=Path, required=True) + parser.add_argument("--tap-repo", type=Path, required=True) + args = parser.parse_args() + update_tap(args.workflow, args.release, args.tap_repo) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/test_release_homebrew_update.py b/tests/integration/test_release_homebrew_update.py new file mode 100644 index 000000000..0c0bd89b0 --- /dev/null +++ b/tests/integration/test_release_homebrew_update.py @@ -0,0 +1,103 @@ +"""Regression coverage for the Homebrew release ownership boundary.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path +from urllib.parse import urlparse + + +def _git(repo: Path, *args: str) -> str: + result = subprocess.run( + ["git", *args], + cwd=repo, + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + + +def test_homebrew_update_commits_without_cross_repo_auth(tmp_path: Path) -> None: + """Run a local tap commit after guarding the release workflow auth boundary.""" + repo_root = Path(__file__).parents[2] + tap_repo = tmp_path / "homebrew-apm" + formula_dir = tap_repo / "Formula" + formula_dir.mkdir(parents=True) + formula = formula_dir / "apm.rb" + formula.write_text( + "class Apm < Formula\n" + ' version "0.0.1"\n' + ' url "https://example.invalid/apm-v0.0.1.tar.gz"\n' + f' sha256 "{"0" * 64}"\n' + "end\n", + encoding="utf-8", + ) + _git(tap_repo, "init", "-b", "main") + _git(tap_repo, "config", "user.name", "Homebrew update harness") + _git(tap_repo, "config", "user.email", "harness@example.invalid") + _git(tap_repo, "add", "Formula/apm.rb") + _git(tap_repo, "commit", "-m", "Initial formula") + + release = tmp_path / "release.json" + release.write_text( + json.dumps( + { + "tag_name": "v9.8.7", + "asset_url": "https://github.com/microsoft/apm/releases/download/v9.8.7/apm.tar.gz", + "sha256": "a" * 64, + } + ), + encoding="utf-8", + ) + + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + gh_marker = tmp_path / "gh-was-called" + fake_gh = fake_bin / "gh" + fake_gh.write_text( + f"#!/bin/sh\nprintf called > {gh_marker}\nexit 97\n", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + + env = os.environ.copy() + env.pop("GH_PKG_PAT", None) + env.pop("GH_TOKEN", None) + env["PATH"] = f"{fake_bin}{os.pathsep}{env['PATH']}" + subprocess.run( + [ + sys.executable, + str(Path(__file__).with_name("release_homebrew_update_harness.py")), + "--workflow", + str(repo_root / ".github" / "workflows" / "build-release.yml"), + "--release", + str(release), + "--tap-repo", + str(tap_repo), + ], + cwd=repo_root, + env=env, + check=True, + ) + + updated = formula.read_text(encoding="utf-8") + assignments = { + line.strip().split(" ", 1)[0]: line.strip().split('"')[1] + for line in updated.splitlines() + if line.strip().startswith(("version ", "url ", "sha256 ")) + } + assert assignments["version"] == "9.8.7" + parsed = urlparse(assignments["url"]) + assert (parsed.scheme, parsed.hostname, parsed.path) == ( + "https", + "github.com", + "/microsoft/apm/releases/download/v9.8.7/apm.tar.gz", + ) + assert assignments["sha256"] == "a" * 64 + assert _git(tap_repo, "rev-list", "--count", "HEAD") == "2" + assert _git(tap_repo, "log", "-1", "--pretty=%s") == "Update APM to v9.8.7" + assert not gh_marker.exists() From 6cbf7cf42bc70fdd7273513088c42cf1bb383733 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 02:54:26 +0200 Subject: [PATCH 05/12] ci(release): remove dormant dispatch credential Remove the disabled Scoop dispatch that retained the same cross-repository PAT pattern and escape harness assignment keys. This folds the final auth, security, and architecture panel findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 39 ------------------- .../release_homebrew_update_harness.py | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 302f340ce..dabebc396 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -740,42 +740,3 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: verbose: true - - # Update Scoop bucket (only stable releases from public repo) - update-scoop: - name: Update Scoop Bucket - runs-on: ubuntu-latest - needs: [build-and-test, build-and-validate-macos-intel, build-and-validate-macos-arm, integration-tests, release-validation, create-release, publish-pypi] - # TODO: Enable once downstream repository and secrets are configured (see #88) - if: false && github.ref_type == 'tag' && needs.create-release.outputs.is_private_repo != 'true' && needs.create-release.outputs.is_prerelease != 'true' - permissions: - contents: read - - steps: - - name: Extract Windows checksum from GitHub release - id: checksums - run: | - RELEASE_TAG="${{ github.ref_name }}" - curl -L -o apm-windows-x86_64.zip.sha256 \ - "https://github.com/${{ github.repository }}/releases/download/$RELEASE_TAG/apm-windows-x86_64.zip.sha256" - WINDOWS_X86_64_SHA=$(cat apm-windows-x86_64.zip.sha256 | cut -d' ' -f1) - echo "windows-x86_64-sha=$WINDOWS_X86_64_SHA" >> $GITHUB_OUTPUT - echo "Windows x86_64 SHA: $WINDOWS_X86_64_SHA" - - - name: Trigger Scoop bucket repository update - uses: peter-evans/repository-dispatch@v3 - with: - token: ${{ secrets.GH_PKG_PAT }} - repository: microsoft/scoop-apm - event-type: bucket-update - client-payload: | - { - "release": { - "version": "${{ github.ref_name }}", - "tag": "${{ github.ref_name }}", - "repository": "${{ github.repository }}" - }, - "checksums": { - "windows_x86_64": "${{ steps.checksums.outputs.windows-x86_64-sha }}" - } - } diff --git a/tests/integration/release_homebrew_update_harness.py b/tests/integration/release_homebrew_update_harness.py index ded209234..7b610b8bd 100644 --- a/tests/integration/release_homebrew_update_harness.py +++ b/tests/integration/release_homebrew_update_harness.py @@ -29,7 +29,7 @@ def _assert_release_workflow_has_no_homebrew_push(workflow_path: Path) -> None: def _replace_assignment(formula: str, key: str, value: str) -> str: updated, count = re.subn( - rf'(?m)^ {key} "[^"]*"$', + rf'(?m)^ {re.escape(key)} "[^"]*"$', f' {key} "{value}"', formula, ) From ef1bfb45c6440570ae66090519047dd5e3e2ed1e Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 02:54:34 +0200 Subject: [PATCH 06/12] docs(release): drop retired Scoop placeholder note Keep the mirrored release architecture and managed instruction hash aligned after removing the disabled Scoop dispatch job. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .apm/instructions/cicd.instructions.md | 1 - .github/instructions/cicd.instructions.md | 1 - apm.lock.yaml | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.apm/instructions/cicd.instructions.md b/.apm/instructions/cicd.instructions.md index fa94c62c4..e64c79f81 100644 --- a/.apm/instructions/cicd.instructions.md +++ b/.apm/instructions/cicd.instructions.md @@ -82,7 +82,6 @@ integration suite runs only at merge time via GitHub Merge Queue - **Merge queue workflow**: ci.yml (Tier 1 against tentative merge ref) + ci-integration.yml (Tier 2: build -> smoke-test -> integration-tests -> release-validation). Queue auto-merges on success; ejects on failure. - **Push/Release workflow (Linux + Windows)**: build-and-test -> integration-tests -> release-validation -> create-release -> publish-pypi (gh-aw-compat runs in parallel, informational) - **Homebrew distribution**: `microsoft/homebrew-apm` polls the latest public APM release and updates its formula with its own `GITHUB_TOKEN`; this workflow must not send a cross-repository dispatch or hold a tap credential. -- **Scoop placeholder**: `update-scoop` remains disabled pending downstream setup; it must not be enabled with the current cross-repository PAT dispatch design. - **Push/Release workflow (macOS Intel)**: build-and-validate-macos-intel (root node: unit tests + build always + conditional integration/release-validation) -> create-release - **Push/Release workflow (macOS ARM)**: build-and-validate-macos-arm (root node, tag/schedule/dispatch only; all phases run) -> create-release - **Tag Triggers**: Only `v*.*.*` tags trigger full release pipeline diff --git a/.github/instructions/cicd.instructions.md b/.github/instructions/cicd.instructions.md index fa94c62c4..e64c79f81 100644 --- a/.github/instructions/cicd.instructions.md +++ b/.github/instructions/cicd.instructions.md @@ -82,7 +82,6 @@ integration suite runs only at merge time via GitHub Merge Queue - **Merge queue workflow**: ci.yml (Tier 1 against tentative merge ref) + ci-integration.yml (Tier 2: build -> smoke-test -> integration-tests -> release-validation). Queue auto-merges on success; ejects on failure. - **Push/Release workflow (Linux + Windows)**: build-and-test -> integration-tests -> release-validation -> create-release -> publish-pypi (gh-aw-compat runs in parallel, informational) - **Homebrew distribution**: `microsoft/homebrew-apm` polls the latest public APM release and updates its formula with its own `GITHUB_TOKEN`; this workflow must not send a cross-repository dispatch or hold a tap credential. -- **Scoop placeholder**: `update-scoop` remains disabled pending downstream setup; it must not be enabled with the current cross-repository PAT dispatch design. - **Push/Release workflow (macOS Intel)**: build-and-validate-macos-intel (root node: unit tests + build always + conditional integration/release-validation) -> create-release - **Push/Release workflow (macOS ARM)**: build-and-validate-macos-arm (root node, tag/schedule/dispatch only; all phases run) -> create-release - **Tag Triggers**: Only `v*.*.*` tags trigger full release pipeline diff --git a/apm.lock.yaml b/apm.lock.yaml index 017a9d1b2..d077bc39e 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -683,7 +683,7 @@ local_deployed_file_hashes: .github/agents/spec-tag-architect.agent.md: sha256:82907265c5e7cf1ac61ad96866fa7c5683b69c8f09b7a4c5f3cc241acc9568ca .github/agents/supply-chain-security-expert.agent.md: sha256:8fb8cc426d6af17ba084a28b3f026c2b475b62e3ca63ed2f88b83bd823f877af .github/instructions/changelog.instructions.md: sha256:1e51ec4c74e847967962bd279dc4c6e582c5d3578490b3c28d5f3acd3e05f73e - .github/instructions/cicd.instructions.md: sha256:ade86dfad284da18e82b8295859f1a61e759e588248ad163d42d534677f7a438 + .github/instructions/cicd.instructions.md: sha256:08d87b7d635761cb41deb8fc71d5d83f54678de463db484afb16d2d4f8713ecb .github/instructions/cli.instructions.md: sha256:8e39e8d5047ce88575cb02f87c2bcede584dfef258bd86f7466c7badf136541a .github/instructions/doc-sync.instructions.md: sha256:bb3816254f8df6bffc6faacd556871f36903e9d7f348982f1e2de0339384c696 .github/instructions/encoding.instructions.md: sha256:93db7377dc896f6efecf2c5d8c5d89255a555562f468d034d64c42edd5cf46d5 From 36606d3a6db0a8dfd9ad92784c98e24fef0332c0 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 02:57:09 +0200 Subject: [PATCH 07/12] test(release): polish ownership harness Compile the formula assignment matcher explicitly and frame the changelog entry around release reliability, folding the final architecture and growth panel nits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 ++-- tests/integration/release_homebrew_update_harness.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84abab735..261879cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Homebrew formula updates now run independently in the tap repository, removing - its cross-repository credential from the release pipeline. (#2088) +- Homebrew formula updates now run independently in the tap repository, improving + release reliability while removing a cross-repository credential. (#2088) ### Fixed diff --git a/tests/integration/release_homebrew_update_harness.py b/tests/integration/release_homebrew_update_harness.py index 7b610b8bd..75fbbce09 100644 --- a/tests/integration/release_homebrew_update_harness.py +++ b/tests/integration/release_homebrew_update_harness.py @@ -28,11 +28,8 @@ def _assert_release_workflow_has_no_homebrew_push(workflow_path: Path) -> None: def _replace_assignment(formula: str, key: str, value: str) -> str: - updated, count = re.subn( - rf'(?m)^ {re.escape(key)} "[^"]*"$', - f' {key} "{value}"', - formula, - ) + assignment = re.compile(rf'(?m)^ {re.escape(key)} "[^"]*"$') + updated, count = assignment.subn(f' {key} "{value}"', formula) if count != 1: raise RuntimeError(f"formula must contain exactly one {key} assignment") return updated From 5216b51741215f0e921c6591553020bc0f83aad6 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 03:00:33 +0200 Subject: [PATCH 08/12] docs(release): finalize Homebrew reliability story Narrow the auth-token guard to credential-shaped secrets, link the tap for operators, and describe the Homebrew outcome in user terms. This folds the last architecture and DevX panel findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 +++-- docs/src/content/docs/contributing/integration-testing.md | 2 +- tests/integration/release_homebrew_update_harness.py | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 261879cd0..a640be50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Homebrew formula updates now run independently in the tap repository, improving - release reliability while removing a cross-repository credential. (#2088) +- Homebrew upgrades now pick up releases automatically through the tap's own + poller, improving reliability while removing a cross-repository credential. + (#2088) ### Fixed diff --git a/docs/src/content/docs/contributing/integration-testing.md b/docs/src/content/docs/contributing/integration-testing.md index ee3113dee..401a0f200 100644 --- a/docs/src/content/docs/contributing/integration-testing.md +++ b/docs/src/content/docs/contributing/integration-testing.md @@ -161,7 +161,7 @@ The workflow ensures quality gates at each step: Each stage must succeed before proceeding to the next, ensuring only fully validated releases reach users. -The Homebrew tap updates independently: `microsoft/homebrew-apm` polls the latest public APM release and commits formula updates with its own repository-scoped `GITHUB_TOKEN`. The release pipeline does not hold a cross-repository Homebrew credential. +The [`microsoft/homebrew-apm`](https://github.com/microsoft/homebrew-apm) tap updates independently: it polls the latest public APM release and commits formula updates with its own repository-scoped `GITHUB_TOKEN`. The release pipeline does not hold a cross-repository Homebrew credential. ### Test Matrix diff --git a/tests/integration/release_homebrew_update_harness.py b/tests/integration/release_homebrew_update_harness.py index 75fbbce09..f7f019017 100644 --- a/tests/integration/release_homebrew_update_harness.py +++ b/tests/integration/release_homebrew_update_harness.py @@ -19,7 +19,11 @@ def _assert_release_workflow_has_no_homebrew_push(workflow_path: Path) -> None: continue forbidden = ("GH_PKG_PAT", "repository-dispatch", "repository_dispatch", "gh pr create") found = [token for token in forbidden if token in rendered] - if re.search(r"(?:secrets\.[A-Z0-9_]+|github\.token)", rendered, re.IGNORECASE): + if re.search( + r"(?:secrets\.[A-Z0-9_]*(?:PAT|TOKEN)|github\.token)", + rendered, + re.IGNORECASE, + ): found.append("workflow token") if found: raise RuntimeError( From 1c67955325f49d7dc2b2ba8f937331489e0e991c Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 03:05:00 +0200 Subject: [PATCH 09/12] test(release): guard all retired dispatch paths Reject GH_PKG_PAT and repository-dispatch anywhere in the release workflow while limiting generic token checks to package-manager jobs. Remove duplicated contributor-guide prose surfaced by the terminal panel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs/contributing/integration-testing.md | 7 +------ .../integration/release_homebrew_update_harness.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/src/content/docs/contributing/integration-testing.md b/docs/src/content/docs/contributing/integration-testing.md index 401a0f200..52fd5e982 100644 --- a/docs/src/content/docs/contributing/integration-testing.md +++ b/docs/src/content/docs/contributing/integration-testing.md @@ -161,7 +161,7 @@ The workflow ensures quality gates at each step: Each stage must succeed before proceeding to the next, ensuring only fully validated releases reach users. -The [`microsoft/homebrew-apm`](https://github.com/microsoft/homebrew-apm) tap updates independently: it polls the latest public APM release and commits formula updates with its own repository-scoped `GITHUB_TOKEN`. The release pipeline does not hold a cross-repository Homebrew credential. +The [`microsoft/homebrew-apm`](https://github.com/microsoft/homebrew-apm) tap updates independently: it polls the latest APM release and commits formula updates with its own repository-scoped `GITHUB_TOKEN`. The release pipeline does not hold a cross-repository Homebrew credential. ### Test Matrix @@ -219,7 +219,6 @@ Promotion integration tests run on: - Guarantees shipped releases work end-to-end - Users can trust the README golden scenario - Cross-platform binary verification -- Independent Homebrew formula updates through tap polling ## Debugging Test Failures @@ -258,7 +257,3 @@ Promotion integration tests run on: so contributors without those credentials still get a clean `SKIPPED` rather than a hard failure. 3. Keep tests focused and fast. - ---- - -This testing strategy ensures we ship with confidence while maintaining fast development cycles. diff --git a/tests/integration/release_homebrew_update_harness.py b/tests/integration/release_homebrew_update_harness.py index f7f019017..04cc956e3 100644 --- a/tests/integration/release_homebrew_update_harness.py +++ b/tests/integration/release_homebrew_update_harness.py @@ -11,15 +11,16 @@ import yaml -def _assert_release_workflow_has_no_homebrew_push(workflow_path: Path) -> None: +def _assert_release_workflow_has_no_package_manager_push(workflow_path: Path) -> None: workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8")) for job_name, job in workflow.get("jobs", {}).items(): rendered = json.dumps(job) - if "homebrew" not in f"{job_name} {rendered}".lower(): - continue forbidden = ("GH_PKG_PAT", "repository-dispatch", "repository_dispatch", "gh pr create") found = [token for token in forbidden if token in rendered] - if re.search( + package_manager_job = any( + name in f"{job_name} {rendered}".lower() for name in ("homebrew", "scoop") + ) + if package_manager_job and re.search( r"(?:secrets\.[A-Z0-9_]*(?:PAT|TOKEN)|github\.token)", rendered, re.IGNORECASE, @@ -27,7 +28,8 @@ def _assert_release_workflow_has_no_homebrew_push(workflow_path: Path) -> None: found.append("workflow token") if found: raise RuntimeError( - f"release workflow restores a Homebrew push/auth path: {', '.join(found)}" + f"release workflow job {job_name} restores a package-manager " + f"push/auth path: {', '.join(found)}" ) @@ -41,7 +43,7 @@ def _replace_assignment(formula: str, key: str, value: str) -> str: def update_tap(workflow_path: Path, release_path: Path, tap_repo: Path) -> None: """Model the tap's repository-local formula commit from release metadata.""" - _assert_release_workflow_has_no_homebrew_push(workflow_path) + _assert_release_workflow_has_no_package_manager_push(workflow_path) release = json.loads(release_path.read_text(encoding="utf-8")) formula_path = tap_repo / "Formula" / "apm.rb" From f9e397d663ad0395e1ba6f93475fca6ce69aed63 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 14:45:34 +0200 Subject: [PATCH 10/12] test(release): remove Homebrew ownership tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../release_homebrew_update_harness.py | 76 ------------- .../test_release_homebrew_update.py | 103 ------------------ 2 files changed, 179 deletions(-) delete mode 100644 tests/integration/release_homebrew_update_harness.py delete mode 100644 tests/integration/test_release_homebrew_update.py diff --git a/tests/integration/release_homebrew_update_harness.py b/tests/integration/release_homebrew_update_harness.py deleted file mode 100644 index 04cc956e3..000000000 --- a/tests/integration/release_homebrew_update_harness.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Structural release guard plus a hermetic model of the tap's local write.""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -from pathlib import Path - -import yaml - - -def _assert_release_workflow_has_no_package_manager_push(workflow_path: Path) -> None: - workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8")) - for job_name, job in workflow.get("jobs", {}).items(): - rendered = json.dumps(job) - forbidden = ("GH_PKG_PAT", "repository-dispatch", "repository_dispatch", "gh pr create") - found = [token for token in forbidden if token in rendered] - package_manager_job = any( - name in f"{job_name} {rendered}".lower() for name in ("homebrew", "scoop") - ) - if package_manager_job and re.search( - r"(?:secrets\.[A-Z0-9_]*(?:PAT|TOKEN)|github\.token)", - rendered, - re.IGNORECASE, - ): - found.append("workflow token") - if found: - raise RuntimeError( - f"release workflow job {job_name} restores a package-manager " - f"push/auth path: {', '.join(found)}" - ) - - -def _replace_assignment(formula: str, key: str, value: str) -> str: - assignment = re.compile(rf'(?m)^ {re.escape(key)} "[^"]*"$') - updated, count = assignment.subn(f' {key} "{value}"', formula) - if count != 1: - raise RuntimeError(f"formula must contain exactly one {key} assignment") - return updated - - -def update_tap(workflow_path: Path, release_path: Path, tap_repo: Path) -> None: - """Model the tap's repository-local formula commit from release metadata.""" - _assert_release_workflow_has_no_package_manager_push(workflow_path) - - release = json.loads(release_path.read_text(encoding="utf-8")) - formula_path = tap_repo / "Formula" / "apm.rb" - formula = formula_path.read_text(encoding="utf-8") - formula = _replace_assignment(formula, "version", release["tag_name"].removeprefix("v")) - formula = _replace_assignment(formula, "url", release["asset_url"]) - formula = _replace_assignment(formula, "sha256", release["sha256"]) - formula_path.write_text(formula, encoding="utf-8") - - subprocess.run(["git", "add", str(formula_path)], cwd=tap_repo, check=True) - subprocess.run( - ["git", "commit", "-m", f"Update APM to {release['tag_name']}"], - cwd=tap_repo, - check=True, - capture_output=True, - text=True, - ) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--workflow", type=Path, required=True) - parser.add_argument("--release", type=Path, required=True) - parser.add_argument("--tap-repo", type=Path, required=True) - args = parser.parse_args() - update_tap(args.workflow, args.release, args.tap_repo) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/test_release_homebrew_update.py b/tests/integration/test_release_homebrew_update.py deleted file mode 100644 index 0c0bd89b0..000000000 --- a/tests/integration/test_release_homebrew_update.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Regression coverage for the Homebrew release ownership boundary.""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -from pathlib import Path -from urllib.parse import urlparse - - -def _git(repo: Path, *args: str) -> str: - result = subprocess.run( - ["git", *args], - cwd=repo, - check=True, - capture_output=True, - text=True, - ) - return result.stdout.strip() - - -def test_homebrew_update_commits_without_cross_repo_auth(tmp_path: Path) -> None: - """Run a local tap commit after guarding the release workflow auth boundary.""" - repo_root = Path(__file__).parents[2] - tap_repo = tmp_path / "homebrew-apm" - formula_dir = tap_repo / "Formula" - formula_dir.mkdir(parents=True) - formula = formula_dir / "apm.rb" - formula.write_text( - "class Apm < Formula\n" - ' version "0.0.1"\n' - ' url "https://example.invalid/apm-v0.0.1.tar.gz"\n' - f' sha256 "{"0" * 64}"\n' - "end\n", - encoding="utf-8", - ) - _git(tap_repo, "init", "-b", "main") - _git(tap_repo, "config", "user.name", "Homebrew update harness") - _git(tap_repo, "config", "user.email", "harness@example.invalid") - _git(tap_repo, "add", "Formula/apm.rb") - _git(tap_repo, "commit", "-m", "Initial formula") - - release = tmp_path / "release.json" - release.write_text( - json.dumps( - { - "tag_name": "v9.8.7", - "asset_url": "https://github.com/microsoft/apm/releases/download/v9.8.7/apm.tar.gz", - "sha256": "a" * 64, - } - ), - encoding="utf-8", - ) - - fake_bin = tmp_path / "bin" - fake_bin.mkdir() - gh_marker = tmp_path / "gh-was-called" - fake_gh = fake_bin / "gh" - fake_gh.write_text( - f"#!/bin/sh\nprintf called > {gh_marker}\nexit 97\n", - encoding="utf-8", - ) - fake_gh.chmod(0o755) - - env = os.environ.copy() - env.pop("GH_PKG_PAT", None) - env.pop("GH_TOKEN", None) - env["PATH"] = f"{fake_bin}{os.pathsep}{env['PATH']}" - subprocess.run( - [ - sys.executable, - str(Path(__file__).with_name("release_homebrew_update_harness.py")), - "--workflow", - str(repo_root / ".github" / "workflows" / "build-release.yml"), - "--release", - str(release), - "--tap-repo", - str(tap_repo), - ], - cwd=repo_root, - env=env, - check=True, - ) - - updated = formula.read_text(encoding="utf-8") - assignments = { - line.strip().split(" ", 1)[0]: line.strip().split('"')[1] - for line in updated.splitlines() - if line.strip().startswith(("version ", "url ", "sha256 ")) - } - assert assignments["version"] == "9.8.7" - parsed = urlparse(assignments["url"]) - assert (parsed.scheme, parsed.hostname, parsed.path) == ( - "https", - "github.com", - "/microsoft/apm/releases/download/v9.8.7/apm.tar.gz", - ) - assert assignments["sha256"] == "a" * 64 - assert _git(tap_repo, "rev-list", "--count", "HEAD") == "2" - assert _git(tap_repo, "log", "-1", "--pretty=%s") == "Update APM to v9.8.7" - assert not gh_marker.exists() From 7a7f3798470106e282e3c02a0deb1691fd71d409 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 15:53:59 +0200 Subject: [PATCH 11/12] test(release): retain Homebrew ownership regression Keep the hermetic release-to-tap flow as an integration regression so restoring the obsolete dispatch credential fails empirically. This addresses the panel coverage follow-up and the operator mutation-break gate. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../release_homebrew_update_harness.py | 76 +++++++++++++ .../test_release_homebrew_update.py | 103 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 tests/integration/release_homebrew_update_harness.py create mode 100644 tests/integration/test_release_homebrew_update.py diff --git a/tests/integration/release_homebrew_update_harness.py b/tests/integration/release_homebrew_update_harness.py new file mode 100644 index 000000000..04cc956e3 --- /dev/null +++ b/tests/integration/release_homebrew_update_harness.py @@ -0,0 +1,76 @@ +"""Structural release guard plus a hermetic model of the tap's local write.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from pathlib import Path + +import yaml + + +def _assert_release_workflow_has_no_package_manager_push(workflow_path: Path) -> None: + workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8")) + for job_name, job in workflow.get("jobs", {}).items(): + rendered = json.dumps(job) + forbidden = ("GH_PKG_PAT", "repository-dispatch", "repository_dispatch", "gh pr create") + found = [token for token in forbidden if token in rendered] + package_manager_job = any( + name in f"{job_name} {rendered}".lower() for name in ("homebrew", "scoop") + ) + if package_manager_job and re.search( + r"(?:secrets\.[A-Z0-9_]*(?:PAT|TOKEN)|github\.token)", + rendered, + re.IGNORECASE, + ): + found.append("workflow token") + if found: + raise RuntimeError( + f"release workflow job {job_name} restores a package-manager " + f"push/auth path: {', '.join(found)}" + ) + + +def _replace_assignment(formula: str, key: str, value: str) -> str: + assignment = re.compile(rf'(?m)^ {re.escape(key)} "[^"]*"$') + updated, count = assignment.subn(f' {key} "{value}"', formula) + if count != 1: + raise RuntimeError(f"formula must contain exactly one {key} assignment") + return updated + + +def update_tap(workflow_path: Path, release_path: Path, tap_repo: Path) -> None: + """Model the tap's repository-local formula commit from release metadata.""" + _assert_release_workflow_has_no_package_manager_push(workflow_path) + + release = json.loads(release_path.read_text(encoding="utf-8")) + formula_path = tap_repo / "Formula" / "apm.rb" + formula = formula_path.read_text(encoding="utf-8") + formula = _replace_assignment(formula, "version", release["tag_name"].removeprefix("v")) + formula = _replace_assignment(formula, "url", release["asset_url"]) + formula = _replace_assignment(formula, "sha256", release["sha256"]) + formula_path.write_text(formula, encoding="utf-8") + + subprocess.run(["git", "add", str(formula_path)], cwd=tap_repo, check=True) + subprocess.run( + ["git", "commit", "-m", f"Update APM to {release['tag_name']}"], + cwd=tap_repo, + check=True, + capture_output=True, + text=True, + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--workflow", type=Path, required=True) + parser.add_argument("--release", type=Path, required=True) + parser.add_argument("--tap-repo", type=Path, required=True) + args = parser.parse_args() + update_tap(args.workflow, args.release, args.tap_repo) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/test_release_homebrew_update.py b/tests/integration/test_release_homebrew_update.py new file mode 100644 index 000000000..0c0bd89b0 --- /dev/null +++ b/tests/integration/test_release_homebrew_update.py @@ -0,0 +1,103 @@ +"""Regression coverage for the Homebrew release ownership boundary.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path +from urllib.parse import urlparse + + +def _git(repo: Path, *args: str) -> str: + result = subprocess.run( + ["git", *args], + cwd=repo, + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + + +def test_homebrew_update_commits_without_cross_repo_auth(tmp_path: Path) -> None: + """Run a local tap commit after guarding the release workflow auth boundary.""" + repo_root = Path(__file__).parents[2] + tap_repo = tmp_path / "homebrew-apm" + formula_dir = tap_repo / "Formula" + formula_dir.mkdir(parents=True) + formula = formula_dir / "apm.rb" + formula.write_text( + "class Apm < Formula\n" + ' version "0.0.1"\n' + ' url "https://example.invalid/apm-v0.0.1.tar.gz"\n' + f' sha256 "{"0" * 64}"\n' + "end\n", + encoding="utf-8", + ) + _git(tap_repo, "init", "-b", "main") + _git(tap_repo, "config", "user.name", "Homebrew update harness") + _git(tap_repo, "config", "user.email", "harness@example.invalid") + _git(tap_repo, "add", "Formula/apm.rb") + _git(tap_repo, "commit", "-m", "Initial formula") + + release = tmp_path / "release.json" + release.write_text( + json.dumps( + { + "tag_name": "v9.8.7", + "asset_url": "https://github.com/microsoft/apm/releases/download/v9.8.7/apm.tar.gz", + "sha256": "a" * 64, + } + ), + encoding="utf-8", + ) + + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + gh_marker = tmp_path / "gh-was-called" + fake_gh = fake_bin / "gh" + fake_gh.write_text( + f"#!/bin/sh\nprintf called > {gh_marker}\nexit 97\n", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + + env = os.environ.copy() + env.pop("GH_PKG_PAT", None) + env.pop("GH_TOKEN", None) + env["PATH"] = f"{fake_bin}{os.pathsep}{env['PATH']}" + subprocess.run( + [ + sys.executable, + str(Path(__file__).with_name("release_homebrew_update_harness.py")), + "--workflow", + str(repo_root / ".github" / "workflows" / "build-release.yml"), + "--release", + str(release), + "--tap-repo", + str(tap_repo), + ], + cwd=repo_root, + env=env, + check=True, + ) + + updated = formula.read_text(encoding="utf-8") + assignments = { + line.strip().split(" ", 1)[0]: line.strip().split('"')[1] + for line in updated.splitlines() + if line.strip().startswith(("version ", "url ", "sha256 ")) + } + assert assignments["version"] == "9.8.7" + parsed = urlparse(assignments["url"]) + assert (parsed.scheme, parsed.hostname, parsed.path) == ( + "https", + "github.com", + "/microsoft/apm/releases/download/v9.8.7/apm.tar.gz", + ) + assert assignments["sha256"] == "a" * 64 + assert _git(tap_repo, "rev-list", "--count", "HEAD") == "2" + assert _git(tap_repo, "log", "-1", "--pretty=%s") == "Update APM to v9.8.7" + assert not gh_marker.exists() From 59f16c96e516bedc708cf6c589285260a20ee625 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 16:15:33 +0200 Subject: [PATCH 12/12] docs(release): sharpen Homebrew user outcome Describe the tap-owned poller through the user-visible brew upgrade result, addressing the growth panel polish item without changing release scope. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f16f6f41d..95f134eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Homebrew upgrades now pick up releases automatically through the tap's own - poller, improving reliability while removing a cross-repository credential. +- Homebrew formula updates now track releases through the tap's own poller, so + `brew upgrade apm` remains current without a cross-repository credential. (#2088) ## [0.24.1] - 2026-07-10