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
69 changes: 42 additions & 27 deletions .github/workflows/docker-release.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
name: Docker release

# Build and publish the Tabletsgo image whenever a semver tag is pushed
# (e.g. v0.11.1). Push a tag with: git tag v0.11.1 && git push origin v0.11.1
# Build and publish the Tabletsgo image for a release. Two entry points:
# 1. push a semver tag by hand (e.g. v0.11.1) — builds from that tag as-is.
# 2. called by release.yml (workflow_call) after it bumps + commits + tags,
# so the image is built from the version-synced commit.
on:
push:
tags:
- 'v*.*.*'
workflow_call:
inputs:
version:
description: 'Version without leading v, e.g. 0.16.2'
required: true
type: string
ref:
description: 'Commit SHA to build (the version-synced commit)'
required: true
type: string

# A re-pushed/moved tag (e.g. re-tagging after a merge) cancels the older
# A re-pushed/moved tag (or a re-run of the same version) cancels the older
# in-progress build instead of running two builds of the same version.
concurrency:
group: docker-release-${{ github.ref }}
group: docker-release-${{ inputs.version || github.ref }}
cancel-in-progress: true

env:
Expand All @@ -26,8 +38,12 @@ jobs:
packages: write

steps:
# For a manual tag push this is the tag; when called by release.yml it is
# the exact version-synced commit that release.yml just created + tagged.
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}

- name: Set up QEMU (multi-arch emulation)
uses: docker/setup-qemu-action@v3
Expand All @@ -42,38 +58,36 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Derives tags/labels from the git ref, e.g. tag v0.11.1 ->
# ghcr.io/<owner>/<repo>:0.11.1
# ghcr.io/<owner>/<repo>:0.11
# ghcr.io/<owner>/<repo>:latest
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest

# Version = the tag without the leading "v" (e.g. v0.16.0 -> 0.16.0).
- name: Derive version
# Version = the workflow_call input, or the tag without the leading "v"
# (e.g. v0.16.2 -> 0.16.2). Tags: <version>, <major>.<minor>, latest.
- name: Derive version and image tags
id: ver
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
run: |
V="${{ inputs.version }}"
if [ -z "$V" ]; then V="${GITHUB_REF_NAME#v}"; fi
echo "version=$V" >> "$GITHUB_OUTPUT"
IMAGE=$(echo "${REGISTRY}/${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]')
MM="${V%.*}"
{
echo "tags<<EOF"
echo "${IMAGE}:${V}"
echo "${IMAGE}:${MM}"
echo "${IMAGE}:latest"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.ver.outputs.tags }}
# Baked into the image so the running app reports its own identity and
# the in-app update checker can compare (version, sha).
build-args: |
APP_VERSION=${{ steps.ver.outputs.version }}
GIT_SHA=${{ github.sha }}
GIT_SHA=${{ inputs.ref || github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

Expand All @@ -87,9 +101,10 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.ver.outputs.version }}
SHA: ${{ github.sha }}
SHA: ${{ inputs.ref || github.sha }}
MIN_UPGRADE_FROM: ${{ vars.MIN_UPGRADE_FROM }}
run: |
TAG="v${VERSION}"
cat > release.json <<EOF
{
"version": "${VERSION}",
Expand All @@ -100,5 +115,5 @@ jobs:
"minUpgradeFrom": "${MIN_UPGRADE_FROM}"
}
EOF
gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || gh release create "$GITHUB_REF_NAME" --generate-notes
gh release upload "$GITHUB_REF_NAME" release.json --clobber
gh release view "$TAG" >/dev/null 2>&1 || gh release create "$TAG" --generate-notes
gh release upload "$TAG" release.json --clobber
67 changes: 67 additions & 0 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Release publish

# Step 2 of the release: fires when a release/* PR (opened by release.yml) is
# MERGED into v0.x.x. It tags the merged commit — which already carries the
# version-synced package.json + README — and builds/publishes the image from it,
# guaranteeing the tag and the synced files are the exact same commit.
on:
pull_request:
types: [closed]
branches: [v0.x.x]

jobs:
tag:
# Only merged release/v* PRs — not closed-without-merge, not other branches.
if: >-
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.head.ref, 'release/v')
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.info.outputs.version }}
sha: ${{ steps.info.outputs.sha }}
steps:
- name: Derive version + merged commit
id: info
env:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
V="${HEAD_REF#release/v}"
echo "$V" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' \
|| { echo "::error::Bad release branch name: $HEAD_REF"; exit 1; }
echo "version=$V" >> "$GITHUB_OUTPUT"
echo "sha=$MERGE_SHA" >> "$GITHUB_OUTPUT"

- name: Checkout merged commit
uses: actions/checkout@v4
with:
ref: ${{ steps.info.outputs.sha }}
fetch-depth: 0

- name: Tag the merged commit
env:
VERSION: ${{ steps.info.outputs.version }}
run: |
if git ls-remote --tags --exit-code origin "refs/tags/v${VERSION}" >/dev/null 2>&1; then
echo "Tag v${VERSION} already exists; skipping tag push."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag "v${VERSION}"
git push origin "v${VERSION}"

# Build/publish from the exact merged (version-synced) commit. Called directly
# because a GITHUB_TOKEN tag push does not trigger docker-release's push:tags.
build:
needs: tag
permissions:
contents: write
packages: write
uses: ./.github/workflows/docker-release.yml
with:
version: ${{ needs.tag.outputs.version }}
ref: ${{ needs.tag.outputs.sha }}
secrets: inherit
86 changes: 86 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Release

# Step 1 of the release: open a version-bump PR. Branch protection on v0.x.x
# means we can't push the bump directly, so this raises a PR you review + merge.
# Merging it triggers release-publish.yml, which tags the merged commit and
# builds the image — so the tag and the version-synced files are the SAME commit.
#
# Trigger from the Actions tab (Run workflow) or:
# gh workflow run release.yml -f version=0.16.5
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release, no leading v (e.g. 0.16.5)'
required: true
type: string

concurrency:
group: release-${{ inputs.version }}
cancel-in-progress: false

jobs:
open-pr:
runs-on: ubuntu-latest
permissions:
contents: write # push the release/* branch
pull-requests: write # open the PR
steps:
- name: Checkout default branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 0

- name: Normalize + validate version
id: norm
run: |
V="${{ inputs.version }}"
V="${V#v}"
echo "$V" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' \
|| { echo "::error::Invalid semver: '$V' (expected X.Y.Z)"; exit 1; }
echo "version=$V" >> "$GITHUB_OUTPUT"

- name: Refuse if already released
env:
VERSION: ${{ steps.norm.outputs.version }}
run: |
if git ls-remote --tags --exit-code origin "refs/tags/v${VERSION}" >/dev/null 2>&1; then
echo "::error::Tag v${VERSION} already exists"; exit 1
fi

- name: Bump package.json + README badge
env:
VERSION: ${{ steps.norm.outputs.version }}
run: |
# package.json "version" (npm pkg set edits in place, no git tag).
npm pkg set "version=${VERSION}"
# README badge: version-<X>-<6-hex-color> — keep the color, swap version.
sed -i -E "s|badge/version-[^-]*-([0-9A-Fa-f]{6})|badge/version-${VERSION}-\1|" README.md
if git diff --quiet -- package.json README.md; then
echo "::error::Nothing to bump — files already show v${VERSION}"; exit 1
fi

- name: Push release branch + open PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.norm.outputs.version }}
BASE: ${{ github.event.repository.default_branch }}
run: |
BR="release/v${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$BR"
git add package.json README.md
git commit -m "release: v${VERSION}"
git push -f origin "$BR"
# Reuse an open PR for this branch if one exists, else open a new one.
if [ -n "$(gh pr list --head "$BR" --state open --json number -q '.[0].number')" ]; then
echo "PR already open for $BR."
else
gh pr create \
--base "$BASE" \
--head "$BR" \
--title "release: v${VERSION}" \
--body "Automated version bump to \`v${VERSION}\` (package.json + README badge). Merge this PR to tag the merged commit, build the multi-arch image, and publish the GitHub Release."
fi
21 changes: 17 additions & 4 deletions BACKEND_DOCUMENTATION.MD
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,11 @@ Node-graph automations (React Flow) run against the connection. A workflow's
`graph` is `{ nodes, edges }` in React Flow shape; each node's `type` is one of
`manual` (trigger), `schedule` (trigger), `query`, `http`, `js`, `switch`, `loop`,
`export`, `storage`, and its config lives in `node.data`:
- `query`: `{ sql }` · `http`: `{ method, url, headers, body }` · `js`: `{ code }`
- `query`: `{ sql }` — the SQL may inline scalar values from the node's input as
`{{input.path}}` (dotted paths supported, e.g. `{{input.row.id}}`); values are
rendered as SQL literals (strings quoted with `''` doubling, numbers bare,
booleans `TRUE`/`FALSE`, missing → `NULL`; a non-scalar value fails the node).
· `http`: `{ method, url, headers, body }` · `js`: `{ code }`
- `switch`: `{ cases: [{ expr, label }] }` · `loop`: `{ itemsExpr }`
- `schedule`: `{ frequency: "manual"|"hourly"|"daily", hourOfDay?: 0-23 }` — UTC.
**Breaking change:** this replaces the old freeform `{ cron }` field, which is now inert.
Expand All @@ -698,6 +702,9 @@ Node-graph automations (React Flow) run against the connection. A workflow's

Edges thread a single `input` (a node's output) to the next node; `sourceHandle`
selects a branch (`out` default; `case-N`/`default` for switch; `body`/`done` for loop).
Trigger nodes (`manual`/`schedule`) pass the run's initial input straight through —
`null` normally, or the `input` posted to `/run` (e.g. a dashboard table row from a
row-action button).

A workflow can be scheduled: set `workflows.schedule_enabled = 1` (via `PUT
.../workflows/:wid { "scheduleEnabled": true }`) with a graph containing exactly
Expand Down Expand Up @@ -766,9 +773,15 @@ Delete a workflow.
### POST `/api/connections/:id/workflows/:wid/run`
Execute a workflow. Runs the posted `graph` (so unsaved edits can be tested), or
the stored graph when `graph` is omitted. Always records the outcome in `workflow_runs`
(`trigger_kind: "manual"`).

- **Body:** `{ "graph"?: { "nodes": [...], "edges": [...] } }`
(`trigger_kind` from `trigger`).

- **Body:** `{ "graph"?: { "nodes": [...], "edges": [...] }, "input"?: <any JSON>, "trigger"?: "manual" | "dashboard" }`
- `input` (optional) seeds the trigger node — it flows to downstream nodes as their
initial input (JS nodes read it as `input`, query nodes can inline scalars via
`{{input.field}}`). Dashboard table-widget row actions post the clicked row here
as a column→value object.
- `trigger` (optional) tags the run in `workflow_runs.trigger_kind` for auditing:
`"dashboard"` for dashboard row-action runs; anything else records `"manual"`.
- **200:** `{ "ok": true, "log": [{ "nodeId", "nodeType", "status", "ms", "output", "logs"?, "error"? }], "output": <terminal output> }`
- On a failed node: `{ "ok": false, "log": [...], "error": "<message>" }` (the failing node's `log` entry has `status: "error"`)
- **404:** `{ "error": "Connection not found" }` or `{ "error": "Not found" }`
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ src/
│ │ │ # ({{name}} in widget SQL, query-backed or static lists), free-placement
│ │ │ # 12-col drag/resize grid (hard collision blocking), fullscreen, JSON
│ │ │ # export/import. Charts are hand-rolled SVG (no chart/grid deps).
│ │ │ # Table widgets: optional server-side pagination (LIMIT/OFFSET wrap,
│ │ │ # N+1 has-more probe) + per-row action buttons (up to 3, styled via
│ │ │ # variant/icon, optional per-row condition that disables/hides by a
│ │ │ # column value) that run a workflow with the clicked row as its trigger
│ │ │ # input (Widget.pageSize/rowActions; icons in lib/rowActionIcons).
│ │ ├── components/ # DashboardView (tab content: toolbar + VariableBar + WidgetGrid),
│ │ │ # DashboardsPanel (rail list), WidgetCard (runs its query), WidgetChart,
│ │ │ # WidgetEditor + DashboardSettings (modals), MarkdownText,
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ Self‑hosted, single Docker image, your data stays yours.
| 📊 | **Data browser** | Fast, spreadsheet‑style data grid — filter, sort, edit rows inline, insert & delete, with a staged **Changes** panel before you commit. |
| 🧮 | **SQL editor** | CodeMirror‑powered editor with SQL highlighting, formatting, query history, and reusable **saved queries**. |
| 🎨 | **Schema designer** | Visual ERD (React Flow) to create/edit tables and columns; staged DDL with a **schema version** audit trail and best‑effort rollback SQL. |
| ⚙️ | **Workflows** | Drag‑and‑drop automation builder: `Manual`/`Schedule` triggers → `Run query`, `HTTP Request`, `Run JavaScript`, `Switch`, `Loop`, `Export SQL`, `Store to Storage`. Real hourly/daily scheduling + run logs. |
| ⚙️ | **Workflows** | Drag‑and‑drop automation builder: `Manual`/`Schedule` triggers → `Run query`, `HTTP Request`, `Run JavaScript`, `Switch`, `Loop`, `Export SQL`, `Store to Storage`. Real hourly/daily scheduling + run logs. Runs can carry an input payload; query nodes inline it as `{{input.field}}`. |
| 📈 | **Dashboards** | Per‑connection query dashboards with dynamic `{{variables}}`, drag/resize grid, and JSON export/import. Table widgets support server‑side pagination and per‑row **action buttons** that run a workflow with the clicked row as its input. |
| 💾 | **Backups & restore** | S3‑compatible storage destinations + per‑connection scheduled backups. Calendar heatmap of runs and point‑in‑time restore — from a tracked backup version, any file browsed out of a storage destination, or a backup file uploaded from your computer. |
| 👥 | **Workspaces & teams** | Multi‑workspace (org/tenant) model with members, teams, and `admin`/`member` roles. Email invites (SMTP optional — always get a copyable link). |
| 🔑 | **Auth & security** | First‑run setup wizard, token‑based auth, per‑workspace roles, and membership‑guarded connection routes. |
Expand Down
Loading