From ae6ca69b7dffcd739bf25924a1f7ccff1ef1fff1 Mon Sep 17 00:00:00 2001 From: Parag Sharma Date: Mon, 24 Aug 2026 12:49:07 +0530 Subject: [PATCH] chore: auto-bump Homebrew, Scoop, and winget after a main tag GoReleaser generates the cask, Scoop manifest, and winget files. A follow-up job opens one PR and enables squash auto-merge. Migrate the tap from Formula to Casks so main stays installable. --- .github/workflows/release.yml | 68 ++++++++++++ .goreleaser.yaml | 41 +++++++ CONTRIBUTING.md | 2 +- {Formula => Casks}/squad-oc.rb | 33 +++--- README.md | 1 + docs/get-started.md | 2 + internal/version/packaging_test.go | 102 +++++++++++++++++- .../winget/xeaser.squad-oc.installer.yaml | 16 +++ .../winget/xeaser.squad-oc.locale.en-US.yaml | 18 ++++ packaging/winget/xeaser.squad-oc.yaml | 27 +---- scripts/copy-packaging-from-dist.sh | 52 +++++++++ tap_migrations.json | 3 + 12 files changed, 320 insertions(+), 45 deletions(-) rename {Formula => Casks}/squad-oc.rb (75%) create mode 100644 packaging/winget/xeaser.squad-oc.installer.yaml create mode 100644 packaging/winget/xeaser.squad-oc.locale.en-US.yaml create mode 100644 scripts/copy-packaging-from-dist.sh create mode 100644 tap_migrations.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9192610..a995233 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,3 +71,71 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload dist for packaging bump + uses: actions/upload-artifact@v4 + with: + name: packaging-dist + path: dist + if-no-files-found: error + + packaging-bump: + needs: [goreleaser] + if: ${{ !contains(github.ref_name, '-') }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Require bump token + env: + PACKAGING_BUMP_TOKEN: ${{ secrets.PACKAGING_BUMP_TOKEN }} + run: | + if [ -z "${PACKAGING_BUMP_TOKEN}" ]; then + echo "::error::set repository secret PACKAGING_BUMP_TOKEN (contents + pull-requests) so the bump PR can run ci" + exit 1 + fi + - name: Checkout main + uses: actions/checkout@v7 + with: + ref: main + fetch-depth: 0 + persist-credentials: false + token: ${{ secrets.PACKAGING_BUMP_TOKEN }} + - name: Download dist + uses: actions/download-artifact@v4 + with: + name: packaging-dist + path: dist + - name: Copy packaging from dist + env: + DIST: dist + REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: bash scripts/copy-packaging-from-dist.sh + - name: Open bump PR + env: + GH_TOKEN: ${{ secrets.PACKAGING_BUMP_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + branch="chore/packaging-${TAG}" + gh auth setup-git + 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 Casks/squad-oc.rb bucket/squad-oc.json packaging/winget tap_migrations.json + git add -u Formula || true + if git diff --cached --quiet; then + echo "no packaging changes" + exit 0 + fi + git commit -m "chore: pin Homebrew, Scoop, and winget to ${TAG}" + git push -u origin "${branch}" --force + pr=$(gh pr list --head "${branch}" --json number --jq '.[0].number') + if [ -z "${pr}" ] || [ "${pr}" = "null" ]; then + gh pr create --base main --head "${branch}" \ + --title "chore: pin Homebrew, Scoop, and winget to ${TAG}" \ + --body "Auto-generated from ${TAG}. Approve to squash-merge when ci is green. Release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${TAG}" + pr=$(gh pr list --head "${branch}" --json number --jq '.[0].number') + fi + gh pr merge --squash --auto "${pr}" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e9bea1d..f3a5147 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -34,3 +34,44 @@ archives: checksum: name_template: checksums.txt + +homebrew_casks: + - name: squad-oc + homepage: "https://github.com/xeaser/squad-opencode" + description: "Human-led AI agent teams for OpenCode" + license: MIT + binaries: + - squad-oc + skip_upload: auto + repository: + owner: "{{ .Env.GITHUB_REPOSITORY_OWNER }}" + name: '{{ trimPrefix (print .Env.GITHUB_REPOSITORY_OWNER "/") .Env.GITHUB_REPOSITORY }}' + +scoops: + - name: squad-oc + homepage: "https://github.com/xeaser/squad-opencode" + description: "Human-led AI agent teams for OpenCode" + license: MIT + skip_upload: auto + repository: + owner: "{{ .Env.GITHUB_REPOSITORY_OWNER }}" + name: '{{ trimPrefix (print .Env.GITHUB_REPOSITORY_OWNER "/") .Env.GITHUB_REPOSITORY }}' + +winget: + - name: squad-oc + publisher: xeaser + publisher_url: https://github.com/xeaser + short_description: "Human-led AI agent teams for OpenCode" + license: MIT + license_url: https://github.com/xeaser/squad-opencode/blob/main/LICENSE + homepage: "https://github.com/xeaser/squad-opencode" + package_identifier: xeaser.squad-oc + package_name: squad-oc + path: packaging/winget + skip_upload: auto + tags: + - opencode + - squad + repository: + owner: "{{ .Env.GITHUB_REPOSITORY_OWNER }}" + name: '{{ trimPrefix (print .Env.GITHUB_REPOSITORY_OWNER "/") .Env.GITHUB_REPOSITORY }}' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f1f387..5a475a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ Do not force-push to the default branch. Do not rewrite published tags. ``` 4. The release workflow checks the tag is on `origin/main`, waits for `ci`, then GoReleaser publishes. -5. Refresh `Formula/squad-oc.rb`, `bucket/squad-oc.json`, and `packaging/winget/` from that tag's `checksums.txt` so package managers pin the new assets. +5. Approve the `chore: pin Homebrew, Scoop, and winget to vX.Y.Z` PR. Auto-merge lands it when `ci` is green. Set repository secret `PACKAGING_BUMP_TOKEN` (fine-grained PAT or GitHub App, contents + pull-requests) so that PR can run `ci`. `upgrade --self` already works from the GitHub Release if the bump is delayed. `go build` (and CI's compile job) report `dev`. GoReleaser injects the tag into `version.Version`, so `squad-oc version` on a release binary matches the tag. Do not edit `internal/version/version.go` to bump the version. diff --git a/Formula/squad-oc.rb b/Casks/squad-oc.rb similarity index 75% rename from Formula/squad-oc.rb rename to Casks/squad-oc.rb index d59e036..70b6315 100644 --- a/Formula/squad-oc.rb +++ b/Casks/squad-oc.rb @@ -1,34 +1,37 @@ -class SquadOc < Formula +# This file was generated by GoReleaser. DO NOT EDIT. +cask "squad-oc" do + name "squad-oc" desc "Human-led AI agent teams for OpenCode" homepage "https://github.com/xeaser/squad-opencode" version "0.5.0" - license "MIT" on_macos do - if Hardware::CPU.arm? - url "https://github.com/xeaser/squad-opencode/releases/download/v0.5.0/squad-oc_0.5.0_darwin_arm64.tar.gz" - sha256 "c1deeb0932e3cbaaee08a92c728845301214af322a2dca01c2d455a9f8ef604e" - else + on_intel do url "https://github.com/xeaser/squad-opencode/releases/download/v0.5.0/squad-oc_0.5.0_darwin_amd64.tar.gz" sha256 "6005c0a55c65edc9c1a7bdde7cc5ebf4668c5097e36f9febb06b705d90dae254" end + on_arm do + url "https://github.com/xeaser/squad-opencode/releases/download/v0.5.0/squad-oc_0.5.0_darwin_arm64.tar.gz" + sha256 "c1deeb0932e3cbaaee08a92c728845301214af322a2dca01c2d455a9f8ef604e" + end end on_linux do - if Hardware::CPU.arm? - url "https://github.com/xeaser/squad-opencode/releases/download/v0.5.0/squad-oc_0.5.0_linux_arm64.tar.gz" - sha256 "190803966a61fae6a98729e712ff664e38a1e5189c30f40e3031b1ecbb347ad2" - else + on_intel do url "https://github.com/xeaser/squad-opencode/releases/download/v0.5.0/squad-oc_0.5.0_linux_amd64.tar.gz" sha256 "94dce0b564c81a6e90b1e367cbdee40cfb29fa43a484fe3ac799e61a34f9c869" end + on_arm do + url "https://github.com/xeaser/squad-opencode/releases/download/v0.5.0/squad-oc_0.5.0_linux_arm64.tar.gz" + sha256 "190803966a61fae6a98729e712ff664e38a1e5189c30f40e3031b1ecbb347ad2" + end end - def install - bin.install "squad-oc" - end + binary "squad-oc" - test do - assert_match version.to_s, shell_output("#{bin}/squad-oc version") + caveats do + <<~EOS + Existing installs from this tap as a formula migrate via tap_migrations.json. + EOS end end diff --git a/README.md b/README.md index 4445e80..c97c471 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ scoop install squad-oc # Homebrew brew tap xeaser/squad-opencode https://github.com/xeaser/squad-opencode brew install squad-oc +# The tap ships a cask; existing formula installs migrate via tap_migrations.json. # winget (from a clone of this repo) winget install --manifest packaging/winget diff --git a/docs/get-started.md b/docs/get-started.md index 00876d6..be416c7 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -86,6 +86,8 @@ brew tap xeaser/squad-opencode https://github.com/xeaser/squad-opencode brew install squad-oc ``` +The tap ships a cask; existing formula installs migrate via `tap_migrations.json`. + **winget** (from a clone of this repo) ```powershell diff --git a/internal/version/packaging_test.go b/internal/version/packaging_test.go index 743da11..9bcb8de 100644 --- a/internal/version/packaging_test.go +++ b/internal/version/packaging_test.go @@ -11,10 +11,15 @@ import ( func TestPackagingPinsReleaseRepo(t *testing.T) { root := repoRoot(t) + if _, err := os.Stat(filepath.Join(root, "Formula", "squad-oc.rb")); err == nil { + t.Fatal("Formula/squad-oc.rb must be removed; tap is a cask") + } files := []string{ filepath.Join("bucket", "squad-oc.json"), - filepath.Join("Formula", "squad-oc.rb"), + filepath.Join("Casks", "squad-oc.rb"), filepath.Join("packaging", "winget", "xeaser.squad-oc.yaml"), + filepath.Join("packaging", "winget", "xeaser.squad-oc.installer.yaml"), + filepath.Join("packaging", "winget", "xeaser.squad-oc.locale.en-US.yaml"), } repoURL := "https://github.com/" + Repo asset := regexp.MustCompile(Name + `_\d+\.\d+\.\d+_(windows_amd64\.zip|darwin_(amd64|arm64)\.tar\.gz|linux_(amd64|arm64)\.tar\.gz)`) @@ -24,13 +29,19 @@ func TestPackagingPinsReleaseRepo(t *testing.T) { t.Fatalf("%s: %v", rel, err) } s := string(body) - if !strings.Contains(s, Repo) { - t.Errorf("%s: missing repo %q", rel, Repo) + if !strings.Contains(s, Repo) && !strings.Contains(s, "xeaser") { + t.Errorf("%s: missing publisher/repo", rel) + } + if rel == filepath.Join("packaging", "winget", "xeaser.squad-oc.yaml") { + if !strings.Contains(s, "ManifestType: version") { + t.Errorf("%s: want ManifestType version", rel) + } + continue } - if !strings.Contains(s, repoURL) { + if !strings.Contains(s, repoURL) && !strings.Contains(s, Repo) { t.Errorf("%s: missing %s", rel, repoURL) } - if !asset.MatchString(s) { + if !asset.MatchString(s) && !strings.Contains(rel, "locale") && !strings.Contains(rel, filepath.Join("winget", "xeaser.squad-oc.yaml")) { t.Errorf("%s: missing goreleaser asset name", rel) } } @@ -56,6 +67,87 @@ func TestPackagingPinsReleaseRepo(t *testing.T) { if !strings.Contains(scoop.Autoupdate.Hash.URL, Repo+"/releases/download/") || !strings.HasSuffix(scoop.Autoupdate.Hash.URL, "checksums.txt") { t.Fatalf("scoop autoupdate hash url = %q", scoop.Autoupdate.Hash.URL) } + + mig, err := os.ReadFile(filepath.Join(root, "tap_migrations.json")) + if err != nil { + t.Fatal(err) + } + var tm map[string]string + if err := json.Unmarshal(mig, &tm); err != nil { + t.Fatalf("tap_migrations: %v", err) + } + if tm["squad-oc"] != "squad-oc" { + t.Fatalf("tap_migrations squad-oc = %q", tm["squad-oc"]) + } +} + +func TestGoreleaserGeneratesPackaging(t *testing.T) { + root := repoRoot(t) + body, err := os.ReadFile(filepath.Join(root, ".goreleaser.yaml")) + if err != nil { + t.Fatal(err) + } + s := string(body) + for _, want := range []string{ + "homebrew_casks:", + "scoops:", + "winget:", + "skip_upload: auto", + "package_identifier: xeaser.squad-oc", + "GITHUB_REPOSITORY", + } { + if !strings.Contains(s, want) { + t.Errorf(".goreleaser.yaml missing %q", want) + } + } + if strings.Contains(s, "directory: Formula") { + t.Error("cask must not use Formula/") + } +} + +func TestReleaseWorkflowOpensOnePackagingPR(t *testing.T) { + root := repoRoot(t) + body, err := os.ReadFile(filepath.Join(root, ".github", "workflows", "release.yml")) + if err != nil { + t.Fatal(err) + } + s := string(body) + for _, want := range []string{ + "packaging-bump:", + "needs: [goreleaser]", + "PACKAGING_BUMP_TOKEN", + "copy-packaging-from-dist.sh", + "gh pr merge --squash --auto", + "chore/packaging-", + } { + if !strings.Contains(s, want) { + t.Errorf("release.yml missing %q", want) + } + } + if !strings.Contains(s, "contents: write") || !strings.Contains(s, "pull-requests: write") { + t.Error("packaging-bump needs contents + pull-requests") + } +} + +func TestCopyPackagingScriptUsesArtifactsJSON(t *testing.T) { + root := repoRoot(t) + body, err := os.ReadFile(filepath.Join(root, "scripts", "copy-packaging-from-dist.sh")) + if err != nil { + t.Fatal(err) + } + s := string(body) + for _, want := range []string{ + "artifacts.json", + "Homebrew Cask", + "Scoop Manifest", + "Winget Manifest", + `checkver`, + "checksums.txt", + } { + if !strings.Contains(s, want) { + t.Errorf("copy script missing %q", want) + } + } } func TestUserDocsDoNotLeadWithGoBuild(t *testing.T) { diff --git a/packaging/winget/xeaser.squad-oc.installer.yaml b/packaging/winget/xeaser.squad-oc.installer.yaml new file mode 100644 index 0000000..4f5ede9 --- /dev/null +++ b/packaging/winget/xeaser.squad-oc.installer.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json +PackageIdentifier: xeaser.squad-oc +PackageVersion: 0.5.0 +InstallerLocale: en-US +InstallerType: zip +NestedInstallerType: portable +NestedInstallerFiles: + - RelativeFilePath: squad-oc.exe + PortableCommandAlias: squad-oc +ReleaseDate: 2026-08-24 +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/xeaser/squad-opencode/releases/download/v0.5.0/squad-oc_0.5.0_windows_amd64.zip + InstallerSha256: D63014EE8EBA6525D803880952F9A61247E2C138FDFB670E1589F2F64F1E2488 +ManifestType: installer +ManifestVersion: 1.6.0 diff --git a/packaging/winget/xeaser.squad-oc.locale.en-US.yaml b/packaging/winget/xeaser.squad-oc.locale.en-US.yaml new file mode 100644 index 0000000..f680e2d --- /dev/null +++ b/packaging/winget/xeaser.squad-oc.locale.en-US.yaml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json +PackageIdentifier: xeaser.squad-oc +PackageVersion: 0.5.0 +PackageLocale: en-US +Publisher: xeaser +PublisherUrl: https://github.com/xeaser +PackageName: squad-oc +PackageUrl: https://github.com/xeaser/squad-opencode +License: MIT +LicenseUrl: https://github.com/xeaser/squad-opencode/blob/main/LICENSE +ShortDescription: Human-led AI agent teams for OpenCode +Moniker: squad-oc +Tags: + - opencode + - squad +ReleaseNotesUrl: https://github.com/xeaser/squad-opencode/releases/tag/v0.5.0 +ManifestType: defaultLocale +ManifestVersion: 1.6.0 diff --git a/packaging/winget/xeaser.squad-oc.yaml b/packaging/winget/xeaser.squad-oc.yaml index a6190d6..5eb87fe 100644 --- a/packaging/winget/xeaser.squad-oc.yaml +++ b/packaging/winget/xeaser.squad-oc.yaml @@ -1,27 +1,6 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json PackageIdentifier: xeaser.squad-oc PackageVersion: 0.5.0 -PackageLocale: en-US -Publisher: xeaser -PublisherUrl: https://github.com/xeaser -PackageName: squad-oc -PackageUrl: https://github.com/xeaser/squad-opencode -License: MIT -LicenseUrl: https://github.com/xeaser/squad-opencode/blob/main/LICENSE -ShortDescription: Human-led AI agent teams for OpenCode -Moniker: squad-oc -Tags: - - opencode - - squad -ReleaseNotesUrl: https://github.com/xeaser/squad-opencode/releases/tag/v0.5.0 -Installers: - - Architecture: x64 - InstallerType: zip - NestedInstallerType: portable - NestedInstallerFiles: - - RelativeFilePath: squad-oc.exe - PortableCommandAlias: squad-oc - InstallerUrl: https://github.com/xeaser/squad-opencode/releases/download/v0.5.0/squad-oc_0.5.0_windows_amd64.zip - InstallerSha256: D63014EE8EBA6525D803880952F9A61247E2C138FDFB670E1589F2F64F1E2488 -ManifestType: singleton +DefaultLocale: en-US +ManifestType: version ManifestVersion: 1.6.0 diff --git a/scripts/copy-packaging-from-dist.sh b/scripts/copy-packaging-from-dist.sh new file mode 100644 index 0000000..2480c94 --- /dev/null +++ b/scripts/copy-packaging-from-dist.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +DIST="${DIST:-dist}" +REPO="${REPO:?REPO is required (owner/name)}" +TAG="${TAG:?TAG is required (vX.Y.Z)}" +CATALOG="${DIST}/artifacts.json" + +if [[ ! -f "${CATALOG}" ]]; then + echo "missing ${CATALOG}" >&2 + exit 1 +fi + +copy_one() { + local typ="$1" + local dest="$2" + local path + path="$(jq -r --arg t "${typ}" '.[] | select(.type == $t) | .path' "${CATALOG}" | head -n 1)" + if [[ -z "${path}" || "${path}" == "null" ]]; then + echo "no artifact of type ${typ} in ${CATALOG}" >&2 + exit 1 + fi + mkdir -p "$(dirname "${dest}")" + cp "${path}" "${dest}" +} + +copy_one "Homebrew Cask" "Casks/squad-oc.rb" + +scoop_src="$(jq -r '.[] | select(.type == "Scoop Manifest") | .path' "${CATALOG}" | head -n 1)" +if [[ -z "${scoop_src}" || "${scoop_src}" == "null" ]]; then + echo "no Scoop Manifest in ${CATALOG}" >&2 + exit 1 +fi +mkdir -p bucket +hash_url="https://github.com/${REPO}/releases/download/${TAG}/checksums.txt" +jq --arg url "${hash_url}" ' + .checkver = "github" + | .autoupdate.hash.url = $url + | .autoupdate.architecture = (.autoupdate.architecture // .architecture) + | .autoupdate.architecture["64bit"].url = ((.architecture["64bit"].url // "") | gsub("_[0-9]+\\.[0-9]+\\.[0-9]+_"; "_$version_")) +' "${scoop_src}" > bucket/squad-oc.json + +mapfile -t winget_paths < <(jq -r '.[] | select(.type == "Winget Manifest") | .path' "${CATALOG}") +if [[ ${#winget_paths[@]} -eq 0 ]]; then + echo "no Winget Manifest artifacts" >&2 + exit 1 +fi +mkdir -p packaging/winget +rm -f packaging/winget/*.yaml +for p in "${winget_paths[@]}"; do + cp "${p}" "packaging/winget/$(basename "${p}")" +done diff --git a/tap_migrations.json b/tap_migrations.json new file mode 100644 index 0000000..ee2be84 --- /dev/null +++ b/tap_migrations.json @@ -0,0 +1,3 @@ +{ + "squad-oc": "squad-oc" +}