From 0a7db97f91fa60435986616f52dba7a200db8511 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Fri, 31 Jul 2026 00:09:43 +0900 Subject: [PATCH 1/3] ci: publish the Homebrew formula to skyoo2003/homebrew-tap devcloud was never available via `brew install`, even though the release already builds every archive the formula needs. Add a `brews` section that writes Formula/devcloud.rb into skyoo2003/homebrew-tap on each tag. The tap lives in another repository, so the job's own GITHUB_TOKEN cannot write to it. Instead of a long-lived PAT, mint a GitHub App installation token that is scoped to homebrew-tap alone and expires with the job. The dry run skips minting it, since nothing is published there. The formula test only asserts the flag usage text: devcloud is a long-running server with no subcommands, so anything else would hang. While here, migrate archives to `formats:`, which `goreleaser check` has flagged as deprecated since v2.6. --- .github/workflows/release.yml | 12 ++++++++++++ .goreleaser.yaml | 24 ++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfd5cba..c0b603b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,6 +57,17 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + # Short-lived token scoped to skyoo2003/homebrew-tap only, for the brews + # section. Skipped on a dry run, where GoReleaser never publishes it. + - name: Mint a Homebrew tap token + id: tap_token + if: ${{ github.event.inputs.dry_run != 'true' }} + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.TAP_APP_ID }} + private-key: ${{ secrets.TAP_APP_PRIVATE_KEY }} + owner: skyoo2003 + repositories: homebrew-tap - name: Execute GoReleaser uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # goreleaser-action v7 with: @@ -65,6 +76,7 @@ jobs: args: release --clean ${{ steps.resolve_tag.outputs.extra_flags }} --release-notes changes/${{ steps.resolve_tag.outputs.tag }}.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_TOKEN: ${{ steps.tap_token.outputs.token }} - name: Upload assets if: ${{ github.event.inputs.dry_run != 'true' }} uses: actions/upload-artifact@v7 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7ea5be1..b0fce06 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -24,10 +24,10 @@ builds: archives: - id: devcloud - format: tar.gz + formats: [ tar.gz ] format_overrides: - goos: windows - format: zip + formats: [ zip ] files: - LICENSE - README.md @@ -40,6 +40,26 @@ checksum: algorithm: sha256 name_template: 'CHECKSUMS' +# Publishes Formula/devcloud.rb to skyoo2003/homebrew-tap. The token comes from +# a GitHub App installed only on that repository: the job's own GITHUB_TOKEN +# cannot write to another repo. +brews: +- ids: + - devcloud + repository: + owner: skyoo2003 + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_TOKEN }}" + homepage: "https://github.com/skyoo2003/devcloud" + description: "Local development companion for cloud-native apps" + license: "Apache-2.0" + install: | + bin.install "devcloud" + # devcloud is a long-running server with no subcommands, so the test only + # asserts the flag usage text -- starting it would hang the test. + test: | + assert_match "-config", shell_output("#{bin}/devcloud -h 2>&1") + dockers_v2: - images: - ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/{{ .ProjectName }} From 2e80a552ee366bf3cacdd2c47e9d876e8df92730 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Fri, 31 Jul 2026 00:12:41 +0900 Subject: [PATCH 2/3] docs: add the changie fragment for Homebrew tap publishing --- changes/unreleased/Added-20260731-001220.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changes/unreleased/Added-20260731-001220.yaml diff --git a/changes/unreleased/Added-20260731-001220.yaml b/changes/unreleased/Added-20260731-001220.yaml new file mode 100644 index 0000000..75c30b4 --- /dev/null +++ b/changes/unreleased/Added-20260731-001220.yaml @@ -0,0 +1,5 @@ +kind: Added +body: Homebrew installation via `brew install skyoo2003/tap/devcloud`, published to the tap on each release +time: 2026-07-31T00:12:20.839744+09:00 +custom: + Issue: "121" From 4a4f20358c0f1d51a95c89de7814ad40d8f4e5e1 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Fri, 31 Jul 2026 01:24:19 +0900 Subject: [PATCH 3/3] ci: read the Homebrew tap location from the workflow environment The tap owner and repository name were spelled out in both .goreleaser.yaml and the release workflow, so a move would have to be caught in two places. Declare them once as workflow-level env vars -- the owner from github.repository_owner -- and let both the token-minting step and the brews section read them. HOMEBREW_TAP_TOKEN keeps its unconditional assignment: the homebrew pipe renders the formula even under --snapshot, and an unset variable fails the template outright rather than resolving empty. A dry run gets a placeholder instead, which nothing ever uses because --snapshot publishes nothing. --- .github/workflows/release.yml | 17 +++++++++++++---- .goreleaser.yaml | 9 +++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0b603b..a384aab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,12 @@ on: type: boolean default: true +# Single source of truth for the Homebrew tap location: the token-minting step +# scopes itself to it, and .goreleaser.yaml reads both from the environment. +env: + HOMEBREW_TAP_OWNER: ${{ github.repository_owner }} + HOMEBREW_TAP_REPO: homebrew-tap + jobs: release: runs-on: ubuntu-latest @@ -57,7 +63,7 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Short-lived token scoped to skyoo2003/homebrew-tap only, for the brews + # Short-lived token scoped to the tap repository only, for the brews # section. Skipped on a dry run, where GoReleaser never publishes it. - name: Mint a Homebrew tap token id: tap_token @@ -66,8 +72,8 @@ jobs: with: app-id: ${{ secrets.TAP_APP_ID }} private-key: ${{ secrets.TAP_APP_PRIVATE_KEY }} - owner: skyoo2003 - repositories: homebrew-tap + owner: ${{ env.HOMEBREW_TAP_OWNER }} + repositories: ${{ env.HOMEBREW_TAP_REPO }} - name: Execute GoReleaser uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # goreleaser-action v7 with: @@ -76,7 +82,10 @@ jobs: args: release --clean ${{ steps.resolve_tag.outputs.extra_flags }} --release-notes changes/${{ steps.resolve_tag.outputs.tag }}.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HOMEBREW_TAP_TOKEN: ${{ steps.tap_token.outputs.token }} + # Always defined: {{ .Env.HOMEBREW_TAP_TOKEN }} fails to render if the + # variable is missing. On a dry run the minting step is skipped, and the + # placeholder is never used because --snapshot publishes nothing. + HOMEBREW_TAP_TOKEN: ${{ steps.tap_token.outputs.token || 'dry-run' }} - name: Upload assets if: ${{ github.event.inputs.dry_run != 'true' }} uses: actions/upload-artifact@v7 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b0fce06..9d155cf 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -40,17 +40,18 @@ checksum: algorithm: sha256 name_template: 'CHECKSUMS' -# Publishes Formula/devcloud.rb to skyoo2003/homebrew-tap. The token comes from +# Publishes Formula/devcloud.rb to the tap repo named by HOMEBREW_TAP_OWNER and +# HOMEBREW_TAP_REPO (set in .github/workflows/release.yml). The token comes from # a GitHub App installed only on that repository: the job's own GITHUB_TOKEN # cannot write to another repo. brews: - ids: - devcloud repository: - owner: skyoo2003 - name: homebrew-tap + owner: "{{ .Env.HOMEBREW_TAP_OWNER }}" + name: "{{ .Env.HOMEBREW_TAP_REPO }}" token: "{{ .Env.HOMEBREW_TAP_TOKEN }}" - homepage: "https://github.com/skyoo2003/devcloud" + homepage: "https://github.com/{{ .Env.GITHUB_REPOSITORY_OWNER }}/{{ .ProjectName }}" description: "Local development companion for cloud-native apps" license: "Apache-2.0" install: |