Skip to content

Commit

Permalink
chore: allow publishing to Brew via custom script
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobmoellerdev committed Nov 8, 2024
1 parent 8ded835 commit 0890356
Show file tree
Hide file tree
Showing 6 changed files with 345 additions and 91 deletions.
29 changes: 11 additions & 18 deletions .github/workflows/publish-to-other-than-github.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,28 @@ jobs:
REPO: open-component-model/homebrew-tap
steps:
- name: Ensure proper version
run: |
if [ -n "${{ github.event.inputs.version }}" ]; then
echo "RELEASE_VERSION=$(echo ${{ github.event.inputs.version }} | tr -d ['v'])" >> $GITHUB_ENV
exit 0
fi
if [ -n "${{ github.event.client_payload.version }}" ]; then
echo "RELEASE_VERSION=$(echo ${{ github.event.client_payload.version }} | tr -d ['v'])" >> $GITHUB_ENV
exit 0
fi
echo "Version not provided"
exit 1
run: echo "RELEASE_VERSION=$(echo ${{ github.event.client_payload.version }} | tr -d ['v'])" >> $GITHUB_ENV
- name: Generate token
id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.OCMBOT_APP_ID }}
private_key: ${{ secrets.OCMBOT_PRIV_KEY }}
- name: Get Update Script
uses: actions/checkout@v4
with:
path: scripts
sparse-checkout: 'hack/brew'
- name: Checkout
uses: actions/checkout@v4
with:
repository: ${{ env.REPO }}
token: ${{ steps.generate_token.outputs.token }}
- name: Get Update Script
uses: actions/checkout@v4
with:
path: scripts
sparse-checkout: 'hack/brew'
- name: Update Homebrew Tap
run: |
formula=$(go run scripts/hack/brew \
--version ${{ env.RELEASE_VERSION }} \
--template scripts/hack/brew/ocm_formula_template.rb.tpl \
--template scripts/hack/brew/internal/ocm_formula_template.rb.tpl \
--outputDirectory Formula)
mkdir -p Aliases
ln -s Formula/$(basename formula) Aliases/ocm
Expand All @@ -64,8 +54,11 @@ jobs:
branch: chore/update-ocm-cli/${{ env.RELEASE_VERSION }}
delete-branch: true
sign-commits: true
add-paths: |
Formula/*
Aliases/*
body: |
Update OCM CLI vendor hash (see: .github/workflows/flake_vendorhash.yaml)
Update OCM CLI to v${{ env.RELEASE_VERSION }}.
push-to-aur:
name: Update Arch Linux User Repository
Expand Down
116 changes: 116 additions & 0 deletions hack/brew/internal/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package internal

import (
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
)

const ClassName = "Ocm"

// GenerateVersionedHomebrewFormula generates a Homebrew formula for a specific version,
// architecture, and operating system. It fetches the SHA256 digest for each combination
// and uses a template to create the formula file.
func GenerateVersionedHomebrewFormula(
version string,
architectures []string,
operatingSystems []string,
releaseURL string,
templateFile string,
outputDir string,
writer io.Writer,
) error {
values := map[string]string{
"ReleaseURL": releaseURL,
"Version": version,
}

for _, targetOs := range operatingSystems {
for _, arch := range architectures {
digest, err := FetchDigestFromGithubRelease(releaseURL, version, targetOs, arch)
if err != nil {
return fmt.Errorf("failed to fetch digest for %s/%s: %w", targetOs, arch, err)
}
values[fmt.Sprintf("%s_%s_sha256", targetOs, arch)] = digest
}
}

if err := GenerateFormula(templateFile, outputDir, version, values, writer); err != nil {
return fmt.Errorf("failed to generate formula: %w", err)
}

return nil
}

// FetchDigestFromGithubRelease retrieves the SHA256 digest for a specific version, operating system, and architecture
// from the given release URL.
func FetchDigestFromGithubRelease(releaseURL, version, targetOs, arch string) (_ string, err error) {
url := fmt.Sprintf("%s/v%s/ocm-%s-%s-%s.tar.gz.sha256", releaseURL, version, version, targetOs, arch)
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("failed to get digest: %w", err)
}
defer func() {
err = errors.Join(err, resp.Body.Close())
}()

digestBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read digest: %w", err)
}

return strings.TrimSpace(string(digestBytes)), nil
}

// GenerateFormula generates the Homebrew formula file using the provided template and values.
func GenerateFormula(templateFile, outputDir, version string, values map[string]string, writer io.Writer) error {
tmpl, err := template.New(filepath.Base(templateFile)).Funcs(template.FuncMap{
"classname": func() string {
return fmt.Sprintf("%sAT%s", ClassName, strings.ReplaceAll(version, ".", ""))
},
}).ParseFiles(templateFile)
if err != nil {
return fmt.Errorf("failed to parse template: %w", err)
}

outputFile := fmt.Sprintf("ocm@%s.rb", version)
if err := ensureDirectory(outputDir); err != nil {
return err
}

versionedFormula, err := os.Create(filepath.Join(outputDir, outputFile))
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer versionedFormula.Close()

if err := tmpl.Execute(versionedFormula, values); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}

if _, err := io.WriteString(writer, versionedFormula.Name()); err != nil {
return fmt.Errorf("failed to write output file path: %w", err)
}

return nil
}

// ensureDirectory checks if a directory exists and creates it if it does not.
func ensureDirectory(dir string) error {
fi, err := os.Stat(dir)
if os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
} else if err != nil {
return fmt.Errorf("failed to stat directory: %w", err)
} else if !fi.IsDir() {
return fmt.Errorf("path is not a directory")
}
return nil
}
145 changes: 145 additions & 0 deletions hack/brew/internal/generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package internal

import (
"bytes"
_ "embed"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)

//go:embed ocm_formula_template.rb.tpl
var tplFile []byte

//go:embed testdata/expected_formula.rb
var expectedResolved []byte

func TestGenerateVersionedHomebrewFormula(t *testing.T) {
version := "1.0.0"
architectures := []string{"amd64", "arm64"}
operatingSystems := []string{"darwin", "linux"}
outputDir := t.TempDir()

templateFile := filepath.Join(outputDir, "ocm_formula_template.rb.tpl")
if err := os.WriteFile(templateFile, tplFile, os.ModePerm); err != nil {
t.Fatalf("failed to write template file: %v", err)
}

dummyDigest := "dummy-digest"
// Mock server to simulate fetching digests
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(dummyDigest))
}))
defer server.Close()
expectedResolved = bytes.ReplaceAll(expectedResolved, []byte("$$TEST_SERVER$$"), []byte(server.URL))

var buf bytes.Buffer

err := GenerateVersionedHomebrewFormula(
version,
architectures,
operatingSystems,
server.URL,
templateFile,
outputDir,
&buf,
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

file := buf.String()

fi, err := os.Stat(file)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if fi.Size() == 0 {
t.Fatalf("expected file to be non-empty")
}
if filepath.Ext(file) != ".rb" {
t.Fatalf("expected file to have .rb extension")
}
if !strings.Contains(file, version) {
t.Fatalf("expected file to contain version")
}

data, err := os.ReadFile(file)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

if string(data) != string(expectedResolved) {
t.Fatalf("expected %s, got %s", string(expectedResolved), string(data))
}
}

func TestFetchDigest(t *testing.T) {
expectedDigest := "dummy-digest"
version := "1.0.0"
targetOS, arch := "linux", "amd64"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1.0.0/ocm-1.0.0-linux-amd64.tar.gz.sha256" {
t.Fatalf("expected path %s, got %s", fmt.Sprintf("/v%[1]s/ocm-%[1]s-%s-%s.tar.gz.sha256", version, targetOS, arch), r.URL.Path)
}
w.Write([]byte(expectedDigest))
}))
defer server.Close()

digest, err := FetchDigestFromGithubRelease(server.URL, version, targetOS, arch)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if digest != expectedDigest {
t.Fatalf("expected %s, got %s", expectedDigest, digest)
}
}

func TestGenerateFormula(t *testing.T) {
templateContent := `class {{ classname }} < Formula
version "{{ .Version }}"
end`
templateFile := "test_template.rb.tpl"
if err := os.WriteFile(templateFile, []byte(templateContent), 0644); err != nil {
t.Fatalf("failed to write template file: %v", err)
}
defer os.Remove(templateFile)

outputDir := t.TempDir()
values := map[string]string{"Version": "1.0.0"}

var buf bytes.Buffer

if err := GenerateFormula(templateFile, outputDir, "1.0.0", values, &buf); err != nil {
t.Fatalf("expected no error, got %v", err)
}

if buf.String() == "" {
t.Fatalf("expected non-empty output")
}

outputFile := filepath.Join(outputDir, "[email protected]")
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
t.Fatalf("expected output file to exist")
}
}

func TestEnsureDirectory(t *testing.T) {
dir := t.TempDir()
if err := ensureDirectory(dir); err != nil {
t.Fatalf("expected no error, got %v", err)
}

nonDirFile := filepath.Join(dir, "file")
if err := os.WriteFile(nonDirFile, []byte("content"), 0644); err != nil {
t.Fatalf("failed to write file: %v", err)
}

if err := ensureDirectory(nonDirFile); err == nil {
t.Fatalf("expected error, got nil")
}
}
File renamed without changes.
54 changes: 54 additions & 0 deletions hack/brew/internal/testdata/expected_formula.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# typed: false
# frozen_string_literal: true

class OcmAT100 < Formula
desc "The OCM CLI makes it easy to create component versions and embed them in build processes."
homepage "https://ocm.software/"
version "1.0.0"

on_macos do
on_intel do
url "$$TEST_SERVER$$/v1.0.0/ocm-1.0.0-darwin-amd64.tar.gz"
sha256 "dummy-digest"

def install
bin.install "ocm"
end
end
on_arm do
url "$$TEST_SERVER$$/v1.0.0/ocm-1.0.0-darwin-arm64.tar.gz"
sha256 "dummy-digest"

def install
bin.install "ocm"
end
end
end

on_linux do
on_intel do
if Hardware::CPU.is_64_bit?
url "$$TEST_SERVER$$/v1.0.0/ocm-1.0.0-linux-amd64.tar.gz"
sha256 "dummy-digest"

def install
bin.install "ocm"
end
end
end
on_arm do
if Hardware::CPU.is_64_bit?
url "$$TEST_SERVER$$/v1.0.0/ocm-1.0.0-linux-arm64.tar.gz"
sha256 "dummy-digest"

def install
bin.install "ocm"
end
end
end
end

test do
system "#{bin}/ocm --version"
end
end
Loading

0 comments on commit 0890356

Please sign in to comment.