Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .github/workflows/_homebrew.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,17 @@ jobs:
path: homebrew-tap

- name: Render formula
env:
FORMULA: ${{ steps.meta.outputs.formula }}
VERSION: ${{ steps.meta.outputs.version }}
SHA_DARWIN_AMD64: ${{ steps.checksums.outputs.darwin_amd64 }}
SHA_DARWIN_ARM64: ${{ steps.checksums.outputs.darwin_arm64 }}
SHA_LINUX_AMD64: ${{ steps.checksums.outputs.linux_amd64 }}
SHA_LINUX_ARM64: ${{ steps.checksums.outputs.linux_arm64 }}
run: |
set -eo pipefail
mkdir -p homebrew-tap/Formula
sed -e 's/{{VERSION}}/${{ steps.meta.outputs.version }}/g' \
-e 's/{{SHA_DARWIN_AMD64}}/${{ steps.checksums.outputs.darwin_amd64 }}/g' \
-e 's/{{SHA_DARWIN_ARM64}}/${{ steps.checksums.outputs.darwin_arm64 }}/g' \
-e 's/{{SHA_LINUX_AMD64}}/${{ steps.checksums.outputs.linux_amd64 }}/g' \
-e 's/{{SHA_LINUX_ARM64}}/${{ steps.checksums.outputs.linux_arm64 }}/g' \
"templates/${{ steps.meta.outputs.formula }}.rb" \
> "homebrew-tap/Formula/${{ steps.meta.outputs.formula }}.rb"
ruby scripts/render_homebrew.rb "templates/${FORMULA}.rb" "homebrew-tap/Formula/${FORMULA}.rb"

- name: Create PR on Homebrew tap
id: create-pr
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Verify

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

concurrency:
group: verify-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
homebrew-templates:
name: Homebrew templates
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
HOMEBREW_NO_ANALYTICS: "1"
steps:
- name: Checkout publisher
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@7001732169813b7bd296f39014e60fcc07f0ef9a # main

- name: Validate Homebrew templates
run: make test-homebrew
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@
#
# Runs focused regression tests for publisher scripts.
.PHONY: test
test:
test: test-homebrew
bash scripts/publish_rpm_reuse_test.sh

## Validate rendered Homebrew templates (requires Homebrew)
.PHONY: test-homebrew
test-homebrew:
brew ruby scripts/homebrew_template_test.rb

## Format code and organize files
#
# Automatically formats all files in the project:
Expand Down
81 changes: 81 additions & 0 deletions scripts/homebrew_template_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require 'tmpdir'
require 'open3'
require 'yaml'
require 'formula'
require 'formulary'
require 'simulate_system'
require_relative 'render_homebrew'

def assert_equal(expected, actual)
raise "Expected #{expected.inspect}, got #{actual.inspect}" unless expected == actual
end

def assert_rejected
yield
rescue ArgumentError
nil
else
raise 'Expected invalid template input to be rejected'
end

root = File.expand_path('..', __dir__)
checksums = {
'SHA_DARWIN_AMD64' => '1' * 64,
'SHA_DARWIN_ARM64' => '2' * 64,
'SHA_LINUX_AMD64' => '3' * 64,
'SHA_LINUX_ARM64' => '4' * 64
}

Dir.mktmpdir('publisher-homebrew-test') do |directory|
Dir[File.join(root, 'packages/*/manifest.yaml')].each do |manifest_path|
manifest = YAML.safe_load_file(manifest_path)
name = manifest.fetch('homebrew_formula')
template_path = File.join(root, 'templates', "#{name}.rb")
template = File.read(template_path)

%w[0.0.3 0.0.4 0.0.5].each do |version|
values = checksums.merge('VERSION' => version)
rendered = render_homebrew(template, values)
raise 'Unresolved template placeholder' if rendered.match?(/\{\{.*?\}\}/)

output_path = File.join(directory, "#{name}-#{version}.rb")
output, status = Open3.capture2e(values, RbConfig.ruby, File.join(__dir__, 'render_homebrew.rb'), template_path,
output_path)
raise "Renderer CLI failed: #{output}" unless status.success?

assert_equal(rendered, File.read(output_path))

manifest.fetch('platforms').each do |platform|
system, architecture = platform.split('-')
os = system == 'darwin' ? :macos : :linux
arch = architecture == 'arm64' ? :arm : :intel
Homebrew::SimulateSystem.with(os: os, arch: arch) do
path = Pathname(directory) / version / platform / "#{name}.rb"
formula = Formulary.from_contents(name, path, rendered, tap: Tap.fetch('techprimate/publisher'))
expected_revision = name == 'apple-docs' && version == '0.0.4' ? 1 : 0
assert_equal(version, formula.version.to_s)
assert_equal(expected_revision, formula.revision)
assert_equal(
"https://packages.techprimate.com/#{manifest.fetch('package')}/bin/v#{version}/#{manifest.fetch('binary')}-#{platform}",
formula.stable.url
)
assert_equal(checksums.fetch("SHA_#{platform.upcase.tr('-', '_')}"), formula.stable.checksum.to_s)
raise 'Missing formula test' unless formula.test_defined?
end
end
end
puts "PASS: #{name} rendering, platform selection, and version-scoped revisions"
end
end

values = checksums.merge('VERSION' => '0.0.4')
assert_rejected { render_homebrew("version '{{UNKNOWN}}'", values) }
assert_rejected { render_homebrew("version '{{VERSION}}'", values.merge('VERSION' => "0.0.4'\nraise 'injected")) }
assert_rejected { render_homebrew("sha256 '{{SHA_DARWIN_ARM64}}'", values.merge('SHA_DARWIN_ARM64' => 'invalid')) }
assert_rejected do
render_homebrew("sha256 '{{SHA_DARWIN_ARM64}}'", values.reject do |key, _|
key == 'SHA_DARWIN_ARM64'
end)
end
assert_rejected { render_homebrew('class Broken < Formula', values) }
puts 'PASS: unknown placeholders, unsafe versions, missing/invalid checksums, and Ruby syntax errors are rejected'
29 changes: 29 additions & 0 deletions scripts/render_homebrew.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require 'ripper'

# Only substitute literal release metadata, never Ruby source supplied by a release.
def render_homebrew(template, values)
rendered = template.gsub(/\{\{(.*?)\}\}/) do
key = Regexp.last_match(1)
pattern = case key
when 'VERSION'
/\A[0-9]+(?:\.[0-9]+)*(?:[-+][0-9A-Za-z.-]+)?\z/
when /\ASHA_(?:DARWIN|LINUX)_(?:AMD64|ARM64)\z/
/\A[0-9a-fA-F]{64}\z/
else
raise ArgumentError, "Unknown Homebrew template placeholder: #{key}"
end
value = values[key]
raise ArgumentError, "Missing or invalid Homebrew template value: #{key}" unless value && pattern.match?(value)

value
end
raise ArgumentError, 'Rendered Homebrew formula has invalid Ruby syntax' unless Ripper.sexp(rendered)

rendered
end

if $PROGRAM_NAME == __FILE__
template_path, output_path = ARGV
rendered = render_homebrew(File.read(template_path), ENV)
File.write(output_path, rendered)
end
37 changes: 24 additions & 13 deletions templates/apple-docs.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,47 @@
class AppleDocs < Formula
desc "CLI to explore Apple Developer Documentation"
homepage "https://github.com/techprimate/apple-docs-cli"
version "{{VERSION}}"
desc 'CLI to explore Apple Developer Documentation'
homepage 'https://github.com/techprimate/apple-docs-cli'
version '{{VERSION}}'
license 'FSL-1.1-MIT'
revision 1 if version.to_s == '0.0.4'

on_macos do
depends_on macos: :ventura

on_arm do
url "https://packages.techprimate.com/apple-docs/bin/v{{VERSION}}/apple-docs-darwin-arm64"
sha256 "{{SHA_DARWIN_ARM64}}"
url 'https://packages.techprimate.com/apple-docs/bin/v{{VERSION}}/apple-docs-darwin-arm64'
sha256 '{{SHA_DARWIN_ARM64}}'
end
on_intel do
url "https://packages.techprimate.com/apple-docs/bin/v{{VERSION}}/apple-docs-darwin-amd64"
sha256 "{{SHA_DARWIN_AMD64}}"
url 'https://packages.techprimate.com/apple-docs/bin/v{{VERSION}}/apple-docs-darwin-amd64'
sha256 '{{SHA_DARWIN_AMD64}}'
end
end

on_linux do
on_arm do
url "https://packages.techprimate.com/apple-docs/bin/v{{VERSION}}/apple-docs-linux-arm64"
sha256 "{{SHA_LINUX_ARM64}}"
url 'https://packages.techprimate.com/apple-docs/bin/v{{VERSION}}/apple-docs-linux-arm64'
sha256 '{{SHA_LINUX_ARM64}}'
end
on_intel do
url "https://packages.techprimate.com/apple-docs/bin/v{{VERSION}}/apple-docs-linux-amd64"
sha256 "{{SHA_LINUX_AMD64}}"
url 'https://packages.techprimate.com/apple-docs/bin/v{{VERSION}}/apple-docs-linux-amd64'
sha256 '{{SHA_LINUX_AMD64}}'
end
end

def install
binary = Dir["apple-docs-*"].first
bin.install binary => "apple-docs"
binary = Dir['apple-docs-*'].first
bin.install binary => 'apple-docs'
generate_completions_from_executable(bin / 'apple-docs', '--generate-completion-script')
end

test do
ENV['TELEMETRY_DISABLED'] = 'true'
assert_match version.to_s, shell_output("#{bin}/apple-docs --version")
assert_match "apple-docs\t", shell_output("#{bin}/apple-docs agent skills list")
assert_match 'name: apple-docs', shell_output("#{bin}/apple-docs agent skills get apple-docs")
assert_path_exists bash_completion / 'apple-docs'
assert_path_exists zsh_completion / '_apple-docs'
assert_path_exists fish_completion / 'apple-docs.fish'
end
end