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
65 changes: 65 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,20 @@ jobs:
# APPLE_SIGNING_IDENTITY / APPLE_TEAM_ID / APPLE_API_* arrive from
# $GITHUB_ENV above, and only when the secrets were non-empty. Nothing
# sets them to "" — see the job comment.
#
# The minisign keypair behind the in-app updater. This one is NOT
# optional the way the Apple identity is: `plugins.updater.pubkey` in
# tauri.conf.json makes signing mandatory, so a checkout without the
# private key cannot build the panel at all — tauri stops with "a
# public key has been found, but no private key". That is the right
# trade (an unsigned updater artifact is worse than none), but it does
# mean a fork must either add these two secrets or drop the
# `plugins.updater` block before it can build the .app.
#
# The password is legitimately empty for a key generated without one,
# so it is passed through as-is rather than gated on being non-empty.
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
# No tagName: tauri-action then only builds. The release itself is
# assembled once, in the `release` job, so every artifact lands on one
Expand Down Expand Up @@ -220,6 +234,50 @@ jobs:
fi
ls -l dist

# The other half of the panel's release: what the in-app updater reads.
#
# `createUpdaterArtifacts` makes tauri tar the finished .app (after it has
# been notarized and stapled, so an updated copy verifies offline too) and
# sign the tarball with the minisign key. The feed is a `latest.json` we
# write here rather than one tauri produces, because the download URL is
# only knowable once we have decided what the asset is called.
#
# Both macOS platform keys point at the same file: the build is universal,
# so one bundle genuinely is the update for both architectures. The
# updater matches on the key, and an Intel Mac that finds no
# `darwin-x86_64` entry concludes there is no update for it.
- name: Collect updater bundle and latest.json
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
bundle="app/src-tauri/target/universal-apple-darwin/release/bundle/macos"
tarball="$bundle/patchbay.app.tar.gz"
if [ ! -f "$tarball" ] || [ ! -f "$tarball.sig" ]; then
echo "::error::no signed updater bundle at $tarball(.sig) — is TAURI_SIGNING_PRIVATE_KEY set?"
ls -l "$bundle" || true
exit 1
fi
asset="patchbay-$tag-universal-apple-darwin.app.tar.gz"
cp "$tarball" "dist/$asset"
cp "$tarball.sig" "dist/$asset.sig"
# Version without the leading v: the updater parses it as semver and
# compares it against the running app's own version.
url="https://github.com/${GITHUB_REPOSITORY}/releases/download/$tag/$asset"
jq -n \
--arg version "${{ needs.verify.outputs.version }}" \
--arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg signature "$(cat "$tarball.sig")" \
--arg url "$url" \
'{
version: $version,
pub_date: $pub_date,
platforms: {
"darwin-aarch64": { signature: $signature, url: $url },
"darwin-x86_64": { signature: $signature, url: $url }
}
}' > dist/latest.json
cat dist/latest.json

# tauri notarizes and staples the .app, then signs the .dmg around it —
# but it does not notarize the .dmg itself. An un-notarized disk image
# still makes Gatekeeper do an online check on first open, and shows a
Expand Down Expand Up @@ -288,7 +346,14 @@ jobs:
# Appends the commit/PR list under whatever body we supplied.
generate_release_notes: true
fail_on_unmatched_files: true
# `*.tar.gz` covers both the CLI tarballs and the panel's updater
# bundle. The `.sig` and `latest.json` beside it are what the in-app
# updater fetches — `latest.json` has to be a release asset under this
# exact name, because the endpoint in tauri.conf.json is the
# `releases/latest/download/latest.json` redirect.
files: |
artifacts/*.tar.gz
artifacts/*.tar.gz.sig
artifacts/latest.json
artifacts/*.dmg
artifacts/SHA256SUMS-*.txt
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **patchbay updates itself** — the panel checks the signed release feed a
couple of seconds after launch and, when a newer build exists, says so in a
slim banner above the board: `update and relaunch`, or `not now`. Applying is
always a click — patchbay is a thing you open to answer a question about your
logins, and an update must never be what happens instead — and the banner sits
in the flow rather than over the board, so the answer you came for is never
covered. The download is verified against a minisign public key compiled into
the app before anything is installed, which is the whole reason this is
allowed to be automatic at all. A failed check is silent: an offline machine
has no update to offer, and that is not news. `not now` lasts for the session
and is written nowhere, because something you have neither accepted nor
refused should be asked again next launch. `pb check-updates` gained the
matching row: patchbay reports its own installed version (the build answering
the question, not whichever `pb` is on `PATH`) against the newest GitHub
release, on the same 24-hour cache and the same shared rate limit as every
other tool. Its `UPDATE WITH` is a human instruction — download the DMG, curl
the CLI tarball — the way `gcloud`'s is, rather than a command that does not
exist.

- **The panel writes to the key vault** — `add key` opens a form (id, provider,
label, a masked secret field, with purpose, scopes, expiry, endpoint and the
rotation checkbox folded away), and every row gets a trash affordance behind
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,11 @@ neon 2.38.2 3.1.1 brew brew upgrade neonctl
wrangler 4.105.0 4.122.0 bun bun add -g wrangler@latest
vercel 42.2.0 58.11.0 pnpm pnpm add -g vercel@latest
gcloud 578.0.0 — self-managed
patchbay 0.2.0 0.3.0 github download the DMG / curl the CLI tarball from the release page
```

**patchbay reports itself** in that table too — installed is the build answering the question, latest is the newest GitHub release — because a tool that tells you twenty-three CLIs are behind while saying nothing about itself is the one row you would have to remember to check by hand. In the panel you do not even get the command: when a newer *signed* build exists it offers `update and relaunch` in a banner above the board, verifies the signature, installs in place and restarts.

patchbay works out **how each tool was installed** and asks the right place. Every Homebrew tool is answered by a single `brew outdated --json=v2` call, npm/bun/pnpm globals by one small registry request each, and self-updating vendor CLIs (`gcloud`, `az`) by nothing at all — they get their own update command instead of a made-up version number. `latest: —` always means "could not check", never "up to date".

Results are cached at `~/.config/patchbay/versions.json` for 24 hours. **`pb status` only ever reads that cache** — it never executes a binary and never touches the network, so the board stays in the tens of milliseconds whether the cache is warm or cold. A warm cache adds an update marker to the board:
Expand Down
6 changes: 6 additions & 0 deletions app/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
},
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
Expand Down
Loading
Loading