From d5cb06b04c2b5cc5d6c13234721a14e58667d932 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Tue, 1 Sep 2026 15:54:23 -0300 Subject: [PATCH 1/4] feat(promote): add Homebrew formula rewrite helpers --- .../2026-08-28-promote-homebrew-design.md | 136 ++++++++++++++++++ scripts/promote/homebrew.mjs | 39 +++++ scripts/promote/homebrew.test.mjs | 60 ++++++++ 3 files changed, 235 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-28-promote-homebrew-design.md create mode 100644 scripts/promote/homebrew.mjs create mode 100644 scripts/promote/homebrew.test.mjs diff --git a/docs/superpowers/specs/2026-08-28-promote-homebrew-design.md b/docs/superpowers/specs/2026-08-28-promote-homebrew-design.md new file mode 100644 index 00000000..7d39bfd3 --- /dev/null +++ b/docs/superpowers/specs/2026-08-28-promote-homebrew-design.md @@ -0,0 +1,136 @@ +# Promote Homebrew CLI Design + +**Status:** Draft (pending review) +**Date:** 2026-08-28 +**Scope:** First promote target only — Homebrew tap for the `dw` CLI. Does not +publish npm or PyPI. Does not change `release.yml`. + +## Goal + +Add a **manual** GitHub Actions workflow that promotes an existing GitHub +Release’s macOS CLI zip into `mulesoft/homebrew-data-weave` by opening a +pull request that updates `formula/dw.rb` (`url`, `sha256`, `version`). + +Customer path stays: + +```bash +brew tap mulesoft/data-weave +brew install dw +``` + +## Non-goals + +- `npm publish` / PyPI / Chocolatey. +- Rebuilding natives or changing `release.yml`. +- Pushing directly to the tap’s default branch. +- Linux or Windows Homebrew/Linuxbrew bottles. + +## Constraints (locked) + +- Trigger: `workflow_dispatch` only. Inputs: `tag` (required, `v*`), `dry_run` + (boolean, default false). +- Source of truth: GitHub Release assets already attached by `release.yml`. +- Exists-check: if tap `formula/dw.rb` `version` already equals the tag + (without the leading `v`), skip with success. +- Tap update is a **PR**, not a push to `master`. +- Auth: repo secret `HOMEBREW_TAP_TOKEN` with contents + pull-requests on + `mulesoft/homebrew-data-weave`. Not npm OIDC. +- Fail if the expected macOS CLI asset is missing from the Release. + +## Current tap + +`formula/dw.rb` today: + +- `url` → `mulesoft-labs/data-weave-cli` release asset `dw-1.0.36-macOS` +- `version` → `2.11.0-20251026` (does not match the url filename) + +This repo’s release asset name (current convention): + +``` +dw-cli--macos-.zip +``` + +`macos-latest` produces `arm64`. First promote retargets `url` at: + +``` +https://github.com/mulesoft/data-weave-cli/releases/download//dw-cli--macos-arm64.zip +``` + +If a future Intel macOS zip exists, that is a later formula `on_intel` / +`on_arm` split — out of scope. + +## Workflow + +File: `.github/workflows/promote-release.yml` + +Job `homebrew` (`ubuntu-latest`): + +1. Normalize `tag` → `version` (`v1.2.3` → `1.2.3`). Reject tags that do not + match `v` + semver-ish (`[0-9].*`). +2. `gh release view ` — must exist. +3. Confirm asset `dw-cli-${version}-macos-arm64.zip` is listed. Download it + (or fetch bytes for sha only). +4. `sha256sum` the zip. +5. Checkout tap with `HOMEBREW_TAP_TOKEN`. +6. Parse current `version` from `formula/dw.rb`. If equal to `${version}`, + exit 0 (“already promoted”). +7. Rewrite `url`, `sha256`, `version`. Keep `desc`, `homepage`, `install`. + Update `homepage` to `https://github.com/mulesoft/data-weave-cli` if it + still points at `mulesoft-labs`. +8. `dry_run`: print the new formula and stop. +9. Else: commit on `promote-dw-` and open a PR to the tap default + branch (`master`). PR body lists tag, asset URL, sha256. + +Idempotent: a second run after the PR merged hits the exists-check. A second +run while the PR is open may fail on branch exists — recreate or reuse the +branch and force-update only that promote branch (not `master`). + +## Formula shape after promote + +```ruby +class Dw < Formula + desc "DataWeave CLI" + homepage "https://github.com/mulesoft/data-weave-cli" + url "https://github.com/mulesoft/data-weave-cli/releases/download/v/dw-cli--macos-arm64.zip" + sha256 "" + version "" + + def install + prefix.install "bin" + prefix.install "libs" + end +end +``` + +`install` assumes the zip still contains `bin/` and `libs/` (current +`native-cli:distro` layout). If the zip layout changes, this job must fail +loudly rather than invent paths — optional sanity: unzip listing must include +`bin/dw` (or `bin/dw.exe` is N/A on macOS). + +## Secrets / permissions + +- `HOMEBREW_TAP_TOKEN`: PAT or GitHub App installation token, repo scope on + the tap. +- Workflow `contents: read` on this repo (release download via `GITHUB_TOKEN`). +- Do not use `GITHUB_TOKEN` to push the tap (wrong repo). + +## Testing + +- Unit-testable helpers if we extract rewrite/parse into a small script + (`scripts/promote/homebrew.mjs` or `.sh`): parse version from formula, + render new formula, reject bad tags. +- CI of this repo does **not** run promote on PRs (dispatch only). +- Manual `dry_run` against a real tag after merge. + +## Success criteria + +- Dispatch with a tag that has the macOS zip opens a tap PR (or no-ops if + already at that version). +- `dry_run` makes no tap changes. +- `release.yml` unchanged. + +## Out of scope (follow-up) + +- npm / PyPI promote jobs in the same workflow file (add later as extra jobs). +- Multi-arch Homebrew bottles. +- Auto-dispatch after `release.yml` (stay manual). diff --git a/scripts/promote/homebrew.mjs b/scripts/promote/homebrew.mjs new file mode 100644 index 00000000..f95e6f4a --- /dev/null +++ b/scripts/promote/homebrew.mjs @@ -0,0 +1,39 @@ +export function parseTag(tag) { + if (!/^v[0-9]/.test(tag)) { + throw new Error(`Expected a version tag beginning with v and a digit: ${tag}`); + } + + return tag.slice(1); +} + +export function assetName(version) { + return `dw-cli-${version}-macos-arm64.zip`; +} + +export function releaseAssetUrl(repo, tag, version) { + return `https://github.com/${repo}/releases/download/${tag}/${assetName(version)}`; +} + +export function parseFormulaVersion(formula) { + return formula.match(/^\s*version "([^"]+)"/m)?.[1]; +} + +export function rewriteFormula(formula, { url, sha256, version, homepage }) { + let rewritten = formula + .replace(/^(\s*url) "[^"]+"/m, `$1 "${url}"`) + .replace(/^(\s*sha256) "[^"]+"/m, `$1 "${sha256}"`) + .replace(/^(\s*version) "[^"]+"/m, `$1 "${version}"`); + + if (rewritten.match(/^\s*homepage "[^"]*mulesoft-labs\/data-weave-cli[^"]*"/m)) { + rewritten = rewritten.replace( + /^(\s*homepage) "[^"]+"/m, + `$1 "${homepage ?? "https://github.com/mulesoft/data-weave-cli"}"`, + ); + } + + return rewritten; +} + +export function alreadyPromoted(formula, version) { + return parseFormulaVersion(formula) === version; +} diff --git a/scripts/promote/homebrew.test.mjs b/scripts/promote/homebrew.test.mjs new file mode 100644 index 00000000..3298ddb3 --- /dev/null +++ b/scripts/promote/homebrew.test.mjs @@ -0,0 +1,60 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + parseTag, + assetName, + releaseAssetUrl, + parseFormulaVersion, + rewriteFormula, + alreadyPromoted, +} from "./homebrew.mjs"; + +const SAMPLE = `class Dw < Formula + desc "DataWeave CLI" + homepage "https://github.com/mulesoft-labs/data-weave-cli" + url "https://github.com/mulesoft-labs/data-weave-cli/releases/download/v1.0.36/dw-1.0.36-macOS" + sha256 "d503f000c24bf0a7701df917561b930bccfc98a922b6425065e13c93f73831fe" + version "2.11.0-20251026" + + def install + prefix.install "bin" + prefix.install "libs" + end +end +`; + +test("parseTag strips v", () => { + assert.equal(parseTag("v1.2.3"), "1.2.3"); + assert.throws(() => parseTag("1.2.3")); + assert.throws(() => parseTag("main")); +}); + +test("asset names and urls", () => { + assert.equal(assetName("1.2.3"), "dw-cli-1.2.3-macos-arm64.zip"); + assert.equal( + releaseAssetUrl("mulesoft/data-weave-cli", "v1.2.3", "1.2.3"), + "https://github.com/mulesoft/data-weave-cli/releases/download/v1.2.3/dw-cli-1.2.3-macos-arm64.zip", + ); +}); + +test("parse and alreadyPromoted", () => { + assert.equal(parseFormulaVersion(SAMPLE), "2.11.0-20251026"); + assert.equal(alreadyPromoted(SAMPLE, "2.11.0-20251026"), true); + assert.equal(alreadyPromoted(SAMPLE, "1.2.3"), false); +}); + +test("rewriteFormula updates url sha version homepage", () => { + const next = rewriteFormula(SAMPLE, { + url: "https://github.com/mulesoft/data-weave-cli/releases/download/v1.2.3/dw-cli-1.2.3-macos-arm64.zip", + sha256: "abc", + version: "1.2.3", + }); + assert.match( + next, + /url "https:\/\/github.com\/mulesoft\/data-weave-cli\/releases\/download\/v1.2.3\/dw-cli-1.2.3-macos-arm64.zip"/, + ); + assert.match(next, /sha256 "abc"/); + assert.match(next, /version "1.2.3"/); + assert.match(next, /homepage "https:\/\/github.com\/mulesoft\/data-weave-cli"/); + assert.match(next, /prefix.install "bin"/); +}); From bac630c8f4bb705a473a8a8aad29e25f57f73299 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Tue, 1 Sep 2026 15:54:31 -0300 Subject: [PATCH 2/4] feat(promote): dispatch Homebrew tap PR from a GitHub Release --- .github/workflows/promote-release.yml | 112 ++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 .github/workflows/promote-release.yml diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml new file mode 100644 index 00000000..ed30db53 --- /dev/null +++ b/.github/workflows/promote-release.yml @@ -0,0 +1,112 @@ +name: Promote release + +on: + workflow_dispatch: + inputs: + tag: + description: GitHub release tag (v1.2.3) + required: true + type: string + dry_run: + description: Print formula changes without opening a tap PR + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + homebrew: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Test Homebrew helpers + run: node --test scripts/promote/homebrew.test.mjs + - name: Resolve tag and download macOS CLI zip + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + VERSION=$(node --input-type=module -e "import { parseTag } from './scripts/promote/homebrew.mjs'; console.log(parseTag(process.env.TAG));") + ASSET=$(VERSION="$VERSION" node --input-type=module -e "import { assetName } from './scripts/promote/homebrew.mjs'; console.log(assetName(process.env.VERSION));") + gh release view "$TAG" --json assets --jq '.assets[].name' | grep -Fx "$ASSET" + gh release download "$TAG" --pattern "$ASSET" + unzip -Z1 "$ASSET" | grep -Fx 'bin/dw' + SHA=$(sha256sum "$ASSET" | awk '{print $1}') + { + echo "VERSION=$VERSION" + echo "ASSET=$ASSET" + echo "SHA=$SHA" + } >> "$GITHUB_ENV" + - name: Checkout tap + uses: actions/checkout@v4 + with: + repository: mulesoft/homebrew-data-weave + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: tap + - name: Rewrite formula + id: formula + env: + TAG: ${{ inputs.tag }} + SHA: ${{ env.SHA }} + run: | + set -euo pipefail + node --input-type=module <<'EOF' + import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; + import { + alreadyPromoted, + parseTag, + releaseAssetUrl, + rewriteFormula, + } from "./scripts/promote/homebrew.mjs"; + + const tag = process.env.TAG; + const version = parseTag(tag); + const formulaPath = "tap/formula/dw.rb"; + const formula = readFileSync(formulaPath, "utf8"); + + if (alreadyPromoted(formula, version)) { + console.log(`already promoted: ${version}`); + appendFileSync(process.env.GITHUB_OUTPUT, "skipped=true\n"); + process.exit(0); + } + + const next = rewriteFormula(formula, { + url: releaseAssetUrl("mulesoft/data-weave-cli", tag, version), + sha256: process.env.SHA, + version, + }); + writeFileSync(formulaPath, next); + console.log(next); + appendFileSync(process.env.GITHUB_OUTPUT, "skipped=false\n"); + EOF + - name: Open tap pull request + if: steps.formula.outputs.skipped == 'false' && inputs.dry_run == false + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + TAG: ${{ inputs.tag }} + VERSION: ${{ env.VERSION }} + ASSET: ${{ env.ASSET }} + SHA: ${{ env.SHA }} + working-directory: tap + run: | + set -euo pipefail + BRANCH="promote-dw-${VERSION}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + git add formula/dw.rb + git commit -m "chore: promote dw ${VERSION}" + git push -u origin "$BRANCH" --force + if gh pr list --repo mulesoft/homebrew-data-weave --head "$BRANCH" --json number --jq '.[0].number' | grep -q .; then + echo "PR already open" + else + gh pr create --repo mulesoft/homebrew-data-weave --base master --head "$BRANCH" \ + --title "Promote dw ${VERSION}" \ + --body "From ${TAG} asset ${ASSET} sha256 ${SHA}" + fi From 8fdffceeb1665a99af034b5eac9162eef173286e Mon Sep 17 00:00:00 2001 From: mlischetti Date: Thu, 3 Sep 2026 10:54:36 -0300 Subject: [PATCH 3/4] Remove Homebrew helper test --- .github/workflows/promote-release.yml | 2 - scripts/promote/homebrew.test.mjs | 60 --------------------------- 2 files changed, 62 deletions(-) delete mode 100644 scripts/promote/homebrew.test.mjs diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml index ed30db53..670cdf9b 100644 --- a/.github/workflows/promote-release.yml +++ b/.github/workflows/promote-release.yml @@ -24,8 +24,6 @@ jobs: - uses: actions/setup-node@v4 with: node-version: "20" - - name: Test Homebrew helpers - run: node --test scripts/promote/homebrew.test.mjs - name: Resolve tag and download macOS CLI zip env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/promote/homebrew.test.mjs b/scripts/promote/homebrew.test.mjs deleted file mode 100644 index 3298ddb3..00000000 --- a/scripts/promote/homebrew.test.mjs +++ /dev/null @@ -1,60 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { - parseTag, - assetName, - releaseAssetUrl, - parseFormulaVersion, - rewriteFormula, - alreadyPromoted, -} from "./homebrew.mjs"; - -const SAMPLE = `class Dw < Formula - desc "DataWeave CLI" - homepage "https://github.com/mulesoft-labs/data-weave-cli" - url "https://github.com/mulesoft-labs/data-weave-cli/releases/download/v1.0.36/dw-1.0.36-macOS" - sha256 "d503f000c24bf0a7701df917561b930bccfc98a922b6425065e13c93f73831fe" - version "2.11.0-20251026" - - def install - prefix.install "bin" - prefix.install "libs" - end -end -`; - -test("parseTag strips v", () => { - assert.equal(parseTag("v1.2.3"), "1.2.3"); - assert.throws(() => parseTag("1.2.3")); - assert.throws(() => parseTag("main")); -}); - -test("asset names and urls", () => { - assert.equal(assetName("1.2.3"), "dw-cli-1.2.3-macos-arm64.zip"); - assert.equal( - releaseAssetUrl("mulesoft/data-weave-cli", "v1.2.3", "1.2.3"), - "https://github.com/mulesoft/data-weave-cli/releases/download/v1.2.3/dw-cli-1.2.3-macos-arm64.zip", - ); -}); - -test("parse and alreadyPromoted", () => { - assert.equal(parseFormulaVersion(SAMPLE), "2.11.0-20251026"); - assert.equal(alreadyPromoted(SAMPLE, "2.11.0-20251026"), true); - assert.equal(alreadyPromoted(SAMPLE, "1.2.3"), false); -}); - -test("rewriteFormula updates url sha version homepage", () => { - const next = rewriteFormula(SAMPLE, { - url: "https://github.com/mulesoft/data-weave-cli/releases/download/v1.2.3/dw-cli-1.2.3-macos-arm64.zip", - sha256: "abc", - version: "1.2.3", - }); - assert.match( - next, - /url "https:\/\/github.com\/mulesoft\/data-weave-cli\/releases\/download\/v1.2.3\/dw-cli-1.2.3-macos-arm64.zip"/, - ); - assert.match(next, /sha256 "abc"/); - assert.match(next, /version "1.2.3"/); - assert.match(next, /homepage "https:\/\/github.com\/mulesoft\/data-weave-cli"/); - assert.match(next, /prefix.install "bin"/); -}); From 84d35a9631ef7282dfcbbc59a5661d3effbf4480 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Thu, 3 Sep 2026 11:08:46 -0300 Subject: [PATCH 4/4] Remove Homebrew promotion design spec --- .../2026-08-28-promote-homebrew-design.md | 136 ------------------ 1 file changed, 136 deletions(-) delete mode 100644 docs/superpowers/specs/2026-08-28-promote-homebrew-design.md diff --git a/docs/superpowers/specs/2026-08-28-promote-homebrew-design.md b/docs/superpowers/specs/2026-08-28-promote-homebrew-design.md deleted file mode 100644 index 7d39bfd3..00000000 --- a/docs/superpowers/specs/2026-08-28-promote-homebrew-design.md +++ /dev/null @@ -1,136 +0,0 @@ -# Promote Homebrew CLI Design - -**Status:** Draft (pending review) -**Date:** 2026-08-28 -**Scope:** First promote target only — Homebrew tap for the `dw` CLI. Does not -publish npm or PyPI. Does not change `release.yml`. - -## Goal - -Add a **manual** GitHub Actions workflow that promotes an existing GitHub -Release’s macOS CLI zip into `mulesoft/homebrew-data-weave` by opening a -pull request that updates `formula/dw.rb` (`url`, `sha256`, `version`). - -Customer path stays: - -```bash -brew tap mulesoft/data-weave -brew install dw -``` - -## Non-goals - -- `npm publish` / PyPI / Chocolatey. -- Rebuilding natives or changing `release.yml`. -- Pushing directly to the tap’s default branch. -- Linux or Windows Homebrew/Linuxbrew bottles. - -## Constraints (locked) - -- Trigger: `workflow_dispatch` only. Inputs: `tag` (required, `v*`), `dry_run` - (boolean, default false). -- Source of truth: GitHub Release assets already attached by `release.yml`. -- Exists-check: if tap `formula/dw.rb` `version` already equals the tag - (without the leading `v`), skip with success. -- Tap update is a **PR**, not a push to `master`. -- Auth: repo secret `HOMEBREW_TAP_TOKEN` with contents + pull-requests on - `mulesoft/homebrew-data-weave`. Not npm OIDC. -- Fail if the expected macOS CLI asset is missing from the Release. - -## Current tap - -`formula/dw.rb` today: - -- `url` → `mulesoft-labs/data-weave-cli` release asset `dw-1.0.36-macOS` -- `version` → `2.11.0-20251026` (does not match the url filename) - -This repo’s release asset name (current convention): - -``` -dw-cli--macos-.zip -``` - -`macos-latest` produces `arm64`. First promote retargets `url` at: - -``` -https://github.com/mulesoft/data-weave-cli/releases/download//dw-cli--macos-arm64.zip -``` - -If a future Intel macOS zip exists, that is a later formula `on_intel` / -`on_arm` split — out of scope. - -## Workflow - -File: `.github/workflows/promote-release.yml` - -Job `homebrew` (`ubuntu-latest`): - -1. Normalize `tag` → `version` (`v1.2.3` → `1.2.3`). Reject tags that do not - match `v` + semver-ish (`[0-9].*`). -2. `gh release view ` — must exist. -3. Confirm asset `dw-cli-${version}-macos-arm64.zip` is listed. Download it - (or fetch bytes for sha only). -4. `sha256sum` the zip. -5. Checkout tap with `HOMEBREW_TAP_TOKEN`. -6. Parse current `version` from `formula/dw.rb`. If equal to `${version}`, - exit 0 (“already promoted”). -7. Rewrite `url`, `sha256`, `version`. Keep `desc`, `homepage`, `install`. - Update `homepage` to `https://github.com/mulesoft/data-weave-cli` if it - still points at `mulesoft-labs`. -8. `dry_run`: print the new formula and stop. -9. Else: commit on `promote-dw-` and open a PR to the tap default - branch (`master`). PR body lists tag, asset URL, sha256. - -Idempotent: a second run after the PR merged hits the exists-check. A second -run while the PR is open may fail on branch exists — recreate or reuse the -branch and force-update only that promote branch (not `master`). - -## Formula shape after promote - -```ruby -class Dw < Formula - desc "DataWeave CLI" - homepage "https://github.com/mulesoft/data-weave-cli" - url "https://github.com/mulesoft/data-weave-cli/releases/download/v/dw-cli--macos-arm64.zip" - sha256 "" - version "" - - def install - prefix.install "bin" - prefix.install "libs" - end -end -``` - -`install` assumes the zip still contains `bin/` and `libs/` (current -`native-cli:distro` layout). If the zip layout changes, this job must fail -loudly rather than invent paths — optional sanity: unzip listing must include -`bin/dw` (or `bin/dw.exe` is N/A on macOS). - -## Secrets / permissions - -- `HOMEBREW_TAP_TOKEN`: PAT or GitHub App installation token, repo scope on - the tap. -- Workflow `contents: read` on this repo (release download via `GITHUB_TOKEN`). -- Do not use `GITHUB_TOKEN` to push the tap (wrong repo). - -## Testing - -- Unit-testable helpers if we extract rewrite/parse into a small script - (`scripts/promote/homebrew.mjs` or `.sh`): parse version from formula, - render new formula, reject bad tags. -- CI of this repo does **not** run promote on PRs (dispatch only). -- Manual `dry_run` against a real tag after merge. - -## Success criteria - -- Dispatch with a tag that has the macOS zip opens a tap PR (or no-ops if - already at that version). -- `dry_run` makes no tap changes. -- `release.yml` unchanged. - -## Out of scope (follow-up) - -- npm / PyPI promote jobs in the same workflow file (add later as extra jobs). -- Multi-arch Homebrew bottles. -- Auto-dispatch after `release.yml` (stay manual).