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 3577e7e commit c666541
Show file tree
Hide file tree
Showing 9 changed files with 479 additions and 13 deletions.
12 changes: 0 additions & 12 deletions .github/config/goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,6 @@ changelog:
- '^docs:'
- '^test:'

brews:
- name: ocm
repository:
owner: open-component-model
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
directory: Formula
homepage: "https://ocm.software/"
description: "The OCM CLI makes it easy to create component versions and embed them in build processes."
test: |
system "#{bin}/ocm --version"
nfpms:
- id: debian
package_name: ocm-cli
Expand Down
59 changes: 59 additions & 0 deletions .github/workflows/publish-to-other-than-github.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,65 @@ on:
types: [publish-ocm-cli]

jobs:
push-to-brew-tap:
name: Update Homebrew Tap
if: github.event.client_payload.push-to-brew-tap && github.event.client_payload.version != ''
runs-on: macos-latest
env:
REPO: open-component-model/homebrew-tap
steps:
- name: Ensure proper version
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: Checkout
uses: actions/checkout@v4
with:
path: tap
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: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: ${{ github.workspace }}/scripts/hack/brew/go.mod
cache: false
- name: Build Script
working-directory: ${{ github.workspace }}/scripts/hack/brew
run: go build -o script
- name: Update Homebrew Tap
run: |
formula=$(${{ github.workspace }}/scripts/hack/brew/script \
--version ${{ env.RELEASE_VERSION }} \
--template ${{ github.workspace }}/scripts/hack/brew/internal/ocm_formula_template.rb.tpl \
--outputDirectory ${{ github.workspace }}/tap/Formula)
mkdir -p ${{ github.workspace }}/tap/Aliases
cd ${{ github.workspace }}/tap/Aliases
ln -s ../Formula/$(basename $formula) ./ocm
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
path: tap
token: ${{ steps.generate_token.outputs.token }}
title: "chore: update OCM CLI to v${{ env.RELEASE_VERSION }}"
commit-message: "[github-actions] update OCM CLI to v${{ env.RELEASE_VERSION }}"
branch: chore/update-ocm-cli/${{ env.RELEASE_VERSION }}
delete-branch: true
sign-commits: true
add-paths: |
Formula/*
Aliases/*
body: |
Update OCM CLI to v${{ env.RELEASE_VERSION }}.
push-to-aur:
name: Update Arch Linux User Repository
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/retrigger-publish-to-other.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ on:
description: Do you want to push to Winget?
required: false
default: false
push-to-brew-tap:
type: boolean
description: Do you want to push to the Homebrew Tap at https://github.com/open-component-model/homebrew-tap?
required: false
default: false

jobs:
retrigger:
Expand Down Expand Up @@ -57,4 +62,4 @@ jobs:
token: ${{ steps.generate_token.outputs.token }}
repository: ${{ github.repository_owner }}/ocm
event-type: publish-ocm-cli
client-payload: '{"version":"${{ env.RELEASE_VERSION }}","push-to-aur":${{ github.event.inputs.push-to-aur }},"push-to-chocolatey":${{ github.event.inputs.push-to-chocolatey }},"push-to-winget":${{ github.event.inputs.push-to-winget }}}'
client-payload: '{"version":"${{ env.RELEASE_VERSION }}","push-to-aur":${{ github.event.inputs.push-to-aur }},"push-to-chocolatey":${{ github.event.inputs.push-to-chocolatey }},"push-to-winget":${{ github.event.inputs.push-to-winget }},"push-to-brew-tap":${{ github.event.inputs.push-to-brew-tap }}}'
3 changes: 3 additions & 0 deletions hack/brew/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module ocm.software/ocm/hack/brew

go 1.23.2
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")
}
}
Loading

0 comments on commit c666541

Please sign in to comment.