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
101 changes: 101 additions & 0 deletions .github/workflows/headless-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Headless image

# Publishes ghcr.io/queryahub/querya-headless:<version> (and :latest) after a
# Release succeeds, or on demand for an existing release.
#
# `workflow_run`, not `release`: the release is created with GITHUB_TOKEN, and
# events made by that token do not start other workflows.
on:
workflow_run:
workflows: [Release]
types: [completed]
workflow_dispatch:
inputs:
version:
description: Querya Desktop release to build the image for (e.g. 0.4.19)
required: true
latest:
description: Also tag the image as latest
type: boolean
default: true

permissions:
contents: read
packages: write

jobs:
image:
name: Build and push querya-headless
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v4

- name: Resolve the version
id: v
shell: bash
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
LATEST="${{ inputs.latest }}"
else
# A tag push has the tag as head_branch.
VERSION="${{ github.event.workflow_run.head_branch }}"
LATEST=true
fi
VERSION="${VERSION#v}"
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::notice::'$VERSION' is not a release tag, nothing to build"
echo "build=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "build=true" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "latest=$LATEST" >> "$GITHUB_OUTPUT"

- uses: docker/setup-buildx-action@v3
if: steps.v.outputs.build == 'true'

- uses: docker/login-action@v3
if: steps.v.outputs.build == 'true'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build (loaded locally for the smoke test)
if: steps.v.outputs.build == 'true'
uses: docker/build-push-action@v6
with:
context: docker/headless
file: docker/headless/Dockerfile
build-args: QUERYA_VERSION=${{ steps.v.outputs.version }}
load: true
tags: querya-headless:test

- name: Smoke test
if: steps.v.outputs.build == 'true'
shell: bash
run: |
set -euo pipefail
docker run --rm querya-headless:test --help | grep -q "Usage: querya-ext-tester"
mkdir -p empty
set +e
docker run --rm -v "$PWD/empty:/ext:ro" querya-headless:test /ext --no-color
code=$?
set -e
# No manifest.json: the run must fail with exit code 1.
test "$code" -eq 1

- name: Push
if: steps.v.outputs.build == 'true'
uses: docker/build-push-action@v6
with:
context: docker/headless
file: docker/headless/Dockerfile
build-args: QUERYA_VERSION=${{ steps.v.outputs.version }}
push: true
tags: |
ghcr.io/queryahub/querya-headless:${{ steps.v.outputs.version }}
${{ steps.v.outputs.latest == 'true' && 'ghcr.io/queryahub/querya-headless:latest' || '' }}
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,9 @@ jobs:
mkdir -p build/ext-tester
EXE=querya-ext-tester
if [ "${{ runner.os }}" = "Windows" ]; then EXE=querya-ext-tester.exe; fi
dart compile exe bin/querya_ext_tester.dart -o "build/ext-tester/$EXE"
# Pure Dart in a throwaway package: `dart compile exe` rejects the
# app's graph (build hooks in sqlite3 and objective_c).
scripts/ci/build_ext_tester.sh "build/ext-tester/$EXE"
MCP=querya-mcp
if [ "${{ runner.os }}" = "Windows" ]; then MCP=querya-mcp.exe; fi
mkdir -p build/mcp
Expand Down
25 changes: 15 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Everything merged to `dev` since 0.4.18: the NoSQL workbench, SSH tunnelling and Production safeguards, the ER diagram, charts and plans, the MCP server, a rebuilt SQL workspace toolbar, a UI kit, and a round of session and connection correctness fixes.
### Planned

- Planned 0.5.0 — live Marketplace download and install
- MCP v2: MongoDB and Redis, extension drivers, Streamable HTTP, writes with confirmation
- Server-side statement timeouts for editor and browse sessions
- High-refresh Linux compositor investigations (#980)

## [0.4.19] - 2026-10-11

The NoSQL workbench, SSH tunnelling and Production safeguards, the ER diagram (layout, groups, notes, saved views, DBML and PDF export), charts and plans, the read-only MCP server, a rebuilt SQL workspace toolbar and run modes, one control scale and button family across the UI kit, a first round of leaving `shadcn_flutter`, security fixes (PostgreSQL TLS, the extension sandbox), and a round of session and connection correctness fixes.

### Added

Expand Down Expand Up @@ -76,6 +85,7 @@ Everything merged to `dev` since 0.4.18: the NoSQL workbench, SSH tunnelling and
**Platform, packaging and tooling**

- **Portable mode out of the box** through a `.portable` marker (#1056).
- **Standalone Flathub manifest and bundle icon (#1060)** in `packaging/linux/flatpak/`.
- **macOS .dmg** in the release pipeline; **AppStream metainfo** for Flatpak; **signed APT and DNF repositories** from the release workflow.
- **Team connection profile export / import.**
- **Extension test harness and CLI runner** (`querya-ext-tester`), published with releases; CI recipes for extension authors.
Expand All @@ -94,7 +104,7 @@ Everything merged to `dev` since 0.4.18: the NoSQL workbench, SSH tunnelling and
- **Flutter is pinned to 3.47.1**, the version the 120 Hz Linux engine is built for.
- **Dialog footers** — About, Driver manager, Preferences, the connection forms and the small name dialogs use `QueryaDialogActions`: Cancel / Close is ghost, Test connection sits between Cancel and the main action, spacing is 8 px, one divider (#1441).
- **One height per toolbar row** — the SQL, ERD, chart and results toolbars set one control size, and an icon button is the same control as every other button (#1439, #1440).
- **ERD view split into parts (#1362)** — layout, routing and geometry moved to `core/erd` (`ErdLayoutEngine`, unit-tested without widgets); cards, relation painter, notes, groups, export data, menus and status bodies are their own files.
- **ERD view split into parts (#1362)** — layout, routing and geometry moved to `core/erd` (`ErdLayoutEngine`, unit-tested without widgets); cards, relation painter, notes, groups, export data, the export flows (`ErdExporter`: Mermaid, DBML, SVG, PNG, PDF), menus and status bodies are their own files.
- **SQL workspace toolbar, connection forms and the editor's highlighter lifecycle** are separate, tested units (`SqlWorkspaceToolbar`, form sections for network, credentials and SSL, a highlight controller managed per session).
- **`QueryaApp.build` has no side effects** — the async loads and one-time work moved out of it.

Expand All @@ -118,11 +128,13 @@ Everything merged to `dev` since 0.4.18: the NoSQL workbench, SSH tunnelling and
- **SQLite** — columns with the same name stay separate in query results (#1144).
- **Updater and Windows** — the Linux bundle replace preserves `QueryaData` and `.portable` (#1055); file associations and registry writes are optional (#1054).
- **Packaging** — desktop and icon caches are refreshed after install and removal.
- **Run on an empty editor says so (#1478)** — *Nothing to run* instead of silence; the status bar follows the active tab and no longer shows another tab's rows and time or repeats the row count (#1479).
- **Removing a connection** asks first and leaves its workspace; *Remove connection* is a destructive menu item.
- **SQL workspace** — the status bar no longer keeps *Running query…* after a failed run; a long query error scrolls instead of overflowing the results pane; a code editor that fills its space starts its text at the top.
- **Editor** — no duplicate `onChanged`, no false cursor jumps, default highlighting on (#1430); MongoDB documents in JSON mode are highlighted (#1432).
- **Data grid** — the WHERE clause of an edit uses the column's real type and tells the string `'NULL'` from SQL `NULL`; the value panel no longer validates twice (#1431).
- **UI** — export and copy menu buttons have one chevron; the tab strip draws its focus ring only for keyboard focus (#1438); dropdown menus are at least as wide as their trigger (#1444); form rules use the theme's border colour, not black, in the light theme (#1443); the new-connection wizard search is the standard search field (#1442).
- **Look and feel** — a disabled primary button is a grey chip with a readable label; the SQL toolbar badge reads *Auto-commit* / *Manual commit* before the first run; below 1100 px the ERD toolbar shows *Note*, *Auto layout* and the group buttons as icons so *Export* does not wrap alone; Preferences MCP buttons are the same size as the rest.
- **Startup recovery** — when the local database cannot be opened, the app shows a recovery screen instead of failing silently.
- **Updater (Windows)** — an in-place update mirrors the new bundle, so obsolete DLLs and assets are removed.
- **SSH** — a failed tunnel closes its sockets and the jump socket, a missing key file is reported, concurrent callers share one dial, and a local forwarder that survived a failed keep-alive is closed; a dropped socket marks the session closed (`isConnected`).
Expand All @@ -149,16 +161,9 @@ Everything merged to `dev` since 0.4.18: the NoSQL workbench, SSH tunnelling and

- **E2E suite** (`test/e2e`, run in CI and gating `release`): connection lifecycle and forms of every type, folders, SSH bastion tunnel, credential scrubbing, SQL workspaces and export, command palette, extensions sandbox, Redis CLI and key tree, Mongo viewer, index manager, aggregations, CRUD, theme import, shutdown.
- **Test sections** run as parallel jobs; self-hosted runners; a MySQL server and bubblewrap for the live database and sandbox tests.
- **Guards** — UI kit usage, and `core/` and `shared/` free of `features/` and `app/` imports; `lib/shared` must not import `shadcn_flutter` (a ratchet); no raw `Divider` outside the kit.
- **Guards** — UI kit usage, and `core/` and `shared/` free of `features/` and `app/` imports; `lib/shared` must not import `shadcn_flutter` (a ratchet); no raw `Divider`, no shadcn or raw Material buttons, no `*Button` class outside the kit (two allowed) and no literal tooltip delay (`test/guards/`).
- **Benchmarks** compare build time over interleaved runs and cover ERD, charts and multi-tab SQL scenarios.

### Planned

- Planned 0.5.0 — live Marketplace download and install
- MCP v2: MongoDB and Redis, extension drivers, Streamable HTTP, writes with confirmation
- Server-side statement timeouts for editor and browse sessions
- High-refresh Linux compositor investigations (#980)

## [0.4.18] - 2026-09-28

Shell and motion performance (sidebar layout isolation and offstage rendering), data-loss guard rails, driver session correctness, and high-refresh grid reliability.
Expand Down
19 changes: 17 additions & 2 deletions bin/querya_ext_tester.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@ Options:
--timeout-ms <n> per-request timeout (default 5000)
--junit <path> write a JUnit XML report
--tap <path> write a TAP report
--trace <path> write every RPC call and a bounded view of the data as JSON
--no-color disable ANSI colors
-h, --help show this help
''';

Future<int> main(List<String> args) async {
/// The exit code is set through `exitCode`: a Dart `main` cannot return one
/// (the `Future<int>` it used to return was ignored, so a failed run exited 0).
Future<void> main(List<String> args) async {
exitCode = await _run(args);
}

Future<int> _run(List<String> args) async {
String? dir;
var target = '0.5.0';
String? connect;
Expand All @@ -42,6 +49,7 @@ Future<int> main(List<String> args) async {
var timeoutMs = 5000;
String? junit;
String? tap;
String? trace;
var color = stdout.hasTerminal;

String? value(int i, String flag) {
Expand Down Expand Up @@ -69,7 +77,8 @@ Future<int> main(List<String> args) async {
'--database' ||
'--timeout-ms' ||
'--junit' ||
'--tap':
'--tap' ||
'--trace':
final v = value(i, a);
if (v == null) return 2;
i++;
Expand All @@ -95,6 +104,8 @@ Future<int> main(List<String> args) async {
junit = v;
case '--tap':
tap = v;
case '--trace':
trace = v;
}
default:
if (a.startsWith('-') || dir != null) {
Expand Down Expand Up @@ -153,5 +164,9 @@ Future<int> main(List<String> args) async {
stdout.write(report.toConsole(color: color));
if (junit != null) File(junit).writeAsStringSync(report.toJUnitXml());
if (tap != null) File(tap).writeAsStringSync(report.toTap());
if (trace != null) {
File(trace).writeAsStringSync(report.toTraceJson());
stdout.writeln('RPC trace of ${report.rpc.length} calls written to $trace');
}
return report.isSuccess ? 0 : 1;
}
49 changes: 49 additions & 0 deletions docker/headless/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# querya-headless: the extension tester of a Querya Desktop release in a pinned,
# reproducible environment, to run in CI or locally without a display, Flutter
# or the Querya source tree.
#
# docker build -f docker/headless/Dockerfile \
# --build-arg QUERYA_VERSION=0.4.19 -t querya-headless:0.4.19 docker/headless
#
# docker run --rm -v "$PWD/dist:/ext:ro" querya-headless:0.4.19 /ext \
# --connect '{"host":"db","port":5432}' --trace /out/trace.json
#
# The tester is downloaded from the GitHub Release and checked against its
# SHA256SUMS.txt; the build fails when the checksum does not match.
#
# glibc (Debian) on purpose: extension entry points are native executables, and
# most of them are not linked against musl.
FROM debian:bookworm-slim

ARG QUERYA_VERSION
ARG TARGET=linux-x64

RUN test -n "${QUERYA_VERSION}" || (echo "set --build-arg QUERYA_VERSION=x.y.z" >&2; exit 1)

# bubblewrap is the OS sandbox of `--sandbox`; curl and tar fetch the tester.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl tar bubblewrap \
&& rm -rf /var/lib/apt/lists/*

RUN set -eux; \
base="https://github.com/QueryaHub/Querya-Desktop/releases/download/${QUERYA_VERSION}"; \
asset="querya-ext-tester-v${QUERYA_VERSION}-${TARGET}.tar.gz"; \
cd /tmp; \
curl -fsSLO "${base}/${asset}"; \
curl -fsSLO "${base}/SHA256SUMS.txt"; \
grep " ${asset}\$" SHA256SUMS.txt | sha256sum -c -; \
tar -xzf "${asset}" -C /usr/local/bin querya-ext-tester; \
rm -f "${asset}" SHA256SUMS.txt; \
querya-ext-tester --help > /dev/null

LABEL org.opencontainers.image.title="querya-headless" \
org.opencontainers.image.description="Querya Desktop extension tester (headless)" \
org.opencontainers.image.source="https://github.com/QueryaHub/Querya-Desktop" \
org.opencontainers.image.version="${QUERYA_VERSION}"

RUN useradd --create-home --uid 1000 querya
USER querya
WORKDIR /ext

ENTRYPOINT ["querya-ext-tester"]
CMD ["--help"]
97 changes: 97 additions & 0 deletions docs/extension-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ querya-ext-tester ./my-extension \
| `--sandbox` | run the plugin in the OS sandbox (`bwrap` on Linux, `sandbox-exec` on macOS) when available |
| `--timeout-ms <n>` | per-request timeout (default 5000) |
| `--junit <path>` / `--tap <path>` | write a JUnit XML / TAP report |
| `--trace <path>` | write every RPC round trip as JSON: method, time, request and response size and a bounded view of the data (see *Following the data*) |
| `--no-color` | plain output |

Exit codes: `0` all checks passed (warnings and skips do not fail), `1` a
Expand All @@ -80,6 +81,102 @@ Credentials only travel inside the `db.connect` payload over the plugin's
stdio; they are never put on the command line of the plugin or into its
environment. Use a throwaway database in CI.

## Following the data: `--trace`

A green run says the lifecycle works. `--trace trace.json` shows *what* went
over the wire, so you can see whether the data of your driver arrives as the
host expects:

```json
{
"extension": "acme.clickhouse-driver",
"targetVersion": "0.5.0",
"success": true,
"totalMs": 41,
"checks": [{ "name": "db.query", "status": "passed", "ms": 3 }],
"rpc": [
{ "method": "db.connect", "ms": 12, "ok": true, "requestBytes": 76,
"responseBytes": 28, "request": { "host": "db", "password": "***" } },
{ "method": "db.query", "ms": 3, "ok": true, "requestBytes": 47,
"responseBytes": 1760,
"response": { "columns": ["id", "name"], "rows": [[0, "user0"], "… (+7 more)"] } }
]
}
```

- Values of keys that look like secrets (`password`, `secret`, `token`,
`passphrase`, ...) are written as `***`.
- Responses are cut: at most 5 items per list, strings of at most 120
characters. The file shows the shape and a sample, not a copy of your data;
still, use a throwaway database and do not publish traces of real data.
- A failing call is recorded with its `error`, so a CI artifact is enough to
see which call broke and after how long.

## Docker image

`ghcr.io/queryahub/querya-headless:<version>` (and `:latest`) is the tester of
that Querya Desktop release in a pinned environment: Debian (glibc), the
tester checked against the release's `SHA256SUMS.txt`, `bubblewrap` for
`--sandbox`, a non-root user, entry point `querya-ext-tester`. Nothing else is
needed on the machine: no Flutter, no display, no Querya install.

```bash
docker pull ghcr.io/queryahub/querya-headless:latest

docker run --rm \
-v "$PWD/dist:/ext:ro" -v "$PWD/out:/out" \
ghcr.io/queryahub/querya-headless:0.4.19 /ext \
--connect '{"host":"db","port":5432}' \
--junit /out/junit.xml --trace /out/trace.json
```

The extension's `main` is a native executable and runs **inside** the image, so
build it for Linux x86-64 against glibc. To reach a database from the image,
put both on one Docker network and use the database's service name as the host.

### In CI, next to a test database

```yaml
jobs:
rpc:
runs-on: ubuntu-latest
services:
db:
image: postgres:16-alpine
env: { POSTGRES_PASSWORD: querya, POSTGRES_USER: querya, POSTGRES_DB: querya }
options: >-
--health-cmd "pg_isready -U querya" --health-interval 5s
--health-retries 12
steps:
- uses: actions/checkout@v4
- run: ./build.sh # writes ./dist with manifest.json
- name: RPC and data check
run: |
mkdir -p out && chmod 777 out
docker run --rm --network "${{ job.services.db.network }}" \
-v "$PWD/dist:/ext:ro" -v "$PWD/out:/out" \
ghcr.io/queryahub/querya-headless:0.4.19 /ext \
--target-version 0.5.0 \
--connect '{"host":"db","port":5432,"username":"querya","password":"querya","database":"querya"}' \
--query 'SELECT 1' --junit /out/junit.xml --trace /out/trace.json
- uses: actions/upload-artifact@v4
if: always()
with: { name: rpc-trace, path: out/ }
```

Use the same `docker run` locally with `--network querya-dev_default` against
the stack from [dev-containers.md](dev-containers.md) (`host` is the service
name, for example `postgres`).

### `--sandbox` in a container

The OS sandbox is `bubblewrap`, which needs unprivileged user namespaces. Many
container runtimes block them by default; the tester then runs the plugin
without the sandbox and says so in the report. To test the sandboxed run, start
the container with `--security-opt seccomp=unconfined` (and, on hosts that
restrict namespaces with AppArmor, `--security-opt apparmor=unconfined`), or
run the tester directly on the CI runner with the action below.

## GitHub Actions

This repository contains a composite action in
Expand Down
Loading
Loading