14 Commits
Author SHA1 Message Date
adrianbonpin 8bcdc0888f chore: release 0.7.14 (#22)
Release / linux (push) Successful in 16m24s
Release / windows (push) Successful in 18m27s
2026-09-04 03:44:32 +00:00
adrianbonpin fd451d219d feat: pin keychain item ACLs to the app's code signature (macOS) (#21) 2026-09-04 03:41:54 +00:00
adrianbonpin a86f5f7199 docs: Homebrew install via Gitea tap (#20) 2026-09-04 02:54:08 +00:00
adrianbonpin 2fccb7b405 fix: use non-deprecated depends_on macos syntax in cask (#19) 2026-09-04 02:53:33 +00:00
adrianbonpin 0efabbdc2e fix: point Homebrew cask at Gitea releases (#18) 2026-09-04 02:46:06 +00:00
adrianbonpin 294e72e6ff chore: drop xattr postflight from Homebrew cask template (#17) 2026-09-04 02:32:43 +00:00
adrianbonpin 15f4dc9bac chore: sync Cargo.lock version to 0.7.13 (#16) 2026-09-04 02:03:13 +00:00
adrianbonpin 867df98958 fix: sign bundled DB tools with Developer ID for notarization (#15) 2026-09-04 02:02:54 +00:00
adrianbonpin e66d7ac102 release: v0.7.13 — Developer-ID signed + notarized macOS (#14)
Release / windows (push) Successful in 19m32s
Release / linux (push) Successful in 22m48s
2026-09-04 01:43:08 +00:00
adrianbonpin 84a6f8d41c feat: Developer-ID sign + notarize macOS builds (#13) 2026-09-04 01:40:48 +00:00
adrianbonpin 08b0f79f07 feat(www): SEO technical + on-page improvements (#12) 2026-09-02 09:10:50 +00:00
adrianbonpin 9611567daa chore: sync Cargo.lock to v0.7.12 + gitignore bundled DB tools (#11) 2026-09-02 08:45:26 +00:00
adrianbonpin 0bc5110e05 fix(www): point download buttons at Gitea + v0.7.12 (#10) 2026-09-02 08:43:31 +00:00
adrianbonpin 2433697951 fix(www): point download buttons at Gitea + v0.7.12 (#9) 2026-09-01 06:15:43 +00:00
27 changed files with 496 additions and 121 deletions
+5 -10
View File
@@ -7,16 +7,11 @@ name: Release
# git checkout prod && git pull
# git tag v0.5.0 && git push origin v0.5.0
#
# SIGNING STATUS: macOS bundles are AD-HOC signed via tauri.conf.json
# (bundle.macOS.signingIdentity: "-", hardenedRuntime: false). That replaces
# the linker-only signature that made macOS report the app as "damaged" and
# refuse Finder launches — users now get the standard "Open Anyway" prompt
# (or run `sudo xattr -dr com.apple.quarantine` for the damaged-error case, see
# README). Still NOT Developer-ID signed / notarized (no certs yet).
# tauri-action automatically signs + notarizes when the signing secrets are
# present, so the moment we add APPLE_CERTIFICATE / APPLE_API_KEY /
# WINDOWS_CERTIFICATE (or Azure Trusted Signing) to repo secrets, future
# builds are properly signed — no changes to this file required.
# SIGNING STATUS: macOS is Developer-ID signed + notarized via
# scripts/release-mac.sh (built manually on Adrian's machine). NOTE: this
# GitHub workflow is NOT the active release path — Gitea Actions uses
# .gitea/workflows/release.yml (Linux/Windows only). Linux/Windows installers
# remain unsigned.
on:
push:
+10
View File
@@ -38,3 +38,13 @@ docs/superpowers/
.env
!.env.example
.worktrees/
# Bundled DB client tools (built/downloaded during release, never committed)
desktop/src-tauri/resources/pg_tools/pg_dump
desktop/src-tauri/resources/pg_tools/pg_restore
desktop/src-tauri/resources/pg_tools/psql
desktop/src-tauri/resources/pg_tools/libpq.5.dylib
desktop/src-tauri/resources/mysql_tools/mariadb
desktop/src-tauri/resources/mysql_tools/mariadb-dump
desktop/src-tauri/resources/mysql_tools/libcrypto.3.dylib
desktop/src-tauri/resources/mysql_tools/libssl.3.dylib
+2 -2
View File
@@ -184,7 +184,7 @@ Cut a release by tagging the **`prod`** branch once the PR is merged — `git ta
- `desktop/src/lib/version.test.ts` and `desktop/src/lib/docs-coverage.test.ts` if they assert the version
- **README download links are static (versioned)** — both download tables (top **Download** section + **Which file should I download?**) link directly to the release-tag assets (`releases/download/v0.7.9/<file>`). tauri-action uses default versioned asset names (`Gridline_<ver>_aarch64.dmg`, `Gridline-<ver>-1.x86_64.rpm`, etc.) — update BOTH tables to the new names on every release (see the MAINTENANCE comment in README.md).
- **Bundled pg tools:** `tauri.conf.json` `bundle.resources` lists `resources/pg_tools/*`; the `release.yml` matrix builds/downloads + checksum-verifies the static binaries before the Tauri build step.
- **Release notes must include the macOS first-launch instructions** — until a Developer ID signing certificate is secured, releases are ad-hoc signed only (not notarized), so every macOS user hits a Gatekeeper prompt. The draft release body must include: right-click → **Open** → **Open** (or System Settings → Privacy & Security → **Open Anyway**) for "developer cannot be verified", and `sudo xattr -dr com.apple.quarantine /Applications/Gridline.app` (with the re-run-after-every-upgrade note) for "damaged and can't be opened".
- **macOS is Developer-ID signed + notarized** via `scripts/release-mac.sh` (built manually on Adrian's machine; not in CI). The script auto-detects the Developer ID identity from the login keychain, reads notarization credentials from `~/.config/gridline/notarize.env` (App Store Connect API key), and verifies codesign + stapled notarization before uploading. If the cert or credentials are missing it fails with setup instructions (see the README's "macOS signing & notarization" section).
### Adding a Tauri Command
@@ -244,7 +244,7 @@ Planned work is prioritized in the [Project Roadmap](./ROADMAP.md) (source of tr
| DB Viewer: SQLite browse + query | ✅ | Full support via rusqlite |
| DB Viewer: MySQL browse + query + edit | ✅ | Full viewer: connect (SSL + SSH tunnel), databases/tables/columns/FKs, query + pagination, inline cell editing + changes queue, DDL copy (`SHOW CREATE TABLE`), CSV/JSON import — added in v0.7.0. PK-only editing (no ctid equivalent); VARBINARY `information_schema` columns decoded correctly |
| DB Viewer: Redis browse | ❌ | Connection + test only; browsing gated off with a clean "not supported" state (v0.7.0) |
| Password storage in OS keychain | ✅ | macOS Keychain, Linux Secret Service, Windows Credential Manager |
| Password storage in OS keychain | ✅ | macOS Keychain, Linux Secret Service, Windows Credential Manager; macOS items get an explicit `SecAccess` ACL pinning them to the app's code-signature requirement on every save (`keychain_acl.rs`) — items survive re-signs/upgrades without keychain permission prompts |
| Enable keychain toggle | ✅ | Default ON (opt-out); OFF = don't persist the DB password (session-only, re-prompt on connect) + purge existing keychain entry; SSH secrets stay keychain-only (v0.7.6) |
| SSH tunnel config UI | ✅ | Host, port, user, auth method, key path, passphrase fields |
| SSH tunnel runtime | ✅ | Real ssh2 tunnel (password + key auth), binds 127.0.0.1 only, secrets in OS keychain (`ssh_password:<id>` / `ssh_passphrase:<id>`), closed on pool eviction / app exit; TLS downgraded to `require` through the tunnel |
+43 -33
View File
@@ -30,31 +30,34 @@
## Download
Grab the installer for your OS from the [latest release](https://git.ranio.xyz/adrianbonpin/gridline/releases/latest) — the links below point at the current release (**v0.7.12**):
Grab the installer for your OS from the [latest release](https://git.ranio.xyz/adrianbonpin/gridline/releases/latest) — the links below point at the current release (**v0.7.14**):
| OS | Architecture | Download |
| :--------------------------- | :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
| **macOS** | Apple Silicon (M1/M2/M3/M4…) | [Gridline_0.7.12_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_aarch64.dmg) |
| **macOS** | Intel | [Gridline_0.7.12_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_x64.dmg) |
| **Windows** | x64 | [Gridline_0.7.12_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_x64-setup.exe) |
| **Debian / Ubuntu** | amd64 | [Gridline_0.7.12_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_amd64.deb) |
| **Fedora / RHEL / openSUSE** | x86_64 | [Gridline-0.7.12-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline-0.7.12-1.x86_64.rpm) |
| **Other Linux** | amd64 | [Gridline_0.7.12_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_amd64.AppImage) |
| **macOS** | Apple Silicon (M1/M2/M3/M4…) | [Gridline_0.7.14_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_aarch64.dmg) |
| **macOS** | Intel | [Gridline_0.7.14_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_x64.dmg) |
| **Windows** | x64 | [Gridline_0.7.14_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_x64-setup.exe) |
| **Debian / Ubuntu** | amd64 | [Gridline_0.7.14_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_amd64.deb) |
| **Fedora / RHEL / openSUSE** | x86_64 | [Gridline-0.7.14-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline-0.7.14-1.x86_64.rpm) |
| **Other Linux** | amd64 | [Gridline_0.7.14_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_amd64.AppImage) |
**macOS via Homebrew** (recommended for Mac users):
```bash
brew install --cask AdrianBonpin/gridline/gridline
# or, after a one-time `brew tap AdrianBonpin/gridline`:
# one-time setup (the tap is hosted on Gitea, not GitHub):
brew tap AdrianBonpin/gridline https://git.ranio.xyz/adrianbonpin/homebrew-gridline.git
brew trust adrianbonpin/gridline
# install / upgrade:
brew install --cask gridline
```
The cask strips the macOS quarantine flag automatically during install, so **no manual `xattr` step is needed** on the Homebrew path — the app just launches. (Direct-DMG downloads still need the [first-launch instructions](#installers-are-unsigned-for-now) below.)
The cask installs the app directly. The DMGs are Developer-ID signed and notarized, so macOS trusts them on first launch — no `xattr` or "Open Anyway" step needed.
> **macOS first launch:** macOS may say *"Gridline is damaged and can't be opened"* or *"the developer cannot be verified"* — this is expected; the app isn't Developer-ID signed/notarized yet. See the [macOS first-launch instructions](#installers-are-unsigned-for-now) (right-click → Open, or the one-time `xattr` fix). Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below.
> **macOS first launch:** Gridline is Developer-ID signed and notarized, so macOS opens it without a Gatekeeper prompt. Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below.
<!--
MAINTENANCE: These links are STATIC (versioned) — they point at the v0.7.12
MAINTENANCE: These links are STATIC (versioned) — they point at the v0.7.14
release assets, not at a moving "latest" target. On every new release,
update BOTH tables here (Download + Which file should I download?) to the
new version's asset names, which are tauri-action's default naming:
@@ -99,6 +102,10 @@ Gridline is built for developers and small teams who manage multiple database en
<details>
<summary>View the full changelog</summary>
- **v0.7.14** — **No more repeated macOS keychain prompts**: saved connection passwords and SSH secrets are now ACL-pinned to the app's code signature on every save (`keychain_acl`), so app upgrades never re-trigger the macOS keychain access dialog (users upgrading from pre-signing builds see one final prompt — choose **Always Allow**). **Install via Homebrew**: `brew tap AdrianBonpin/gridline https://git.ranio.xyz/adrianbonpin/homebrew-gridline.git` then `brew install --cask gridline`; version bump.
- **v0.7.13** — **macOS is now Developer-ID signed and notarized**: the app is properly code-signed with a Developer ID Application certificate and notarized by Apple, so macOS opens it without the Gatekeeper "Open Anyway" prompt or the `xattr` quarantine workaround. The release script (`scripts/release-mac.sh`) auto-detects the signing identity from the keychain, notarizes + staples both architectures, and verifies the result before uploading; version bump.
- **v0.7.12** — **Reliable cell editing for non-text columns**: editing a PostgreSQL cell whose type isn't `text` (integers, booleans, UUIDs, `json`/`jsonb`, timestamps, enums, numerics, …) previously failed with a Rust serialization error; edited values are now sent to PostgreSQL in text wire format so the server parses them into the column's own type. **Recent connections fix**: the Recent strip no longer renders empty on first launch when the connections list hadn't loaded yet. **Full CI release pipeline for Windows**: self-hosted Windows runner builds the `.exe`/`.msi` installers (bundled `pg_dump`/`pg_restore`/`psql` + MariaDB clients) and uploads them to the release alongside Linux; version bump.
- **v0.7.10** — **Copy connection URL**: from a connection card's ⋮ menu, copy the connection string for use in env vars / other tools — **Copy connection URL** (includes the password, fetched from the OS keychain on demand) and **Copy connection URL (no password)** (safe to share). Builds `postgresql://` / `mysql://` / `sqlite://` / `redis://` strings with percent-encoded credentials and the PG `sslmode` appended; version bump.
@@ -307,37 +314,40 @@ Pre-built installers for macOS, Windows, and Linux are published on the [Release
> ⚠️ Gridline is under active development. Expect rough edges and please [open issues](https://git.ranio.xyz/adrianbonpin/gridline/issues/new) when you hit them.
#### Installers are unsigned (for now)
#### macOS signing & notarization
Gridline is currently distributed **unsigned** — it doesn't pay for code-signing certificates yet (macOS builds are *ad-hoc signed*, so they pass Apple Silicon's launch checks but aren't Developer-ID signed or notarized). Your OS will warn you the first time you open it. This is expected — the app is safe, it just hasn't paid the signing fee:
Gridline's macOS builds are **Developer-ID signed and notarized**, so macOS opens them without a Gatekeeper prompt. This is configured on the machine that runs `scripts/release-mac.sh` (macOS is built manually, not in CI).
> **Homebrew users:** the [Homebrew cask](#download) strips the quarantine flag automatically during install, so you skip all of the steps below — the app just launches.
**One-time setup** (only the repo owner needs to do this):
- **macOS:** if you see *"Gridline can't be opened because the developer cannot be verified"* (or the app simply won't open from Finder), right-click the app → **Open** → **Open** (or System Settings → Privacy & Security → **Open Anyway**). Do this once per version.
- **macOS — "Gridline is damaged and can't be opened":** remove the quarantine flag macOS attaches to downloaded apps, then launch normally:
1. **Create a Developer ID Application certificate** — [developer.apple.com](https://developer.apple.com) → Certificates, Identifiers & Profiles → **+** → **Developer ID Application**. Generate a Certificate Signing Request from Keychain Access (Certificate Assistant → *Request a Certificate From a Certificate Authority*), upload it, download the `.cer`, and double-click to install into your login keychain. Verify with `security find-identity -v -p codesigning`.
2. **Create an App Store Connect API key** for notarization — [appstoreconnect.apple.com](https://appstoreconnect.apple.com) → Users and Access → **Integrations** → **App Store Connect API** → **+** (name it, *Developer* access). Download the `.p8` (shown once) and note the **Key ID** and **Issuer ID**.
3. **Save the notarization credentials** to `~/.config/gridline/notarize.env` (never committed):
```bash
sudo xattr -dr com.apple.quarantine /Applications/Gridline.app
```
```bash
APPLE_API_KEY=<Key ID>
APPLE_API_ISSUER=<Issuer ID>
APPLE_API_KEY_PATH=/absolute/path/to/AuthKey_<KeyID>.p8
```
(`sudo` is required — some files inside the bundle are read-only, so removing the flag needs admin rights.) Re-run after **every upgrade** — each freshly-downloaded copy gets re-quarantined.
- **Windows:** on the SmartScreen prompt, click **More info** → **Run anyway**.
- **Linux:** no warning — install and run normally.
(Alternatively, drop the `.p8` at `~/.appstoreconnect/private_keys/AuthKey_<KeyID>.p8` and omit `APPLE_API_KEY_PATH`.)
Code signing **will be added in the future** (Apple Developer Program + a Windows signing cert, e.g. Azure Trusted Signing) — the CI workflow is already wired to pick up the signing secrets automatically the moment they exist, no workflow changes needed.
`scripts/release-mac.sh` auto-detects the Developer ID identity from your keychain, signs both architectures, notarizes + staples them, and verifies the result before uploading. If the certificate or credentials are missing it fails with instructions (set `GRIDLINE_SKIP_NOTARIZE=1` to build signed-but-unnotarized for testing only).
**Windows** still shows a SmartScreen prompt (no Windows signing cert yet); **Linux** installs without a warning.
#### Which file should I download?
Each release contains **one file per platform** — you only need the one that matches your computer. The links below point at the current release (**v0.7.12**):
Each release contains **one file per platform** — you only need the one that matches your computer. The links below point at the current release (**v0.7.14**):
| Your system | Download this | Notes |
| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |
| macOS **Apple Silicon** (M1/M2/M3/M4…) | [Gridline_0.7.12_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_aarch64.dmg) | `aarch64` = Apple's own chip |
| macOS **Intel** | [Gridline_0.7.12_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_x64.dmg) | `x64` = Intel/AMD |
| **Windows** (most PCs) | [Gridline_0.7.12_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_x64-setup.exe) | The `.msi` is an alternate installer (for enterprises/IT admins) |
| **Debian / Ubuntu** | [Gridline_0.7.12_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_amd64.deb) | Install: `sudo apt install ./Gridline_0.7.12_amd64.deb` |
| **Fedora / RHEL / openSUSE** | [Gridline-0.7.12-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline-0.7.12-1.x86_64.rpm) | Install: `sudo dnf install Gridline-0.7.12-1.x86_64.rpm` |
| **Any other Linux** | [Gridline_0.7.12_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_amd64.AppImage) | Works on every distro: `chmod +x` the file, then double-click it |
| macOS **Apple Silicon** (M1/M2/M3/M4…) | [Gridline_0.7.14_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_aarch64.dmg) | `aarch64` = Apple's own chip |
| macOS **Intel** | [Gridline_0.7.14_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_x64.dmg) | `x64` = Intel/AMD |
| **Windows** (most PCs) | [Gridline_0.7.14_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_x64-setup.exe) | The `.msi` is an alternate installer (for enterprises/IT admins) |
| **Debian / Ubuntu** | [Gridline_0.7.14_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_amd64.deb) | Install: `sudo apt install ./Gridline_0.7.14_amd64.deb` |
| **Fedora / RHEL / openSUSE** | [Gridline-0.7.14-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline-0.7.14-1.x86_64.rpm) | Install: `sudo dnf install Gridline-0.7.14-1.x86_64.rpm` |
| **Any other Linux** | [Gridline_0.7.14_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_amd64.AppImage) | Works on every distro: `chmod +x` the file, then double-click it |
**Not sure if your Mac is Intel or Apple Silicon?** Click the **Apple menu** → **About This Mac**. If it shows "Apple M1/M2/M3/M4…" download the `aarch64` file; if it shows an Intel chip, download `x64`. Downloading the wrong one won't run.
@@ -347,8 +357,8 @@ Cutting a release is one command — CI builds everything. **Releases are cut fr
```bash
git checkout prod && git pull
git tag v0.7.12
git push origin v0.7.12
git tag v0.7.14
git push origin v0.7.14
```
Gitea Actions (`.gitea/workflows/release.yml`) builds the **Linux and Windows** installers on the self-hosted runners, then opens a **draft release** on the [Releases](https://git.ranio.xyz/adrianbonpin/gridline/releases) page. **macOS is built manually** on the dev Mac and its DMGs uploaded to the same release via `scripts/release-mac.sh`. Review the draft and hit **Publish release**.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "gridline-desktop",
"private": true,
"version": "0.7.12",
"version": "0.7.14",
"description": "An open-source, high-performance database GUI client for PostgreSQL and beyond",
"type": "module",
"scripts": {
+4 -1
View File
@@ -1783,10 +1783,11 @@ dependencies = [
[[package]]
name = "gridline"
version = "0.7.11"
version = "0.7.14"
dependencies = [
"bytes",
"chrono",
"core-foundation 0.10.1",
"deadpool-postgres",
"indexmap 2.14.0",
"redis",
@@ -1795,6 +1796,8 @@ dependencies = [
"rustls-native-certs",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"security-framework 3.7.0",
"security-framework-sys",
"serde",
"serde_json",
"sqlx",
+10 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gridline"
version = "0.7.12"
version = "0.7.14"
description = "An open-source, high-performance database GUI client for PostgreSQL and beyond"
authors = ["you"]
edition = "2021"
@@ -49,3 +49,12 @@ rustls-pemfile = "2"
rustls-pki-types = "1"
rustls-native-certs = "0.7"
# Keychain ACL normalization (explicit SecAccess on stored secrets so items
# survive app re-signs without permission prompts) — macOS only.
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "3.7"
security-framework-sys = "2.17"
# core-foundation: CFString/CFArray/TCFType helpers for the SecAccess FFI
# (already a transitive dep of security-framework; pinned to the same 0.10 line)
core-foundation = "0.10"
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Required for the WKWebView (JIT-compiled JavaScript). Without this the
app crashes under the hardened runtime. -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- The WebView allocates executable memory for JIT; allow it. -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Tauri's dev server / debug builds rely on DYLD_* environment variables. -->
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
+11 -3
View File
@@ -1,3 +1,4 @@
use crate::keychain_acl;
use tauri_plugin_keyring_store::KeyringExt;
/// Store a connection password in the OS keychain.
@@ -11,7 +12,10 @@ pub fn save_connection_password(
app.keyring()
.store
.set_password(&connection_id, &password)
.map_err(|e| e.to_string())
.map_err(|e| e.to_string())?;
// Best-effort: pin the item's ACL to this app so re-signed builds never prompt.
keychain_acl::after_store(&app, &connection_id);
Ok(())
}
/// Retrieve a connection password from the OS keychain.
@@ -55,7 +59,9 @@ pub fn save_connection_ssh_password(
app.keyring()
.store
.set_password(&ssh_account("password", &connection_id), &password)
.map_err(|e| e.to_string())
.map_err(|e| e.to_string())?;
keychain_acl::after_store(&app, &ssh_account("password", &connection_id));
Ok(())
}
/// Retrieve an SSH tunnel password from the OS keychain.
@@ -93,7 +99,9 @@ pub fn save_connection_ssh_passphrase(
app.keyring()
.store
.set_password(&ssh_account("passphrase", &connection_id), &passphrase)
.map_err(|e| e.to_string())
.map_err(|e| e.to_string())?;
keychain_acl::after_store(&app, &ssh_account("passphrase", &connection_id));
Ok(())
}
/// Retrieve an SSH private-key passphrase from the OS keychain.
+198
View File
@@ -0,0 +1,198 @@
//! Keychain ACL normalization for stored secrets (macOS).
//!
//! # Why this exists
//!
//! Generic-password items stored via the legacy keychain API get a default ACL
//! that grants access to "the application that created the item". When a Tauri
//! app is rebuilt, its code signature changes (dev builds are ad-hoc signed, so
//! the identity differs on every build), and the new binary no longer matches
//! the item's ACL — macOS then shows the "Gridline wants to access your
//! keychain" prompt.
//!
//! The fix: after storing a secret, replace the item's ACL with an explicit
//! `SecAccess` whose trusted-application list contains exactly *this app*,
//! resolved by `SecTrustedApplicationCreateFromPath(NULL)` — the current
//! process's code-signature requirement. For a Developer ID-signed release
//! build that requirement is stable across versions (anchored to the signing
//! certificate), so items stay accessible without prompts through upgrades.
//!
//! # Prompt behavior
//!
//! - Item created by this build → the ACL set is silent.
//! - Item created by an older/differently-signed build → macOS shows the
//! standard one-time authorization dialog; "Always Allow" replaces the ACL
//! and the prompt never reappears (the same dialog the app would show on the
//! first read anyway — this just makes the result permanent and clean).
//!
//! All operations are best-effort: `after_store` swallows errors so keychain
//! writes never fail because of ACL normalization.
/// Best-effort ACL normalization after a successful keychain write.
///
/// Resolves the service name the same way `tauri-plugin-keyring-store` does
/// (the Tauri bundle identifier) and never fails the caller's operation.
pub fn after_store(app: &tauri::AppHandle, account: &str) {
let service = app.config().identifier.clone();
if let Err(e) = ensure_item_acl(&service, account) {
eprintln!("[keychain_acl] skipped for {account}: {e}");
}
}
/// Grant this app (by its code-signature requirement) access to a keychain item.
#[cfg(target_os = "macos")]
pub fn ensure_item_acl(service: &str, account: &str) -> Result<(), String> {
use core_foundation::array::CFArray;
use core_foundation::base::{CFTypeRef, TCFType};
use core_foundation::string::CFString;
use security_framework::item::{ItemClass, ItemSearchOptions, Reference, SearchResult};
use security_framework::os::macos::access::SecAccess;
use security_framework_sys::base::SecAccessRef;
// Locate the item WITHOUT reading its secret: a ref-only lookup
// (`kSecReturnRef`, no data/attributes) never triggers an authorization
// prompt, so merely probing an item we can't yet access is safe.
let found = ItemSearchOptions::new()
.class(ItemClass::generic_password())
.service(service)
.account(account)
.load_refs(true)
.load_data(false)
.load_attributes(false)
.search()
.map_err(|e| format!("keychain item lookup failed: {e}"))?;
let item = match found.first() {
Some(SearchResult::Ref(Reference::KeychainItem(item))) => item,
// No item (yet) or an unexpected result shape — nothing to normalize.
_ => return Ok(()),
};
fn cvt(status: i32) -> Result<(), String> {
if status == 0 {
Ok(())
} else {
Err(format!("security framework error {status}"))
}
}
// Build an access object whose trusted list is exactly this app. A NULL
// path resolves the trusted application to the current process's code
// signature requirement.
unsafe {
let mut trusted_app_ref: *mut OpaqueSecTrustedApplication = std::ptr::null_mut();
cvt(SecTrustedApplicationCreateFromPath(std::ptr::null(), &mut trusted_app_ref))?;
let trusted_app = SecTrustedApplication::wrap_under_create_rule(trusted_app_ref);
let trusted_list = CFArray::from_CFTypes(&[trusted_app]);
let descriptor = CFString::new("Gridline");
let mut access_ref: SecAccessRef = std::ptr::null_mut();
cvt(SecAccessCreate(
descriptor.as_concrete_TypeRef() as CFTypeRef,
trusted_list.as_concrete_TypeRef() as CFTypeRef,
&mut access_ref,
))?;
let access = SecAccess::wrap_under_create_rule(access_ref);
// Replace the item's ACL. Silent when we already own the item;
// one-time authorization dialog when the item was created by a
// differently-signed build.
cvt(SecKeychainItemSetAccess(
item.as_concrete_TypeRef(),
access.as_concrete_TypeRef(),
))
}
}
/// Non-macOS platforms have no keychain ACL concept — nothing to do.
#[cfg(not(target_os = "macos"))]
pub fn ensure_item_acl(_service: &str, _account: &str) -> Result<(), String> {
Ok(())
}
#[cfg(target_os = "macos")]
mod macos_ffi {
use core_foundation::base::{CFTypeRef, CFTypeID};
/// Minimal `TCFType` wrapper for `SecTrustedApplication` — the
/// security-framework crate exposes no high-level API for it.
#[repr(C)]
pub struct OpaqueSecTrustedApplication {
_priv: [u8; 0],
}
pub type SecTrustedApplicationRef = *mut OpaqueSecTrustedApplication;
use core_foundation::{declare_TCFType, impl_TCFType};
declare_TCFType!(SecTrustedApplication, SecTrustedApplicationRef);
impl_TCFType!(
SecTrustedApplication,
SecTrustedApplicationRef,
SecTrustedApplicationGetTypeID
);
unsafe impl Send for SecTrustedApplication {}
unsafe impl Sync for SecTrustedApplication {}
extern "C" {
// SecAccess/SecTrustedApplication APIs are deprecated in the macOS SDK
// but remain fully functional (the legacy keychain store this app uses
// via tauri-plugin-keyring-store relies on the same API family).
pub fn SecTrustedApplicationGetTypeID() -> CFTypeID;
pub fn SecTrustedApplicationCreateFromPath(
path: CFTypeRef,
app: *mut SecTrustedApplicationRef,
) -> i32;
pub fn SecAccessCreate(
descriptor: CFTypeRef,
trustedlist: CFTypeRef,
access: *mut security_framework_sys::base::SecAccessRef,
) -> i32;
pub fn SecKeychainItemSetAccess(
item: security_framework_sys::base::SecKeychainItemRef,
access: security_framework_sys::base::SecAccessRef,
) -> i32;
}
}
#[cfg(target_os = "macos")]
use macos_ffi::{
OpaqueSecTrustedApplication, SecAccessCreate, SecKeychainItemSetAccess, SecTrustedApplication,
SecTrustedApplicationCreateFromPath,
};
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
use security_framework::os::macos::keychain::SecKeychain;
use security_framework::os::macos::passwords::find_generic_password;
/// Real-keychain integration test: creates a generic-password item the
/// same way the keyring store does, normalizes its ACL, verifies the
/// secret is still readable, and cleans up.
///
/// Ignored by default (`cargo test`) so CI/dev runs never touch the login
/// keychain. Run explicitly with:
/// `cargo test -- --ignored keychain_acl`
#[test]
#[ignore = "touches the real login keychain"]
fn keychain_acl_normalizes_item() {
let keychain = SecKeychain::default().expect("default keychain");
let service = "com.adrianbonpin.gridline.acl-test";
let account = format!("acl-test-{}", std::process::id());
// Create the item exactly like apple-native-keyring-store does
// (legacy SecKeychain generic-password API).
keychain
.set_generic_password(service, &account, b"test-secret")
.expect("create test item");
// ACL normalization must succeed on an item we just created.
ensure_item_acl(service, &account).expect("normalize ACL");
// The item must remain readable with the correct secret.
let (password, item) =
find_generic_password(Some(&[keychain]), service, &account).expect("read back");
assert_eq!(password.as_ref(), b"test-secret");
// Cleanup: delete the test item.
item.delete();
}
}
+1
View File
@@ -5,6 +5,7 @@
mod cancel;
mod commands;
mod db;
mod keychain_acl;
mod models;
mod store;
+3 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Gridline",
"version": "0.7.12",
"version": "0.7.14",
"identifier": "com.adrianbonpin.gridline",
"build": {
"beforeDevCommand": "bun run dev",
@@ -29,7 +29,8 @@
"resources": ["resources/pg_tools/*", "resources/mysql_tools/*"],
"macOS": {
"signingIdentity": "-",
"hardenedRuntime": false
"hardenedRuntime": true,
"entitlements": "entitlements.plist"
},
"icon": [
"icons/32x32.png",
+3 -3
View File
@@ -1,11 +1,11 @@
import { describe, it, expect } from "vitest";
import tauriConf from "../../src-tauri/tauri.conf.json";
describe("tauri bundle config (v0.7.12)", () => {
describe("tauri bundle config (v0.7.14)", () => {
it("declares bundled pg_tools resources", () => {
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
});
it("version is 0.7.12", () => {
expect(tauriConf.version).toBe("0.7.12");
it("version is 0.7.14", () => {
expect(tauriConf.version).toBe("0.7.14");
});
});
+7 -7
View File
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";
import agents from "../../../AGENTS.md?raw";
import readme from "../../../README.md?raw";
describe("v0.7.12 docs coverage", () => {
describe("v0.7.14 docs coverage", () => {
it("AGENTS.md marks inline cell editing complete", () => {
expect(agents).toContain("Inline cell editing");
expect(agents).toMatch(/Inline cell editing \| ✅/);
@@ -24,8 +24,8 @@ describe("v0.7.12 docs coverage", () => {
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
});
it("README declares v0.7.12", () => {
expect(readme).toContain("0.7.12");
it("README declares v0.7.14", () => {
expect(readme).toContain("0.7.14");
});
it("AGENTS.md marks schema CRUD complete", () => {
expect(agents).toMatch(/Schema CRUD \| ✅/);
@@ -64,9 +64,9 @@ describe("v0.7.12 docs coverage", () => {
expect(agents).toMatch(/Cancel long-running queries \| ✅/);
expect(agents).toMatch(/Settings export\/import \| ✅/);
});
it("README links to v0.7.12 assets in both download tables", () => {
expect(readme).toContain("releases/download/v0.7.12/");
expect(readme).toContain("Gridline_0.7.12_aarch64.dmg");
expect(readme).toContain("Gridline-0.7.12-1.x86_64.rpm");
it("README links to v0.7.14 assets in both download tables", () => {
expect(readme).toContain("releases/download/v0.7.14/");
expect(readme).toContain("Gridline_0.7.14_aarch64.dmg");
expect(readme).toContain("Gridline-0.7.14-1.x86_64.rpm");
});
});
+2 -2
View File
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import pkg from "../../package.json";
describe("version", () => {
it("declares v0.7.12 across the app shell", () => {
expect(pkg.version).toBe("0.7.12");
it("declares v0.7.14 across the app shell", () => {
expect(pkg.version).toBe("0.7.14");
});
});
+5 -19
View File
@@ -11,13 +11,8 @@
#
# brew install --cask gridline
#
# QUARANTINE: Gridline is ad-hoc signed (not Developer-ID signed/notarized),
# so Homebrew casks (which deliberately set com.apple.quarantine on the
# downloaded artifact) would make Gatekeeper refuse to launch it. The
# `postflight` block strips the quarantine flag automatically — the standard
# GoReleaser-documented pattern for unsigned casks — so `brew install` works
# with no manual `xattr` step. This is a Gatekeeper bypass; drop the
# postflight once the app is properly signed + notarized.
# SIGNING: Gridline is Developer-ID signed + notarized, so macOS trusts it
# and no quarantine-stripping postflight is needed.
cask "gridline" do
arch arm: "aarch64", intel: "x64"
@@ -26,24 +21,15 @@ cask "gridline" do
sha256 arm: "@ARM_SHA256@",
intel: "@INTEL_SHA256@"
url "https://github.com/AdrianBonpin/gridline/releases/download/v#{version}/Gridline_#{version}_#{arch}.dmg"
url "https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v#{version}/Gridline_#{version}_#{arch}.dmg"
name "Gridline"
desc "Open-source, cross-platform database GUI client for PostgreSQL"
homepage "https://github.com/AdrianBonpin/gridline"
homepage "https://git.ranio.xyz/adrianbonpin/gridline"
depends_on macos: ">= :monterey"
depends_on macos: :monterey
app "Gridline.app"
# The "installation step" — strip the quarantine flag so the unsigned
# (ad-hoc signed) app launches without the Gatekeeper prompt.
postflight do
if OS.mac?
system_command "/usr/bin/xattr",
args: ["-dr", "com.apple.quarantine", "#{appdir}/Gridline.app"]
end
end
zap trash: [
"~/Library/Application Support/com.adrianbonpin.gridline",
"~/Library/Caches/com.adrianbonpin.gridline",
+77 -1
View File
@@ -51,6 +51,58 @@ get_token() {
GITEA_TOKEN="$(get_token)"
# ---- macOS signing + notarization -----------------------------------------
# Requires a "Developer ID Application" certificate in the login keychain and
# an App Store Connect API key for notarization. See the README's "macOS
# signing" section for how to create both.
#
# Notarization credentials are read from ~/.config/gridline/notarize.env
# (never committed). Create it with:
# APPLE_API_KEY=<Key ID>
# APPLE_API_ISSUER=<Issuer ID>
# APPLE_API_KEY_PATH=/absolute/path/to/AuthKey_<KeyID>.p8
# (or drop the .p8 at ~/.appstoreconnect/private_keys/AuthKey_<KeyID>.p8 and
# omit APPLE_API_KEY_PATH — tauri auto-searches that location.)
# 1. Resolve the Developer ID Application signing identity from the keychain.
SIGNING_IDENTITY="$(security find-identity -v -p codesigning 2>/dev/null \
| grep -i 'Developer ID Application' \
| sed -E 's/.*"([^"]+)".*/\1/' \
| head -1 || true)"
if [ -z "$SIGNING_IDENTITY" ]; then
echo "ERROR: No 'Developer ID Application' certificate found in the login keychain." >&2
echo " Create one at https://developer.apple.com/account/resources/certificates/list" >&2
echo " (Certificates, IDs & Profiles -> + -> Developer ID Application), download the" >&2
echo " .cer, and double-click it to install into your login keychain." >&2
echo " Verify with: security find-identity -v -p codesigning" >&2
exit 1
fi
export APPLE_SIGNING_IDENTITY="$SIGNING_IDENTITY"
echo ">> Signing identity: $SIGNING_IDENTITY"
# 2. Load notarization credentials.
NOTARIZE_ENV="${HOME}/.config/gridline/notarize.env"
if [ -f "$NOTARIZE_ENV" ]; then
set -a
# shellcheck disable=SC1090
source "$NOTARIZE_ENV"
set +a
fi
if [ -z "${APPLE_API_KEY:-}" ] || [ -z "${APPLE_API_ISSUER:-}" ]; then
if [ "${GRIDLINE_SKIP_NOTARIZE:-0}" = "1" ]; then
echo ">> WARNING: notarization credentials missing; GRIDLINE_SKIP_NOTARIZE=1 set." >&2
echo " Building signed but NOT notarized (test-only — users will still see a prompt)." >&2
else
echo "ERROR: Notarization credentials not configured." >&2
echo " Create ${NOTARIZE_ENV} with APPLE_API_KEY and APPLE_API_ISSUER" >&2
echo " (and APPLE_API_KEY_PATH, or place the .p8 at ~/.appstoreconnect/private_keys/)." >&2
echo " See the README's 'macOS signing' section for the App Store Connect API key setup." >&2
exit 1
fi
else
echo ">> Notarization: API key ${APPLE_API_KEY} (issuer ${APPLE_API_ISSUER})"
fi
# ---- build bundled DB tools (macOS) ----------------------------------------
WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)"
OUT_PG="${WORKSPACE}/desktop/src-tauri/resources/pg_tools"
@@ -75,6 +127,12 @@ for b in pg_dump pg_restore psql; do
libpq=$(otool -L "$OUT_PG/$b" | awk '/libpq/ {print $1; exit}')
install_name_tool -change "$libpq" "@loader_path/libpq.5.dylib" "$OUT_PG/$b"
done
# install_name_tool invalidates the linker's signature; re-sign with the
# Developer ID identity + hardened runtime + secure timestamp so the bundle
# passes notarization (Apple rejects ad-hoc-signed nested executables).
for f in "$OUT_PG"/pg_dump "$OUT_PG"/pg_restore "$OUT_PG"/psql "$OUT_PG"/libpq.5.dylib; do
codesign --force --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$f"
done
echo ">> Building MariaDB client tools..."
mkdir -p "$OUT_MY"
@@ -112,7 +170,7 @@ for pass in 1 2 3 4 5 6; do
if [ ! -f "$OUT_MY/$base" ]; then cp "$lib" "$OUT_MY/$base" 2>/dev/null || true; fi
install_name_tool -change "$lib" "@loader_path/$base" "$dylib" 2>/dev/null || true
done || true
codesign --force --sign - "$dylib" 2>/dev/null || true
codesign --force --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$dylib"
done
done
@@ -130,6 +188,24 @@ VERSION="${TAG#v}"
BUNDLE_ARM="${WORKSPACE}/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle"
BUNDLE_INTEL="${WORKSPACE}/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle"
# ---- verify signing + notarization -----------------------------------------
# Confirm both .app bundles are properly signed and carry a stapled
# notarization ticket before we upload them. spctl can lag right after
# notarization (assessment cache), so stapler validate is the authoritative
# check; a pending spctl is only a warning.
for APP in \
"${BUNDLE_ARM}/macos/Gridline.app" \
"${BUNDLE_INTEL}/macos/Gridline.app"; do
[ -d "$APP" ] || { echo "missing $APP" >&2; exit 1; }
echo ">> Verifying $APP"
codesign --verify --deep --strict --verbose=2 "$APP" \
|| { echo "codesign verify FAILED" >&2; exit 1; }
xcrun stapler validate "$APP" \
|| { echo "notarization ticket not stapled" >&2; exit 1; }
spctl --assess --type execute --verbose=4 "$APP" \
|| echo " (spctl assessment pending — normal right after notarization; stapler validate passed)"
done
# ---- create/ensure release + upload DMGs -----------------------------------
API="${GITEA_SERVER}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}"
AUTH="Authorization: token ${GITEA_TOKEN}"
+1 -3
View File
@@ -10,9 +10,7 @@ export default defineConfig({
compressHTML: true,
trailingSlash: 'ignore',
integrations: [
sitemap({
lastmod: new Date(),
}),
sitemap(),
react(),
],
vite: {
+1
View File
@@ -0,0 +1 @@
/* /404.html 404
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

+5 -22
View File
@@ -59,18 +59,15 @@
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(userAgent);
const keepLabel = downloadBtn.hasAttribute("data-download-label");
const releasesUrl = "https://github.com/AdrianBonpin/gridline/releases";
const version = "0.7.10";
const releasesUrl = "https://git.ranio.xyz/adrianbonpin/gridline/releases";
const version = "0.7.14";
if (isMobile) {
downloadBtn.href = releasesUrl;
if (!keepLabel) downloadBtn.textContent = "Get it on GitHub";
if (!keepLabel) downloadBtn.textContent = "Get it on Gitea";
} else if (isMac) {
downloadBtn.href = `${releasesUrl}/download/v${version}/Gridline_${version}_aarch64.dmg`;
if (!keepLabel) downloadBtn.textContent = "Download for macOS";
if (downloadBtn.id === "download") {
document.getElementById("macos-hint")?.classList.remove("hidden");
}
} else if (isWin) {
downloadBtn.href = `${releasesUrl}/download/v${version}/Gridline_${version}_x64-setup.exe`;
if (!keepLabel) downloadBtn.textContent = "Download for Windows";
@@ -83,18 +80,6 @@
}
});
document.getElementById("copy-quarantine")?.addEventListener("click", async () => {
const command = "sudo xattr -dr com.apple.quarantine /Applications/Gridline.app";
try {
await navigator.clipboard.writeText(command);
const btn = document.getElementById("copy-quarantine");
if (!btn) return;
const original = btn.textContent;
btn.textContent = "Copied";
setTimeout(() => (btn.textContent = original), 1500);
} catch {}
});
// Nav scroll background
const nav = document.getElementById("nav");
function onScroll() {
@@ -141,24 +126,22 @@
const subtitle = hero.querySelector(".hero-animate > p:nth-of-type(2)");
const buttons = hero.querySelector(".hero-animate > div");
const screenshot = hero.querySelector(".hero-screenshot");
const macosHint = hero.querySelector("#macos-hint");
if (reducedMotion) {
// Reduced motion: reveal the hero immediately (CSS hides it at first
// paint to avoid flicker, but the timeline below is skipped). Section
// reveals below still run through the IntersectionObserver.
anime.set([eyebrow, h1, subtitle, buttons, screenshot, macosHint], { opacity: 1, translateY: 0 });
anime.set([eyebrow, h1, subtitle, buttons, screenshot], { opacity: 1, translateY: 0 });
} else {
const heroTl = anime.timeline({ easing: "easeOutExpo" });
anime.set([eyebrow, h1, subtitle, buttons, screenshot, macosHint], { opacity: 0, translateY: 20 });
anime.set([eyebrow, h1, subtitle, buttons, screenshot], { opacity: 0, translateY: 20 });
anime.set(screenshot, { opacity: 0, scale: 0.96, translateY: 0 });
if (eyebrow) heroTl.add({ targets: eyebrow, opacity: [0, 1], translateY: [15, 0] }, 0);
if (h1) heroTl.add({ targets: h1, opacity: [0, 1], translateY: [30, 0], duration: 800 }, 100);
if (subtitle) heroTl.add({ targets: subtitle, opacity: [0, 1], translateY: [20, 0] }, 250);
if (buttons) heroTl.add({ targets: buttons, opacity: [0, 1], translateY: [20, 0] }, 350);
if (macosHint) heroTl.add({ targets: macosHint, opacity: [0, 1], translateY: [20, 0] }, 400);
if (screenshot) heroTl.add({ targets: screenshot, opacity: [0, 1], scale: [0.96, 1], duration: 900 }, 450);
}
}
+6
View File
@@ -12,6 +12,12 @@
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any"
}
]
}
+24
View File
@@ -5,6 +5,30 @@ export interface ChangelogEntry {
}
export const changelogEntries: ChangelogEntry[] = [
{
version: "0.7.14",
date: "2026-09-04",
highlights: [
"No more repeated macOS keychain prompts — saved connection passwords and SSH secrets are now ACL-pinned to the app's code signature when stored, so app upgrades never re-trigger the macOS keychain access dialog (users upgrading from older builds see one final prompt — choose Always Allow).",
"Install via Homebrew — add the Gitea tap (brew tap AdrianBonpin/gridline https://git.ranio.xyz/adrianbonpin/homebrew-gridline.git) and run brew install --cask gridline.",
],
},
{
version: "0.7.13",
date: "2026-09-04",
highlights: [
"macOS is now Developer-ID signed and notarized — the app is properly code-signed with a Developer ID Application certificate and notarized by Apple, so macOS opens it without the Gatekeeper 'Open Anyway' prompt or the xattr quarantine workaround.",
],
},
{
version: "0.7.12",
date: "2026-09-01",
highlights: [
"Reliable cell editing for non-text columns — editing a PostgreSQL cell whose type isn't text (integers, booleans, UUIDs, json/jsonb, timestamps, enums, numerics) now works; edited values are sent to PostgreSQL in text wire format so the server parses them into the column's own type.",
"Recent connections fix — the Recent strip no longer renders empty on first launch when the connections list hadn't loaded yet.",
"Full CI release pipeline for Windows — the self-hosted Windows runner now builds the .exe/.msi installers (bundled pg_dump/pg_restore/psql + MariaDB clients) and uploads them to the release alongside Linux.",
],
},
{
version: "0.7.10",
date: "2026-08-13",
+7 -1
View File
@@ -36,8 +36,13 @@ const isProd = import.meta.env.PROD;
<link rel="canonical" href={canonicalURL} />
<link rel="sitemap" href="/sitemap-index.xml" />
<link rel="icon" type="image/svg+xml" href="/gridline-icon.svg" />
<link rel="apple-touch-icon" href="/gridline-icon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="preload" as="image" href="/og.png" />
<link rel="preconnect" href="https://stat.ranio.xyz" crossorigin />
<link rel="preconnect" href="https://git.ranio.xyz" />
<link rel="dns-prefetch" href="https://stat.ranio.xyz" />
<link rel="dns-prefetch" href="https://git.ranio.xyz" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
@@ -54,6 +59,7 @@ const isProd = import.meta.env.PROD;
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageURL} />
<meta name="twitter:image:alt" content="Gridline — the open-source database GUI" />
<meta name="twitter:site" content="@AdrianBonpin" />
<meta name="twitter:creator" content="@AdrianBonpin" />
+15
View File
@@ -6,6 +6,7 @@ import {
buildWebSiteJsonLd,
buildBreadcrumbJsonLd,
buildTitle,
buildProductJsonLd,
} from "./seo";
describe("seo builders", () => {
@@ -70,4 +71,18 @@ describe("seo builders", () => {
it("buildTitle appends the brand", () => {
expect(buildTitle("PostgreSQL GUI")).toBe("PostgreSQL GUI | Gridline");
});
it("buildProductJsonLd returns valid Product schema", () => {
const ld = buildProductJsonLd({
name: "Gridline",
url: "https://getgridline.app/",
description: "Open-source database GUI",
image: "https://getgridline.app/og.png",
offersPrice: 0,
offersCurrency: "USD",
});
expect(ld["@type"]).toBe("Product");
expect(ld.name).toBe("Gridline");
expect((ld.offers as { price: number }).price).toBe(0);
});
});
+23
View File
@@ -108,3 +108,26 @@ export function buildBreadcrumbJsonLd(
export function buildTitle(primary: string, brand = "Gridline"): string {
return `${primary} | ${brand}`;
}
export function buildProductJsonLd(opts: {
name: string;
url: string;
description: string;
image: string;
offersPrice: number;
offersCurrency: string;
}): JsonLd {
return {
"@context": "https://schema.org",
"@type": "Product",
name: opts.name,
url: opts.url,
description: opts.description,
image: opts.image,
offers: {
"@type": "Offer",
price: opts.offersPrice,
priceCurrency: opts.offersCurrency,
},
};
}
+16 -10
View File
@@ -6,6 +6,8 @@ import {
buildFaqJsonLd,
buildOrganizationJsonLd,
buildWebSiteJsonLd,
buildProductJsonLd,
buildBreadcrumbJsonLd,
} from "../lib/seo";
import { Image } from "astro:assets";
import HeroDbViewer from "../components/hero/HeroDbViewer";
@@ -13,7 +15,7 @@ import queryEditor from "../assets/screenshots/query-editor.png";
import erDiagram from "../assets/screenshots/er-diagram.png";
import home from "../assets/screenshots/home.png";
import dataGrid from "../assets/screenshots/data-grid.png";
const version = "0.7.12";
const version = "0.7.14";
const site = "https://getgridline.app";
const jsonLd = [
buildSoftwareApplicationJsonLd({
@@ -42,9 +44,21 @@ const jsonLd = [
name: "Gridline",
url: `${site}/`,
}),
buildProductJsonLd({
name: "Gridline",
url: `${site}/`,
description:
"The open-source database GUI for PostgreSQL, MySQL, SQLite, and Redis. No paywalls, no tab limits, fully open source.",
image: `${site}/og.png`,
offersPrice: 0,
offersCurrency: "USD",
}),
buildBreadcrumbJsonLd([
{ name: "Home", url: `${site}/` },
]),
];
---
<Layout title="Gridline — Open-Source Database GUI" jsonLd={jsonLd}>
<Layout title="Gridline — Free Open-Source Database GUI" jsonLd={jsonLd}>
<script src="/landing.js" defer></script>
<!-- Hero -->
@@ -66,14 +80,6 @@ const jsonLd = [
View on Gitea
</a>
</div>
<div id="macos-hint" class="hidden max-w-lg mx-auto w-full rounded-xl border border-[var(--border)] bg-[var(--surface)] p-3 text-left mt-4">
<p class="text-xs text-[var(--text-muted)] mb-2">First launch on macOS? Run this in Terminal:</p>
<div class="flex items-center justify-between gap-3 rounded-lg bg-[var(--bg)] border border-[var(--border)] px-3 py-2">
<code class="font-mono text-xs truncate">sudo xattr -dr com.apple.quarantine /Applications/Gridline.app</code>
<button id="copy-quarantine" class="text-xs font-medium text-[var(--primary)] hover:text-[var(--primary-hover)] transition-colors shrink-0" aria-label="Copy command">Copy</button>
</div>
</div>
</div>
<div class="hero-screenshot mt-14 md:mt-20 mx-auto max-w-6xl">