Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}"
41 changes: 41 additions & 0 deletions .goreleaser.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }}'
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
33 changes: 18 additions & 15 deletions Formula/squad-oc.rb → Casks/squad-oc.rb
Original file line numberDiff line numberDiff line change
@@ -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
1 change: 1 addition & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/get-started.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
102 changes: 97 additions & 5 deletions internal/version/packaging_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)`)
Expand All@@ -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)
}
}
Expand All@@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions packaging/winget/xeaser.squad-oc.installer.yaml
Original file line numberDiff line numberDiff line change
@@ -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
18 changes: 18 additions & 0 deletions packaging/winget/xeaser.squad-oc.locale.en-US.yaml
Original file line numberDiff line numberDiff line change
@@ -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
27 changes: 3 additions & 24 deletions packaging/winget/xeaser.squad-oc.yaml
Original file line numberDiff line numberDiff line change
@@ -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
Loading