Skip to content

Commit 6f08a66

Browse files
authored
fix(brew): generate canonical Formula/ocm.rb alongside versioned pin (#1997)
## Summary Users who install via `brew install open-component-model/tap/ocm` accumulate a new keg per release (`ocm@0.40.0`, `ocm@0.41.0`, …) instead of upgrading in place. Root cause is in this repo's release pipeline. The publish workflow only ever generated `Formula/ocm@<version>.rb` and pointed `Aliases/ocm` at the latest one. Brew aliases shadow same-named formula files and resolve to the target, so `brew install …/ocm` installed a versioned formula. Homebrew treats versioned formulas as separate packages and installs them side-by-side, hence the pile-up. Generate **both** files on every release: - `Formula/ocm.rb` (class `Ocm`) — canonical, overwritten each release; `brew install ocm` upgrades in place. - `Formula/ocm@<version>.rb` (class `OcmAT<digits>`) — opt-in pin; users who want a specific version run `brew install ocm@X.Y.Z`. Drop the `Aliases/ocm` symlink creation in the workflow and remove any stale symlink left over from earlier runs. Companion cleanup in the tap (bumps `Formula/ocm.rb` to v0.43.0 manually so users aren't stuck on v0.18.0 until the next release, and removes the existing `Aliases/ocm`): open-component-model/homebrew-tap#47 ## Test plan - [x] `go test ./...` in `hack/brew` passes - [x] Generator output for v0.43.0 byte-identical to the existing tap files (both `Formula/ocm.rb` and `Formula/ocm@0.43.0.rb`) - [x] Locally tapped a synthetic tap with both generated files: `brew info …/ocm` resolves to canonical formula 0.43.0; `brew info …/ocm@0.43.0` still resolves to the versioned formula - [x] Reviewer sanity-checks the workflow diff --------- Signed-off-by: Fabian Burth <fabian.burth@sap.com>
1 parent 778eddb commit 6f08a66

5 files changed

Lines changed: 60 additions & 42 deletions

File tree

.github/workflows/publish-to-other-than-github.yaml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,10 @@ jobs:
5656
run: go build -o script
5757
- name: Update Homebrew Tap
5858
run: |
59-
formula=$(${{ github.workspace }}/scripts/hack/brew/script \
59+
${{ github.workspace }}/scripts/hack/brew/script \
6060
--version ${{ env.RELEASE_VERSION }} \
6161
--template ${{ github.workspace }}/scripts/hack/brew/internal/ocm_formula_template.rb.tpl \
62-
--outputDirectory ${{ github.workspace }}/tap/Formula)
63-
mkdir -p ${{ github.workspace }}/tap/Aliases
64-
cd ${{ github.workspace }}/tap/Aliases
65-
ln -sf ../Formula/$(basename $formula) ./ocm
62+
--outputDirectory ${{ github.workspace }}/tap/Formula
6663
- name: Create Pull Request
6764
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
6865
with:
@@ -75,7 +72,6 @@ jobs:
7572
sign-commits: true
7673
add-paths: |
7774
Formula/*
78-
Aliases/*
7975
body: |
8076
Update OCM CLI to v${{ env.RELEASE_VERSION }}.
8177

hack/brew/internal/generate.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@ import (
1313

1414
const ClassName = "Ocm"
1515

16-
// GenerateVersionedHomebrewFormula generates a Homebrew formula for a specific version,
17-
// architecture, and operating system. It fetches the SHA256 digest for each combination
18-
// and uses a template to create the formula file.
19-
func GenerateVersionedHomebrewFormula(
16+
// GenerateHomebrewFormula generates the canonical Homebrew formula and a
17+
// version-pinned formula for the given version. It fetches the SHA256 digest
18+
// for each os/arch combination and renders both files into outputDir:
19+
//
20+
// - Formula/ocm.rb (class Ocm) — the canonical, unversioned formula. Overwritten
21+
// on every release so `brew install ocm` and `brew upgrade` operate in place.
22+
// - Formula/ocm@<version>.rb (class OcmAT<digits>) — opt-in pin to a specific
23+
// version, for users who want `brew install ocm@X.Y.Z`.
24+
func GenerateHomebrewFormula(
2025
version string,
2126
architectures []string,
2227
operatingSystems []string,
@@ -40,8 +45,16 @@ func GenerateVersionedHomebrewFormula(
4045
}
4146
}
4247

43-
if err := GenerateFormula(templateFile, outputDir, version, values, writer); err != nil {
44-
return fmt.Errorf("failed to generate formula: %w", err)
48+
if err := GenerateFormula(templateFile, outputDir, "ocm.rb", ClassName, values, writer); err != nil {
49+
return fmt.Errorf("failed to generate canonical formula: %w", err)
50+
}
51+
if _, err := io.WriteString(writer, "\n"); err != nil {
52+
return fmt.Errorf("failed to write separator: %w", err)
53+
}
54+
versionedFile := fmt.Sprintf("ocm@%s.rb", version)
55+
versionedClass := fmt.Sprintf("%sAT%s", ClassName, strings.ReplaceAll(version, ".", ""))
56+
if err := GenerateFormula(templateFile, outputDir, versionedFile, versionedClass, values, writer); err != nil {
57+
return fmt.Errorf("failed to generate versioned formula: %w", err)
4558
}
4659

4760
return nil
@@ -67,33 +80,33 @@ func FetchDigestFromGithubRelease(releaseURL, version, targetOs, arch string) (_
6780
return strings.TrimSpace(string(digestBytes)), nil
6881
}
6982

70-
// GenerateFormula generates the Homebrew formula file using the provided template and values.
71-
func GenerateFormula(templateFile, outputDir, version string, values map[string]string, writer io.Writer) error {
83+
// GenerateFormula renders templateFile into outputDir/outputFile using the
84+
// provided className for the Ruby class and values for template substitution.
85+
func GenerateFormula(templateFile, outputDir, outputFile, className string, values map[string]string, writer io.Writer) error {
7286
tmpl, err := template.New(filepath.Base(templateFile)).Funcs(template.FuncMap{
7387
"classname": func() string {
74-
return fmt.Sprintf("%sAT%s", ClassName, strings.ReplaceAll(version, ".", ""))
88+
return className
7589
},
7690
}).ParseFiles(templateFile)
7791
if err != nil {
7892
return fmt.Errorf("failed to parse template: %w", err)
7993
}
8094

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

86-
versionedFormula, err := os.Create(filepath.Join(outputDir, outputFile))
99+
formula, err := os.Create(filepath.Join(outputDir, outputFile))
87100
if err != nil {
88101
return fmt.Errorf("failed to create output file: %w", err)
89102
}
90-
defer versionedFormula.Close()
103+
defer formula.Close()
91104

92-
if err := tmpl.Execute(versionedFormula, values); err != nil {
105+
if err := tmpl.Execute(formula, values); err != nil {
93106
return fmt.Errorf("failed to execute template: %w", err)
94107
}
95108

96-
if _, err := io.WriteString(writer, versionedFormula.Name()); err != nil {
109+
if _, err := io.WriteString(writer, formula.Name()); err != nil {
97110
return fmt.Errorf("failed to write output file path: %w", err)
98111
}
99112

hack/brew/internal/generate_test.go

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var tplFile []byte
1818
//go:embed testdata/expected_formula.rb
1919
var expectedResolved []byte
2020

21-
func TestGenerateVersionedHomebrewFormula(t *testing.T) {
21+
func TestGenerateHomebrewFormula(t *testing.T) {
2222
version := "1.0.0"
2323
architectures := []string{"amd64", "arm64"}
2424
operatingSystems := []string{"darwin", "linux"}
@@ -39,7 +39,7 @@ func TestGenerateVersionedHomebrewFormula(t *testing.T) {
3939

4040
var buf bytes.Buffer
4141

42-
err := GenerateVersionedHomebrewFormula(
42+
err := GenerateHomebrewFormula(
4343
version,
4444
architectures,
4545
operatingSystems,
@@ -52,29 +52,38 @@ func TestGenerateVersionedHomebrewFormula(t *testing.T) {
5252
t.Fatalf("expected no error, got %v", err)
5353
}
5454

55-
file := buf.String()
56-
57-
fi, err := os.Stat(file)
58-
if err != nil {
59-
t.Fatalf("expected no error, got %v", err)
55+
paths := strings.Split(strings.TrimSpace(buf.String()), "\n")
56+
if len(paths) != 2 {
57+
t.Fatalf("expected 2 generated paths, got %d: %q", len(paths), paths)
6058
}
61-
if fi.Size() == 0 {
62-
t.Fatalf("expected file to be non-empty")
63-
}
64-
if filepath.Ext(file) != ".rb" {
65-
t.Fatalf("expected file to have .rb extension")
66-
}
67-
if !strings.Contains(file, version) {
68-
t.Fatalf("expected file to contain version")
59+
60+
for _, file := range paths {
61+
fi, err := os.Stat(file)
62+
if err != nil {
63+
t.Fatalf("expected no error, got %v", err)
64+
}
65+
if fi.Size() == 0 {
66+
t.Fatalf("expected file to be non-empty")
67+
}
68+
if filepath.Ext(file) != ".rb" {
69+
t.Fatalf("expected file to have .rb extension")
70+
}
6971
}
7072

71-
data, err := os.ReadFile(file)
73+
canonical, err := os.ReadFile(paths[0])
7274
if err != nil {
7375
t.Fatalf("expected no error, got %v", err)
7476
}
7577

76-
if string(data) != string(expectedResolved) {
77-
t.Fatalf("expected %s, got %s", string(expectedResolved), string(data))
78+
if string(canonical) != string(expectedResolved) {
79+
t.Fatalf("expected %s, got %s", string(expectedResolved), string(canonical))
80+
}
81+
82+
if filepath.Base(paths[0]) != "ocm.rb" {
83+
t.Fatalf("expected first generated file to be ocm.rb, got %s", paths[0])
84+
}
85+
if filepath.Base(paths[1]) != "ocm@1.0.0.rb" {
86+
t.Fatalf("expected second generated file to be ocm@1.0.0.rb, got %s", paths[1])
7887
}
7988
}
8089

@@ -114,15 +123,15 @@ end`
114123

115124
var buf bytes.Buffer
116125

117-
if err := GenerateFormula(templateFile, outputDir, "1.0.0", values, &buf); err != nil {
126+
if err := GenerateFormula(templateFile, outputDir, "ocm.rb", "Ocm", values, &buf); err != nil {
118127
t.Fatalf("expected no error, got %v", err)
119128
}
120129

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

125-
outputFile := filepath.Join(outputDir, "ocm@1.0.0.rb")
134+
outputFile := filepath.Join(outputDir, "ocm.rb")
126135
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
127136
t.Fatalf("expected output file to exist")
128137
}

hack/brew/internal/testdata/expected_formula.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# typed: false
22
# frozen_string_literal: true
33

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

hack/brew/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func main() {
2828
log.Fatalf("version is required")
2929
}
3030

31-
if err := internal.GenerateVersionedHomebrewFormula(*version,
31+
if err := internal.GenerateHomebrewFormula(*version,
3232
strings.Split(*architecturesRaw, ","),
3333
strings.Split(*operatingSystemsRaw, ","),
3434
*releaseURL,

0 commit comments

Comments
 (0)