From 7644d985924b2795142995a3725ed18de5727dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20L=C3=A9pine?= Date: Sat, 8 Aug 2026 09:56:23 +0200 Subject: [PATCH] Publish PhpMetrics on Homebrew via a dedicated tap --- .gitattributes | 1 + .github/workflows/homebrew.yml | 142 +++++++++++++++++++++++++++++++++ Formula/phpmetrics.rb | 66 +++++++++++++++ doc/installation.md | 16 +++- 4 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/homebrew.yml create mode 100644 Formula/phpmetrics.rb diff --git a/.gitattributes b/.gitattributes index 3318e8e6..9c748da4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ releases/ export-ignore tests/ export-ignore artifacts/ export-ignore +Formula/ export-ignore build/ export-ignore doc/ export-ignore diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml new file mode 100644 index 00000000..af32e854 --- /dev/null +++ b/.github/workflows/homebrew.yml @@ -0,0 +1,142 @@ +name: Homebrew + +# Points the Homebrew formula at a newly promoted release. +# +# release.yml publishes every release as a pre-release, to be promoted by hand +# once the artifacts have been checked. This workflow listens to the "released" +# event, which GitHub fires when a release is published as a real release *or* +# when an existing pre-release is promoted to one. Pre-releases never trigger it, +# so `brew install phpmetrics` can never end up serving an rc. +# +# It does two things: +# 1. rewrites Formula/phpmetrics.rb on master, so the formula stays versioned +# and reviewable next to the code it installs; +# 2. mirrors that file to the PhpMetrics/homebrew-phpmetrics tap, which is what +# `brew tap phpmetrics/phpmetrics && brew install phpmetrics` actually reads. +# Homebrew only resolves formulae from repositories named homebrew-, +# hence the separate repository. +# +# Step 2 needs a token allowed to write to *another* repository, which the +# default GITHUB_TOKEN is not. Create a fine-grained PAT (or a GitHub App token) +# with "Contents: read and write" on the tap, and store it as the +# HOMEBREW_TAP_TOKEN secret. Until that secret exists the job still updates the +# formula here and simply skips the mirroring, with a warning. +on: + release: + types: [released] + workflow_dispatch: + inputs: + version: + description: 'Version to point the formula at, format vX.Y.Z (e.g. v2.10.1). The release must already be published.' + required: true + type: string + +jobs: + formula: + runs-on: ubuntu-latest + permissions: + contents: write + env: + TAP_REPOSITORY: PhpMetrics/homebrew-phpmetrics + steps: + - uses: actions/checkout@v4 + with: + # The formula is committed back to master, whatever branch + # the workflow has been dispatched from. + ref: master + + - name: Resolve version + id: resolve + env: + VERSION: ${{ inputs.version || github.event.release.tag_name }} + run: | + # Stable versions only. A tag carrying an alpha/beta/rc suffix + # is skipped rather than failed: promoting a release candidate + # by mistake should not turn the release red, it should just + # leave the formula where it is. + if ! echo "$VERSION" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::warning::$VERSION is not a stable version, leaving the Homebrew formula untouched" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "tag=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Checksum the released phar + id: phar + if: steps.resolve.outputs.skip == 'false' + env: + TAG: ${{ steps.resolve.outputs.tag }} + run: | + URL="https://github.com/PhpMetrics/PhpMetrics/releases/download/$TAG/phpmetrics.phar" + curl -fsSL -o phpmetrics.phar "$URL" + echo "url=$URL" >> "$GITHUB_OUTPUT" + echo "sha256=$(sha256sum phpmetrics.phar | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + rm -f phpmetrics.phar + + - name: Update the formula + if: steps.resolve.outputs.skip == 'false' + env: + URL: ${{ steps.phar.outputs.url }} + SHA256: ${{ steps.phar.outputs.sha256 }} + run: | + # Only url and sha256: Homebrew derives the version from the + # tag in the url, and `brew audit` rejects a redundant + # `version` line. + sed -i -E \ + -e "s|^ url \".*\"| url \"$URL\"|" \ + -e "s|^ sha256 \".*\"| sha256 \"$SHA256\"|" \ + Formula/phpmetrics.rb + grep -E '^ (url|sha256) ' Formula/phpmetrics.rb + + # Advisory only: a Homebrew style rule changing upstream should not + # be able to block a release from reaching the tap. + - name: Check the formula style + if: steps.resolve.outputs.skip == 'false' + continue-on-error: true + run: brew style Formula/phpmetrics.rb + + - name: Commit the formula + if: steps.resolve.outputs.skip == 'false' + env: + TAG: ${{ steps.resolve.outputs.tag }} + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + if git diff --quiet -- Formula/phpmetrics.rb; then + echo "Formula already up to date" + exit 0 + fi + git add Formula/phpmetrics.rb + git commit -m "Point the Homebrew formula at $TAG" + # A release is promoted by hand, possibly long after the tag, + # so master may have moved since the checkout. + git pull --rebase origin master + git push origin HEAD:master + + - name: Mirror the formula to the tap + if: steps.resolve.outputs.skip == 'false' + env: + TAG: ${{ steps.resolve.outputs.tag }} + TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + run: | + if [ -z "$TAP_TOKEN" ]; then + echo "::warning::HOMEBREW_TAP_TOKEN is not set, skipping the push to $TAP_REPOSITORY. Create the tap repository and the secret to enable 'brew install phpmetrics'." + exit 0 + fi + git clone --depth 1 "https://x-access-token:$TAP_TOKEN@github.com/$TAP_REPOSITORY.git" tap + mkdir -p tap/Formula + cp Formula/phpmetrics.rb tap/Formula/phpmetrics.rb + git -C tap config user.name 'github-actions[bot]' + git -C tap config user.email '41898282+github-actions[bot]@users.noreply.github.com' + # `git add` before comparing: on the very first run the file + # is untracked, and `git diff` alone would report no change. + git -C tap add Formula/phpmetrics.rb + if git -C tap diff --cached --quiet; then + echo "Tap already up to date" + exit 0 + fi + git -C tap commit -m "phpmetrics $TAG" + # `push origin HEAD` rather than a bare `push`: it needs no + # upstream, so it still works on a freshly created tap. + git -C tap push origin HEAD diff --git a/Formula/phpmetrics.rb b/Formula/phpmetrics.rb new file mode 100644 index 00000000..4a72a2ec --- /dev/null +++ b/Formula/phpmetrics.rb @@ -0,0 +1,66 @@ +# Homebrew formula for PhpMetrics. +# +# This file is the source of truth, but it is NOT what `brew install` reads: +# Homebrew only looks inside "taps", i.e. repositories named homebrew-. +# .github/workflows/homebrew.yml mirrors this file to PhpMetrics/homebrew-phpmetrics +# every time a release is promoted, which is what users actually install from: +# +# brew tap phpmetrics/phpmetrics +# brew install phpmetrics +# +# The url and sha256 lines below are rewritten by that same workflow. +# Do not bump them by hand; everything else can be edited freely. +# +# The formula ships the phar rather than the standalone binaries attached to each +# release. Those are ad-hoc signed (CodeDirectory identifier "micro.sfx", no +# Developer ID and no notarization), so macOS does run them once Homebrew has +# downloaded them, but shipping them would mean four url/sha256 pairs to keep in +# sync and a formula homebrew-core would not accept. The phar gives one formula +# for every architecture and matches what phpstan, php-cs-fixer and psalm do in +# homebrew-core. The cost is `depends_on "php"`, which pulls Homebrew's own PHP +# even for a user who already has one. +class Phpmetrics < Formula + desc "Static analyzer for PHP: coupling, cyclomatic complexity, maintainability index" + homepage "https://phpmetrics.github.io/website/" + # Homebrew reads the version off the tag in the url, dropping the "v" prefix. + url "https://github.com/PhpMetrics/PhpMetrics/releases/download/v2.9.0/phpmetrics.phar" + sha256 "52c1f14aa0b94695eb34cd1d0049ce7f6a7f89d7d3c3d46bfa45526d3412698a" + license "MIT" + + livecheck do + url :stable + strategy :github_latest + end + + depends_on "php" + + def install + libexec.install "phpmetrics.phar" + + # A wrapper rather than a symlink: it pins the php Homebrew depends on, + # instead of whichever php happens to come first in the user's PATH. + (bin/"phpmetrics").write <<~SHELL + #!/bin/bash + exec "#{formula_opt_bin("php")}/php" "#{libexec}/phpmetrics.phar" "$@" + SHELL + end + + test do + # --version prints "PhpMetrics " + assert_match version.to_s, shell_output("#{bin}/phpmetrics --version") + + (testpath/"src/Foo.php").write <<~PHP + 0 ? $a : -$a; + } + } + PHP + + system bin/"phpmetrics", "--report-json=#{testpath}/report.json", "#{testpath}/src" + assert_path_exists testpath/"report.json" + end +end diff --git a/doc/installation.md b/doc/installation.md index 9fc2fd21..ba7a2675 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -16,24 +16,34 @@ export PATH=~/.composer/vendor/bin:$PATH ## Phar ```bash -curl https://github.com/phpmetrics/PhpMetrics/releases/download/2.10.0/phpmetrics.phar +curl -fsSL -o phpmetrics.phar https://github.com/phpmetrics/PhpMetrics/releases/download/v2.10.0/phpmetrics.phar chmod +x phpmetrics.phar && mv phpmetrics.phar /usr/local/bin/phpmetrics ``` ## Apt (Debian, Ubuntu...) ```bash -curl https://github.com/phpmetrics/PhpMetrics/releases/download/2.10.0/phpmetrics.deb +curl -fsSL -o phpmetrics.deb https://github.com/phpmetrics/PhpMetrics/releases/download/v2.10.0/phpmetrics.deb dpkg -i phpmetrics.deb ``` -## Brew (OSX) +## Homebrew (macOS, Linux) +PhpMetrics is not in homebrew-core, it is published through its own tap: ```bash +brew tap phpmetrics/phpmetrics +brew trust --tap phpmetrics/phpmetrics brew install phpmetrics ``` +Recent versions of Homebrew refuse to load formulae from a tap they don't know, +hence the `brew trust` line. On older versions that command does not exist, and +`brew install` works straight after `brew tap`. + +The formula installs the phar and depends on the `php` formula, so Homebrew +pulls PHP in if you don't have it already. + ## PhpArch ```bash