name: CI on: push: branches: - master pull_request: merge_group: workflow_dispatch: inputs: casks: description: List of casks to audit (comma-separated) required: true skip_install: description: Skip installation of casks required: false default: true type: boolean new_cask: description: Apply new cask audit required: false default: false type: boolean env: HOMEBREW_DEVELOPER: 1 HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_INSTALL_FROM_API: 1 HOMEBREW_GITHUB_API_TOKEN: ${{ github.token }} concurrency: group: "${{ github.ref }}" cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read jobs: generate-matrix: outputs: matrix: ${{ steps.generate-matrix.outputs.matrix }} runs-on: macos-latest steps: - name: Set up Homebrew id: set-up-homebrew uses: Homebrew/actions/setup-homebrew@master with: core: false cask: true test-bot: false - name: Check out Pull Request uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 persist-credentials: false - name: Generate CI matrix id: generate-matrix env: INPUT_CASKS: ${{ github.event.inputs.casks }} PULL_REQUEST_URL: ${{ github.event.pull_request.url }} run: | if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]] then # shellcheck disable=SC2086 # $INPUT_CASKS is a space-separated list of cask tokens brew generate-cask-ci-matrix ${{ github.event.inputs.skip_install && '--skip-install' }} ${{ github.event.inputs.new_cask && '--new' }} --casks $INPUT_CASKS elif [[ "${GITHUB_EVENT_NAME}" == "push" ]] then brew generate-cask-ci-matrix --syntax-only else brew generate-cask-ci-matrix --url "$PULL_REQUEST_URL" fi test: name: ${{ matrix.name }} needs: generate-matrix runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} steps: - name: Set up Homebrew id: set-up-homebrew uses: Homebrew/actions/setup-homebrew@master with: core: false cask: true test-bot: true - name: Enable debug mode run: | echo 'HOMEBREW_DEBUG=1' >> "${GITHUB_ENV}" echo 'HOMEBREW_VERBOSE=1' >> "${GITHUB_ENV}" if: runner.debug - name: Check out Pull Request uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 persist-credentials: false - name: Clean up CI machine if: runner.os == 'macOS' run: brew test-bot --cleanup --only-cleanup-before - name: Cache Homebrew Gems id: cache uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ${{ steps.set-up-homebrew.outputs.gems-path }} key: ${{ matrix.runner }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }} restore-keys: ${{ matrix.runner }}-rubygems- - name: Cache style cache if: runner.os == 'macOS' uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ~/Library/Caches/Homebrew/style key: macos-style-cache-${{ github.sha }} restore-keys: macos-style-cache- - name: Run brew test-bot --only-tap-syntax id: tap-syntax run: brew test-bot --tap '${{ matrix.tap }}' --only-tap-syntax if: always() && !matrix.cask - name: Run brew fetch --cask ${{ matrix.cask.token }} id: fetch run: | brew fetch --cask --retry --force ${{ join(matrix.fetch_args, ' ') }} '${{ matrix.cask.path }}' timeout-minutes: 30 if: > always() && contains(fromJSON('["success", "skipped"]'), steps.tap-syntax.outcome) && matrix.cask - name: Run brew audit --cask${{ (matrix.cask && ' ') || ' --tap ' }}${{ matrix.cask.token || matrix.tap }} id: audit run: | brew audit --cask ${{ join(matrix.audit_args, ' ') }}${{ (matrix.cask && ' ') || ' --tap ' }}'${{ matrix.cask.token || matrix.tap }}' timeout-minutes: 30 if: > always() && contains(fromJSON('["success", "skipped"]'), steps.tap-syntax.outcome) && (!matrix.cask || steps.fetch.outcome == 'success') && !matrix.skip_audit - name: Gather cask information id: info run: | brew ruby <<'EOF' require 'cask/cask_loader' require 'cask/installer' cask = Cask::CaskLoader.load('${{ matrix.cask.path }}') manual_installer = cask.artifacts.any? do |artifact| if defined?(artifact.manual_install) artifact.manual_install end end macos_requirement_satisfied = if macos_requirement = cask.depends_on.macos macos_requirement.satisfied? else true end cask_conflicts = cask.conflicts_with&.dig(:cask).to_a.select { |c| Cask::CaskLoader.load(c).installed? } formula_conflicts = cask.conflicts_with&.dig(:formula).to_a.select { |f| Formula[f].any_version_installed? } installer = Cask::Installer.new(cask) cask_and_formula_dependencies = installer.missing_cask_and_formula_dependencies cask_dependencies = cask_and_formula_dependencies.select { |d| d.is_a?(Cask::Cask) }.map(&:full_name) formula_dependencies = cask_and_formula_dependencies.select { |d| d.is_a?(Formula) }.map(&:full_name) File.open(ENV.fetch("GITHUB_OUTPUT"), "a") do |f| f.puts "manual_installer=#{JSON.generate(manual_installer)}" f.puts "macos_requirement_satisfied=#{JSON.generate(macos_requirement_satisfied)}" f.puts "formula_dependencies=#{JSON.generate(formula_dependencies)}" end File.open(ENV.fetch("GITHUB_ENV"), "a") do |f| f.puts "CASK_CONFLICTS=#{cask_conflicts&.join(" ")}" if cask_conflicts.present? f.puts "CASK_DEPENDENCIES=#{cask_dependencies&.join(" ")}" if cask_dependencies.present? f.puts "FORMULA_CONFLICTS=#{formula_conflicts&.join(" ")}" if formula_conflicts.present? end EOF if: always() && steps.fetch.outcome == 'success' && matrix.cask - name: Uninstall conflicting formulae run: | read -r -a formula_conflicts_array <<< "$FORMULA_CONFLICTS" brew uninstall --formula "${formula_conflicts_array[@]}" if: ${{ always() && steps.info.outcome == 'success' && env.FORMULA_CONFLICTS != '' }} timeout-minutes: 30 - name: Uninstall conflicting casks run: | read -r -a cask_conflicts_array <<< "$CASK_CONFLICTS" brew uninstall --cask "${cask_conflicts_array[@]}" if: ${{ always() && steps.info.outcome == 'success' && env.CASK_CONFLICTS != '' }} timeout-minutes: 30 - name: Run brew uninstall --cask --force --zap ${{ matrix.cask.token }} run: | brew uninstall --cask --force --zap '${{ matrix.cask.path }}' if: always() && steps.info.outcome == 'success' timeout-minutes: 30 - name: Take snapshot of installed and running apps and services id: snapshot run: | brew ruby -r "$(brew --repository homebrew/cask)/cmd/lib/check.rb" <<'EOF' File.open(ENV.fetch("GITHUB_ENV"), "a") do |f| # We have to use a `HOMEBREW_` prefix so it will survive the # environment variable filtering in `brew`. f.puts "HOMEBREW_SNAPSHOT_BEFORE=#{JSON.generate(Check.all)}" end EOF if: always() && steps.info.outcome == 'success' - name: Run brew install --cask ${{ matrix.cask.token }} id: install run: brew install --cask '${{ matrix.cask.path }}' if: > always() && steps.info.outcome == 'success' && fromJSON(steps.info.outputs.macos_requirement_satisfied) && !matrix.skip_install timeout-minutes: 30 - name: Run brew uninstall --cask ${{ matrix.cask.token }} run: brew uninstall --cask '${{ matrix.cask.path }}' if: always() && steps.install.outcome == 'success' && !fromJSON(steps.info.outputs.manual_installer) timeout-minutes: 30 - name: Uninstall cask dependencies run: | read -r -a cask_dependencies_array <<< "$CASK_DEPENDENCIES" brew uninstall --cask "${cask_dependencies_array[@]}" if: ${{ always() && steps.install.outcome == 'success' && env.CASK_DEPENDENCIES != '' }} timeout-minutes: 30 - name: Compare installed and running apps and services with snapshot run: | brew ruby -r "$(brew --repository homebrew/cask)/cmd/lib/check.rb" <<'EOF' require "cask/cask_loader" require "utils/github/actions" before = JSON.parse(ENV.fetch("HOMEBREW_SNAPSHOT_BEFORE", "{}")) .transform_keys(&:to_sym) after = Check.all cask = Cask::CaskLoader.load('${{ matrix.cask.path }}') errors = Check.errors(before, after, cask: cask) errors.each do |error| onoe error puts GitHub::Actions::Annotation.new(:error, error, file: '${{ matrix.cask.path }}') end exit 1 if errors.any? EOF if: always() && steps.snapshot.outcome == 'success' conclusion: name: conclusion needs: test runs-on: ubuntu-latest if: always() steps: - name: Result run: ${{ needs.test.result == 'success' }}