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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Homebrew: install through a Formula (`brew install openclaw/tap/goplaces`) instead of the retired Cask; quarantine is preserved.

## 0.4.11 - 2026-09-13

- Release tooling: fix the Developer ID signing canary under the isolated producer home so signed macOS binaries publish again; v0.4.10 was tagged but shipped no binaries, and 0.4.11 contains everything listed under 0.4.10.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![GitHub release](https://img.shields.io/github/v/release/openclaw/goplaces?style=flat-square)](https://github.com/openclaw/goplaces/releases/latest)
[![Go](https://img.shields.io/badge/Go-1.26.8-00ADD8?style=flat-square&logo=go&logoColor=white)](https://go.dev/dl/)
[![License](https://img.shields.io/github/license/openclaw/goplaces?style=flat-square)](LICENSE)
[![Homebrew](https://img.shields.io/badge/Homebrew-openclaw%2Ftap-FBB040?style=flat-square&logo=homebrew&logoColor=black)](https://github.com/openclaw/homebrew-tap/blob/main/Casks/goplaces.rb)
[![Homebrew](https://img.shields.io/badge/Homebrew-openclaw%2Ftap-FBB040?style=flat-square&logo=homebrew&logoColor=black)](https://github.com/openclaw/homebrew-tap/blob/main/Formula/goplaces.rb)
[![Docs](https://img.shields.io/badge/docs-goplaces.sh-3b82f6?style=flat-square)](https://goplaces.sh)

`goplaces` is a Go client and CLI for Google Places API (New) and selected Routes API workflows. It is for terminal use, shell automation, agents, and Go programs that need typed requests and JSON output without hand-building field masks or request payloads.
Expand All @@ -16,9 +16,11 @@
Homebrew installs the published binary on macOS or Linux:

```sh
brew install --cask openclaw/tap/goplaces
brew install openclaw/tap/goplaces
```

If you installed the retired Cask, run `brew uninstall --cask goplaces` first, then install the Formula.

With Go 1.26.8 or newer:

```sh
Expand Down
6 changes: 3 additions & 3 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ <h1>Places &amp;<br>routes, in&nbsp;Go.</h1>
</div>
<div class="qi" role="group" aria-label="Quick install">
<span class="dollar">$</span>
<span class="text" id="qi-cmd">brew install --cask openclaw/tap/goplaces</span>
<span class="text" id="qi-cmd">brew install openclaw/tap/goplaces</span>
<button class="copy" data-copy="#qi-cmd" aria-label="Copy install command">Copy</button>
</div>
<div class="stats" aria-label="Project at a glance">
Expand Down Expand Up @@ -199,11 +199,11 @@ <h3>Three steps to your first call</h3>
</div>

<div role="tabpanel" id="t-brew" class="tab-panel is-active">
<div class="codebar"><span class="pill">shell</span><button class="copy" data-copy="#code-brew" data-copy-text="brew install --cask openclaw/tap/goplaces
<div class="codebar"><span class="pill">shell</span><button class="copy" data-copy="#code-brew" data-copy-text="brew install openclaw/tap/goplaces
export GOOGLE_PLACES_API_KEY=&quot;...&quot;
goplaces search &quot;bookstore&quot;">Copy</button></div>
<pre id="code-brew"><span class="tk-c"># Install via the OpenClaw tap</span>
<span class="tk-prompt">$ </span>brew install --cask openclaw/tap/goplaces
<span class="tk-prompt">$ </span>brew install openclaw/tap/goplaces
<span class="tk-prompt">$ </span>export GOOGLE_PLACES_API_KEY=<span class="tk-str">"..."</span>
<span class="tk-prompt">$ </span>goplaces search <span class="tk-str">"bookstore"</span></pre>
</div>
Expand Down
43 changes: 24 additions & 19 deletions docs/releasing-homebrew.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# goplaces Homebrew Release Playbook

This is the required `verified-hashes-v1` handoff contract for a published, notarized goplaces release. It is not currently runnable; see [Current Blocker](#current-blocker). Do not mutate `openclaw/homebrew-tap` or change public install instructions until that blocker is resolved under a serialized gate.
This is the required `verified-hashes-v1` Formula handoff contract for a published, notarized goplaces release. Run `scripts/release-local homebrew vX.Y.Z` under the serialized release gate before release closeout advances `main`. The command targets `Formula/goplaces.rb`; local release preparation must not generate a Cask or publish Homebrew metadata.

## Frozen Tap Trust Anchor

- Repository: `openclaw/homebrew-tap`.
- Pinned base commit: `45b93a0b3de27e46b636a0cef819fb1ecef25bcd`.
- Pinned base commit: `104616d9828cf28202bccff19c0738f179c2a3f8`.
- Workflow: `.github/workflows/update-formula.yml`.
- Workflow numeric ID: `220664022`.
- Updater: `.github/scripts/update_formula.py`.
- Required updater marker: `# verified-hashes-v1`.

The protected default branch, workflow bytes, updater bytes, and marker must match this pinned contract before dispatch. A newer tap commit is not implicitly trusted.
This base includes the [Formula migration](https://github.com/openclaw/homebrew-tap/pull/56) and the [literal-URL updater fix](https://github.com/openclaw/homebrew-tap/pull/57). Verified updates must retain literal release URLs so Homebrew can infer the version without an explicit version declaration.

The protected default branch, workflow bytes, updater bytes (including `.github/scripts/formula_text.py` at the pinned commit), and marker must match this pinned contract before dispatch. A newer tap commit is not implicitly trusted.

## Required Handoff

Expand All @@ -21,7 +23,7 @@ Only begin after the GitHub release is published and the newest protected-main A
2. Recheck the goplaces signed tag object, peeled commit, protected `main`, release ID, asset IDs, sizes, digests, and newest verifier run.
3. Dispatch the pinned tap workflow from protected current default `main` with GitHub API version `2026-03-10`. Supply `formula=goplaces`, the release tag, source repository, explicit artifact template, all four platform hashes, source tag object, source tag commit, and a unique request ID.
4. Require the live tap workflow’s numeric ID to remain exactly `220664022`. Require the run record’s canonical path to equal `.github/workflows/update-formula.yml`; pin the protected tap branch and head SHA separately. Snapshot existing run IDs, consume the response’s numeric `workflow_run_id`, reject any pre-existing or concurrent substitution, watch that exact run, and recheck the same canonical path after it completes.
5. Require the workflow head SHA to equal protected current tap default, and require exactly one new tap commit. That commit must be the direct child of the pinned base and carry the exact updater-defined provenance trailers for the source repository, tag object, tag commit, and request ID. The four verified hashes remain bound by the dispatch record and the exact Formula bytes checked next.
5. Require the workflow head SHA to equal protected current tap default, and require exactly one new tap commit. That commit must be the direct child of the pinned base and carry the exact updater-defined provenance trailers for the source repository, tag object, tag commit, and request ID. The four verified hashes remain bound by the dispatch record and the exact Formula bytes checked next. An already-current Formula still receives one provenance commit with an unchanged tree; otherwise only `Formula/goplaces.rb` may change.
6. Read `Formula/goplaces.rb` from that exact tap commit, not from a moving branch. Require the exact version, four release URLs, and four verified hashes. The tap must not rebuild, resign, repackage, mirror, or substitute release assets.
7. Install from a clean checkout pinned to that exact tap commit. Installed binary bytes must equal the corresponding verified release archive member. Verify architecture, reported version, signing identity, Team ID, canonical designated requirement, hardened runtime, secure timestamp, and online notarization.
8. Run the package test, then recheck the goplaces tag refs, source default branch, tap base, tap commit, workflow/updater bytes, and exact live tap default-branch head as the final external closeout action. Any movement fails closed.
Expand All @@ -32,27 +34,30 @@ Each gated attempt persists mode-`0400` dispatch intent, bound-run, tap-result,

An unbound intent with no exact run after the bounded, fully paginated reconciliation window fails closed and sends no second dispatch. Keep the serialized gate held and preserve the state. Do not delete the intent merely because the API is empty; a manual reset is allowed only after an independent review proves the POST never occurred and records that proof. Otherwise leave the intent intact and escalate the release as blocked.

## Current Blocker

At pinned tap commit `45b93a0b3de27e46b636a0cef819fb1ecef25bcd`, `Formula/goplaces.rb` does not exist. The generic `verified-hashes-v1` updater accepts Formula input and therefore cannot update the existing `Casks/goplaces.rb`.

The current published Cask also strips `com.apple.quarantine`. Removing that generator behavior from local goplaces release preparation is necessary, but it does not rewrite the already-published tap entry and does not make the generic Formula handoff work.
## Installation and Migration

Therefore:

- do not dispatch the tap workflow for goplaces;
- do not claim exact-run, exact-commit, install-byte, signature, or notarization proof through Homebrew;
- do not change README or website installation text away from the currently published Cask command;
- resolve the tap side under separate review. Either the compatible Formula/Cask change must occur within the one authorized direct-child handoff commit, or the reviewed tap change must establish a new pinned base and updated trust anchor before dispatch. Never insert a pre-dispatch tap commit while continuing to claim the old pinned base.

After the blocker is resolved and the full handoff passes, public install text can change to:
The retired `Casks/goplaces.rb` is absent. Install the Formula with:

```bash
brew install openclaw/tap/goplaces
```

Until then, the existing published-install documentation remains:
For an existing Cask installation, remove it before installing the Formula:

```bash
brew install --cask openclaw/tap/goplaces
brew uninstall --cask goplaces
brew install openclaw/tap/goplaces
```

## Handoff After Release Closeout

The producer currently requires the release tag commit to equal protected current `main`, and its draft-to-published identity comparison still includes GitHub's changing `browser_download_url`. The v0.4.11 publication recovery recorded both limitations before this Formula migration. Do not move the tag, overwrite frozen release records, republish assets, or claim that the producer command passed when it did not.

For an already-published release such as v0.4.11, an equivalent serialized handoff must retain the checks above, with these explicit bindings:

- Freeze current protected source `main` independently from the original signed release commit; verify raw ancestry and that the release verifier workflow, signer policy, and verification scripts are unchanged since the successful published-asset proof.
- Revalidate the newest successful published-asset run at its original protected-main SHA, both native proof markers, and the exact frozen numeric asset inventory. Validate canonical URLs for each state, allowing only the documented draft-to-tag URL transition when comparing the original draft with the published record.
- Download and verify all published assets twice, derive all four handoff hashes from the second copy, then dispatch the pinned updater once with a unique request ID and bind the returned numeric run ID. Preserve read-only intent, run, result, installation, and completion receipts beside the original release state.
- Verify one direct-child provenance commit, exact Formula bytes, an install from that commit, binary equality, package test, signature and notarization, and final unchanged source/tag/tap identities. A missing or ambiguous dispatch response requires reconciliation, never a blind retry.

This recovery is a Homebrew handoff only. The original Apple-silicon and Intel proofs remain bound to their original release commit; they are not represented as fresh CI runs on the later documentation commit.
4 changes: 2 additions & 2 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ After publication:

- verify every public archive and checksum against the frozen release record;
- verify `go install github.com/steipete/goplaces/cmd/goplaces@vX.Y.Z` reports `X.Y.Z`, then retry `@latest` after the module proxy catches up;
- complete the Homebrew handoff only if the separate gate in [releasing-homebrew.md](releasing-homebrew.md) is unblocked;
- complete the Formula handoff under the separate gate in [releasing-homebrew.md](releasing-homebrew.md);
- reopen the next patch section as `Unreleased`, commit that closeout separately, pull with `--ff-only`, and finish on clean synchronized `main`.

## Homebrew Status

The pinned `verified-hashes-v1` tap handoff is currently blocked for goplaces. Do not run or claim Homebrew handoff proof until [the documented blocker](releasing-homebrew.md#current-blocker) is resolved under its own serialized gate. Until then, public installation text remains the already-published Cask command.
The pinned `verified-hashes-v1` handoff updates `Formula/goplaces.rb` in `openclaw/homebrew-tap`. The Formula installs published archives without rebuilding or stripping quarantine. GoReleaser does not generate or publish Homebrew metadata. Follow [the Homebrew playbook](releasing-homebrew.md), including its recovery procedure when release closeout has already advanced `main`.
49 changes: 38 additions & 11 deletions scripts/release-local
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ readonly EXPECTED_GO_VERSION="go1.26.8"
readonly VERIFIER_WORKFLOW=".github/workflows/release-assets.yml"
readonly VERIFIER_WORKFLOW_ID="311062804"
readonly TAP_REPOSITORY="openclaw/homebrew-tap"
readonly TAP_BASE="45b93a0b3de27e46b636a0cef819fb1ecef25bcd"
readonly TAP_BASE="104616d9828cf28202bccff19c0738f179c2a3f8"
readonly TAP_WORKFLOW=".github/workflows/update-formula.yml"
readonly TAP_WORKFLOW_ID="220664022"
readonly TAP_UPDATER=".github/scripts/update_formula.py"
Expand Down Expand Up @@ -2452,7 +2452,6 @@ tap_trust_preflight() {
decode_api_content "$current_updater" "$updater_bytes"
grep -Fqx '# verified-hashes-v1' "$updater_bytes" || die "tap updater lacks the exact verified-hashes-v1 marker"

# Current contract is expected to stop here until the gated Formula seed lands.
fetch_content_record "$TAP_REPOSITORY" Formula/goplaces.rb "$tap_head" "$current_formula" || die "Homebrew blocked: Formula/goplaces.rb is absent at protected current tap"
formula_file="${output_dir}/goplaces.rb"
decode_api_content "$current_formula" "$formula_file"
Expand Down Expand Up @@ -2772,16 +2771,37 @@ formula_pair_count() {
' "$file"
}

validate_formula_version() {
# Tokenize without executing the Formula or treating strings/comments as DSL calls.
/usr/bin/env -i PATH="$SECRET_SCOPE_SYSTEM_PATH" /usr/bin/ruby --disable-gems -rripper - "$1" "$2" <<'RUBY' || die "tap Formula has an invalid or duplicate version declaration"
source = File.read(ARGV.fetch(0))
abort "invalid Formula Ruby syntax" unless Ripper.sexp(source)
expected = %(version "#{ARGV.fetch(1)}")
ignored = [:on_sp, :on_nl, :on_ignored_nl, :on_comment]
tokens = Ripper.lex(source).reject { |token| ignored.include?(token[1]) }
count = 0
tokens.each_with_index do |token, index|
next unless token[1] == :on_ident && token[2] == "version"
following = tokens[index + 1]
next if following && following[1] == :on_period && following[2] == "."
previous = index.positive? ? tokens[index - 1] : nil
abort "noncanonical Formula version call" if previous && previous[1] == :on_period
abort "noncanonical Formula version call" unless source.lines[token[0][0] - 1].strip == expected
count += 1
end
abort "duplicate Formula version calls" if count > 1
RUBY
}

validate_formula_content() {
local file="$1" tag="$2" darwin_amd64="$3" darwin_arm64="$4" linux_amd64="$5" linux_arm64="$6" version url_count digest_count version_count
local file="$1" tag="$2" darwin_amd64="$3" darwin_arm64="$4" linux_amd64="$5" linux_arm64="$6" version url_count digest_count
local darwin_amd64_url darwin_arm64_url linux_amd64_url linux_arm64_url
version="$(version_from_tag "$tag")"
darwin_amd64_url="https://github.com/${REPOSITORY}/releases/download/${tag}/goplaces_${version}_darwin_amd64.tar.gz"
darwin_arm64_url="https://github.com/${REPOSITORY}/releases/download/${tag}/goplaces_${version}_darwin_arm64.tar.gz"
linux_amd64_url="https://github.com/${REPOSITORY}/releases/download/${tag}/goplaces_${version}_linux_amd64.tar.gz"
linux_arm64_url="https://github.com/${REPOSITORY}/releases/download/${tag}/goplaces_${version}_linux_arm64.tar.gz"
version_count="$(/usr/bin/awk -v expected="version \"${version}\"" '{line=$0; sub(/[[:space:]]*#.*/, "", line); sub(/^[[:space:]]+/, "", line); sub(/[[:space:]]+$/, "", line); if (line == expected) count++} END {print count+0}' "$file")"
[[ "$version_count" == 1 ]] || die "tap Formula lacks one exact active version declaration"
validate_formula_version "$file" "$version"
url_count="$(/usr/bin/awk '{line=$0; sub(/[[:space:]]*#.*/, "", line); sub(/^[[:space:]]+/, "", line); if (line ~ /^url "/) count++} END {print count+0}' "$file")"
digest_count="$(/usr/bin/awk '{line=$0; sub(/[[:space:]]*#.*/, "", line); sub(/^[[:space:]]+/, "", line); if (line ~ /^sha256 "/) count++} END {print count+0}' "$file")"
[[ "$url_count" == 4 && "$digest_count" == 4 ]] || die "tap Formula must contain exactly four active URL/SHA stanzas"
Expand All @@ -2804,6 +2824,18 @@ validate_tap_commit_record() {
[[ "$message" == "$expected_message" ]] || die "tap commit message differs from the pinned updater contract"
}

validate_tap_compare_record() {
local file="$1" old_head="$2" new_head="$3"
jq -e --arg old "$old_head" --arg new "$new_head" '
select(.status == "ahead" and .ahead_by == 1 and .behind_by == 0 and .total_commits == 1) |
select(.base_commit.sha == $old and .merge_base_commit.sha == $old and .head_commit.sha == $new) |
select((.commits | length) == 1 and .commits[0].sha == $new) |
select((.files | type) == "array") |
select((.files | length) == 0 or
((.files | length) == 1 and .files[0].filename == "Formula/goplaces.rb" and (.files[0].status == "added" or .files[0].status == "modified")))
' "$file" >/dev/null || die "tap result is not one exact direct-child Formula commit"
}

verify_tap_commit() {
local work="$1" tag="$2" request_id="$3" old_head="$4" darwin_amd64="$5" darwin_arm64="$6" linux_amd64="$7" linux_arm64="$8"
local branch_json new_head compare_json commit_json expected_message formula_json result_formula
Expand All @@ -2820,12 +2852,7 @@ verify_tap_commit() {
[[ "$new_head" != "$old_head" ]] || die "tap workflow did not create a commit"

gh_api "repos/${TAP_REPOSITORY}/compare/${old_head}...${new_head}" > "$compare_json"
jq -e --arg old "$old_head" --arg new "$new_head" '
select(.status == "ahead" and .ahead_by == 1 and .behind_by == 0 and .total_commits == 1) |
select(.base_commit.sha == $old and .merge_base_commit.sha == $old and .head_commit.sha == $new) |
select((.commits | length) == 1 and .commits[0].sha == $new) |
select((.files | length) == 1 and .files[0].filename == "Formula/goplaces.rb" and (.files[0].status == "added" or .files[0].status == "modified"))
' "$compare_json" >/dev/null || die "tap result is not one exact direct-child Formula commit"
validate_tap_compare_record "$compare_json" "$old_head" "$new_head"

gh_api "repos/${TAP_REPOSITORY}/commits/${new_head}" > "$commit_json"
expected_message="goplaces: update formula for ${tag}
Expand Down
Loading
Loading