From 3e1bcb4e45e8fedd79857842aae63a5ea87dd818 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Fri, 24 Apr 2026 16:56:39 +1200 Subject: [PATCH 1/3] feat: add GoReleaser, release workflow, and Homebrew formula Closes #32. - .goreleaser.yaml builds darwin/linux amd64/arm64 binaries with CGO_ENABLED=0 and publishes a formula to agent-receipts/homebrew-tap via feature branch + PR. Release creation stays in the workflow so formula SHAs match the uploaded binaries. - release.yml runs on v* tags, vet/test, GoReleaser, then gh release create with dist/*.tar.gz + checksums.txt. - publish.yml slimmed to the pkg.go.dev indexing ping; build and test validation now live in release.yml. - cmd/dashboard gains --version (required by the formula test block). - scripts/release.sh pushes the tag instead of creating the release directly, so the workflow drives the whole pipeline. --- .github/workflows/publish.yml | 21 ++-------- .github/workflows/release.yml | 56 +++++++++++++++++++++++++ .goreleaser.yaml | 79 +++++++++++++++++++++++++++++++++++ README.md | 10 +++++ cmd/dashboard/main.go | 24 +++++++++++ scripts/release.sh | 16 +++---- 6 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yaml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7d27b65..e830f3c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,26 +8,13 @@ permissions: contents: read jobs: - publish: + # Nudges proxy.golang.org to fetch the new version so it shows up on + # pkg.go.dev without waiting for the next scheduled crawl. Build and + # test validation happens in release.yml before the release is cut. + ping-pkg-go-dev: if: startsWith(github.ref_name, 'v') runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: go.mod - - - name: Vet - run: go vet ./... - - - name: Build - run: go build ./cmd/dashboard - - - name: Test - run: go test ./... - - name: Request pkg.go.dev indexing run: | URL="https://proxy.golang.org/github.com/agent-receipts/dashboard/@v/${GITHUB_REF_NAME}.info" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b0bf37c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + # Deployment environment lets you attach protection rules (required + # reviewers, wait timer, branch restrictions) to gate who can trigger + # a release and push to the Homebrew tap. Configure at: + # https://github.com/agent-receipts/dashboard/settings/environments + environment: release + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + + - name: Vet + run: go vet ./... + + - name: Test + run: go test ./... + + # Single GoReleaser run: builds, archives, pushes Homebrew formula. + # Release creation + binary upload happen in the next step with `gh` + # so the same dist/ artifacts (and therefore matching SHA256s) are + # used for both the formula and the release assets — re-running + # GoReleaser would produce different tar hashes. + - name: Build and publish Homebrew formula + uses: goreleaser/goreleaser-action@e24998b8b67b290c2fa8b7c14fcfa7de2c5c9b8c # v7 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean --skip=validate,announce + env: + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + + - name: Create GitHub release with binaries + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${GITHUB_REF_NAME}" \ + --title "dashboard ${GITHUB_REF_NAME}" \ + --generate-notes \ + dist/*.tar.gz \ + dist/checksums.txt diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..cf52311 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,79 @@ +version: 2 + +project_name: dashboard + +before: + hooks: + - go mod download + +builds: + - id: dashboard + main: ./cmd/dashboard + binary: dashboard + env: + - CGO_ENABLED=0 + goos: + - darwin + - linux + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X main.version=v{{.Version}} + +archives: + - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + formats: [tar.gz] + files: + - README.md + - LICENSE + +checksum: + name_template: "checksums.txt" + +# GitHub release is created outside GoReleaser so the workflow can upload +# the same dist/ artifacts to the release as were hashed into the Homebrew +# formula. Re-running GoReleaser would regenerate tarballs with different +# SHA256s and break `brew install`. +release: + disable: true + +brews: + - name: dashboard + repository: + owner: agent-receipts + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_TOKEN }}" + # Push the formula to a feature branch, then open a PR against + # main. Without an explicit branch, GoReleaser pushes to the + # default branch (main) and tries to open a main→main PR, which + # the GitHub API rejects — so the formula sneaks onto main and + # skips the `brew audit` gate. + branch: "bump-{{.ProjectName}}-{{.Version}}" + pull_request: + enabled: true + base: + branch: main + directory: Formula + url_template: "https://github.com/agent-receipts/dashboard/releases/download/v{{ .Version }}/{{ .ArtifactName }}" + homepage: "https://github.com/agent-receipts/dashboard" + description: "Local web UI for browsing Agent Receipts SQLite databases" + license: "Apache-2.0" + commit_author: + name: agent-receipts-bot + email: bot@agentreceipts.ai + commit_msg_template: "chore(dashboard): bump to v{{ .Version }}" + install: | + bin.install "dashboard" + test: | + system "#{bin}/dashboard", "--version" + # `brew outdated` and `brew livecheck` use this to detect new releases. + # Dashboard tags as plain `vX.Y.Z`, so `:github_latest` would work, but + # `:github_releases` with a regex keeps the strategy consistent with + # the other agent-receipts formulas and tolerates prereleases. + custom_block: | + livecheck do + url "https://github.com/agent-receipts/dashboard" + strategy :github_releases + regex(/^v?(\d+(?:\.\d+)+)$/i) + end diff --git a/README.md b/README.md index 9ee7508..ea96aea 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,16 @@ Lightweight local web UI for browsing [Agent Receipt](https://github.com/agent-r ## Install +Homebrew (macOS / Linux): + +```sh +brew install agent-receipts/tap/dashboard +``` + +Pre-built binaries for darwin/linux (amd64, arm64) are attached to each [GitHub release](https://github.com/agent-receipts/dashboard/releases). + +With a Go toolchain: + ```sh go install github.com/agent-receipts/dashboard/cmd/dashboard@latest ``` diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 24f3968..975eea1 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -7,12 +7,36 @@ import ( "log" "net/http" "os" + "runtime/debug" "github.com/agent-receipts/dashboard/internal/server" "github.com/agent-receipts/dashboard/internal/store" ) +// version is set at build time via -ldflags "-X main.version=vX.Y.Z". +// Falls back to the module version from Go's build info (set automatically +// for binaries installed with `go install`), then to "dev". +var version string + +func resolveVersion() string { + if version != "" { + return version + } + if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" { + return info.Main.Version + } + return "dev" +} + func main() { + if len(os.Args) > 1 { + switch os.Args[1] { + case "-version", "--version": + fmt.Printf("dashboard %s\n", resolveVersion()) + return + } + } + dbPath := flag.String("db", "", "path to receipts SQLite database") host := flag.String("host", "127.0.0.1", "address to bind to (use 0.0.0.0 for all interfaces)") port := flag.Int("port", 8080, "HTTP server port") diff --git a/scripts/release.sh b/scripts/release.sh index e057177..7828a79 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -76,17 +76,19 @@ go test ./... -count=1 echo "" echo "--- All checks passed" echo "" -echo "Will create release:" +echo "Will push tag:" echo " Tag: $TAG" -echo " Title: v$VERSION" +echo "" +echo "The Release workflow (.github/workflows/release.yml) will build binaries," +echo "publish the Homebrew formula, and create the GitHub release." echo "" read -rp "Proceed? [y/N] " confirm [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; } REPO_URL=$(gh repo view --json url -q '.url') -release_args=("$TAG" --title "v$VERSION" --generate-notes) -[[ "$VERSION" == *-* ]] && release_args+=(--prerelease) -gh release create "${release_args[@]}" +git tag -a "$TAG" -m "dashboard $TAG" +git push "$REMOTE_NAME" "$TAG" echo "" -echo "==> Released dashboard v$VERSION" -echo " ${REPO_URL}/releases/tag/$TAG" +echo "==> Pushed tag $TAG" +echo " Follow the release workflow: ${REPO_URL}/actions/workflows/release.yml" +echo " Release page: ${REPO_URL}/releases/tag/$TAG" From 2ec75932a3cf949562462d3b0905f86555d81544 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Fri, 24 Apr 2026 17:03:22 +1200 Subject: [PATCH 2/3] fix: address Copilot review on release pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - goreleaser: clarify that livecheck regex is stable-only (prereleases intentionally skipped so Homebrew users don't land on unstable). - release.yml: make release creation idempotent — check for an existing release and upload/clobber assets instead of failing on re-run. Add `--prerelease` for tags containing `-` (e.g. v1.0.0-beta.1). - scripts/release.sh: drop the `gh` CLI dependency. The workflow owns release creation now; the script only needs git + go. Derive the repo URL for the final info line from `git remote get-url`. --- .github/workflows/release.yml | 25 ++++++++++++++++++++----- .goreleaser.yaml | 4 +++- scripts/release.sh | 13 +++++++++---- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0bf37c..ca30abf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,12 +45,27 @@ jobs: env: HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + # Idempotent: if the release already exists (e.g. workflow re-run + # after a transient tap-push failure), upload assets to the + # existing release instead of failing on duplicate-create. - name: Create GitHub release with binaries env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh release create "${GITHUB_REF_NAME}" \ - --title "dashboard ${GITHUB_REF_NAME}" \ - --generate-notes \ - dist/*.tar.gz \ - dist/checksums.txt + prerelease_flag="" + if [[ "${GITHUB_REF_NAME}" == *-* ]]; then + prerelease_flag="--prerelease" + fi + + if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then + gh release upload "${GITHUB_REF_NAME}" --clobber \ + dist/*.tar.gz \ + dist/checksums.txt + else + gh release create "${GITHUB_REF_NAME}" \ + --title "dashboard ${GITHUB_REF_NAME}" \ + --generate-notes \ + ${prerelease_flag} \ + dist/*.tar.gz \ + dist/checksums.txt + fi diff --git a/.goreleaser.yaml b/.goreleaser.yaml index cf52311..5177f5e 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -70,7 +70,9 @@ brews: # `brew outdated` and `brew livecheck` use this to detect new releases. # Dashboard tags as plain `vX.Y.Z`, so `:github_latest` would work, but # `:github_releases` with a regex keeps the strategy consistent with - # the other agent-receipts formulas and tolerates prereleases. + # the other agent-receipts formulas. The regex matches stable tags + # only — prereleases like `v1.0.0-beta.1` are intentionally skipped + # so Homebrew users don't get bumped onto unstable versions. custom_block: | livecheck do url "https://github.com/agent-receipts/dashboard" diff --git a/scripts/release.sh b/scripts/release.sh index 7828a79..1f6f117 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -22,10 +22,9 @@ cd "$(git rev-parse --show-toplevel)" REMOTE_NAME="${REMOTE:-origin}" -# Preflight: ensure required tools are available +# Preflight: ensure required tools are available. The release workflow +# handles the GitHub Release itself, so this script only needs git + go. command -v go >/dev/null 2>&1 || fail "go is not installed" -command -v gh >/dev/null 2>&1 || fail "gh CLI is not installed — see https://cli.github.com" -gh auth status >/dev/null 2>&1 || fail "gh is not authenticated — run gh auth login" [[ $# -eq 1 ]] || usage @@ -85,7 +84,13 @@ echo "" read -rp "Proceed? [y/N] " confirm [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; } -REPO_URL=$(gh repo view --json url -q '.url') +# Derive the GitHub web URL from the remote so this script doesn't need +# the `gh` CLI. Handles both HTTPS and SSH remote formats. +REMOTE_URL=$(git remote get-url "$REMOTE_NAME") +REPO_URL=$(echo "$REMOTE_URL" | sed -E \ + -e 's|^git@github\.com:|https://github.com/|' \ + -e 's|\.git$||') + git tag -a "$TAG" -m "dashboard $TAG" git push "$REMOTE_NAME" "$TAG" echo "" From 8f044e63c895103669670381ffeb93be36396634 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Fri, 24 Apr 2026 17:09:22 +1200 Subject: [PATCH 3/3] fix: normalize ssh:// git remote URLs in release.sh Previously only the SCP-style `git@github.com:org/repo.git` form was rewritten to an https web URL. The URL-style `ssh://git@github.com/...` form would slip through unchanged and leave the final echo pointing at a non-browsable URL. Per Copilot review on #33. --- scripts/release.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/release.sh b/scripts/release.sh index 1f6f117..3de58e7 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -85,10 +85,12 @@ read -rp "Proceed? [y/N] " confirm [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; } # Derive the GitHub web URL from the remote so this script doesn't need -# the `gh` CLI. Handles both HTTPS and SSH remote formats. +# the `gh` CLI. Handles GitHub HTTPS remotes and common GitHub SSH forms +# (SCP-style `git@github.com:` and URL-style `ssh://git@github.com/`). REMOTE_URL=$(git remote get-url "$REMOTE_NAME") REPO_URL=$(echo "$REMOTE_URL" | sed -E \ -e 's|^git@github\.com:|https://github.com/|' \ + -e 's|^ssh://git@github\.com/|https://github.com/|' \ -e 's|\.git$||') git tag -a "$TAG" -m "dashboard $TAG"