queryview 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- queryview-0.0.2/.claude/settings.json +23 -0
- queryview-0.0.2/.github/actions/start-git-daemon/action.yaml +31 -0
- queryview-0.0.2/.github/workflows/ci.yml +182 -0
- queryview-0.0.2/.github/workflows/publish.yaml +443 -0
- queryview-0.0.2/.gitignore +39 -0
- queryview-0.0.2/.mcp.json +15 -0
- queryview-0.0.2/.pre-commit-config.yaml +23 -0
- queryview-0.0.2/CLAUDE.md +39 -0
- queryview-0.0.2/LICENSE +21 -0
- queryview-0.0.2/PKG-INFO +183 -0
- queryview-0.0.2/README.md +159 -0
- queryview-0.0.2/backend/alembic.ini +9 -0
- queryview-0.0.2/backend/queryview/__init__.py +0 -0
- queryview-0.0.2/backend/queryview/conftest.py +86 -0
- queryview-0.0.2/backend/queryview/connect.py +428 -0
- queryview-0.0.2/backend/queryview/dashboard_queries.py +75 -0
- queryview-0.0.2/backend/queryview/dashboards.py +156 -0
- queryview-0.0.2/backend/queryview/drivers/__init__.py +10 -0
- queryview-0.0.2/backend/queryview/drivers/base.py +165 -0
- queryview-0.0.2/backend/queryview/drivers/clickhouse.py +138 -0
- queryview-0.0.2/backend/queryview/drivers/duckdb.py +153 -0
- queryview-0.0.2/backend/queryview/drivers/postgres.py +166 -0
- queryview-0.0.2/backend/queryview/drivers/test_base.py +49 -0
- queryview-0.0.2/backend/queryview/drivers/test_clickhouse.py +53 -0
- queryview-0.0.2/backend/queryview/drivers/test_contract.py +77 -0
- queryview-0.0.2/backend/queryview/drivers/test_duckdb.py +80 -0
- queryview-0.0.2/backend/queryview/drivers/test_postgres.py +80 -0
- queryview-0.0.2/backend/queryview/gitsync.py +374 -0
- queryview-0.0.2/backend/queryview/main.py +740 -0
- queryview-0.0.2/backend/queryview/mcp_server.py +294 -0
- queryview-0.0.2/backend/queryview/migrations/env.py +39 -0
- queryview-0.0.2/backend/queryview/migrations/script.py.mako +29 -0
- queryview-0.0.2/backend/queryview/migrations/versions/9a536b7c0328_initial_schema.py +89 -0
- queryview-0.0.2/backend/queryview/migrations/versions/a1b2c3d4e5f6_connection_config_blob.py +59 -0
- queryview-0.0.2/backend/queryview/migrations/versions/b2c3d4e5f6a7_predefined_presentation.py +32 -0
- queryview-0.0.2/backend/queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py +98 -0
- queryview-0.0.2/backend/queryview/queries.py +159 -0
- queryview-0.0.2/backend/queryview/remote.py +141 -0
- queryview-0.0.2/backend/queryview/static/assets/index-CvnC_D68.js +47 -0
- queryview-0.0.2/backend/queryview/static/assets/index-Qe7bhycG.css +2 -0
- queryview-0.0.2/backend/queryview/static/favicon.svg +1 -0
- queryview-0.0.2/backend/queryview/static/index.html +14 -0
- queryview-0.0.2/backend/queryview/test_api_db.py +51 -0
- queryview-0.0.2/backend/queryview/test_api_export_import.py +85 -0
- queryview-0.0.2/backend/queryview/test_api_gitsync.py +83 -0
- queryview-0.0.2/backend/queryview/test_api_workspaces.py +44 -0
- queryview-0.0.2/backend/queryview/test_connect_flow.py +123 -0
- queryview-0.0.2/backend/queryview/test_connect_store.py +34 -0
- queryview-0.0.2/backend/queryview/test_dashboards.py +216 -0
- queryview-0.0.2/backend/queryview/test_gitsync.py +346 -0
- queryview-0.0.2/backend/queryview/test_main.py +18 -0
- queryview-0.0.2/backend/queryview/test_mcp_gitsync.py +72 -0
- queryview-0.0.2/backend/queryview/test_migrations.py +99 -0
- queryview-0.0.2/backend/queryview/test_queries.py +170 -0
- queryview-0.0.2/backend/queryview/test_remote.py +260 -0
- queryview-0.0.2/backend/queryview/test_validation.py +87 -0
- queryview-0.0.2/backend/queryview/test_workspaces.py +109 -0
- queryview-0.0.2/backend/queryview/test_yamlio.py +198 -0
- queryview-0.0.2/backend/queryview/validation.py +111 -0
- queryview-0.0.2/backend/queryview/workspaces.py +167 -0
- queryview-0.0.2/backend/queryview/yamlio.py +245 -0
- queryview-0.0.2/docker/Dockerfile +32 -0
- queryview-0.0.2/docs/api.md +96 -0
- queryview-0.0.2/docs/connect.md +208 -0
- queryview-0.0.2/docs/dashboard.md +107 -0
- queryview-0.0.2/docs/explorer.md +74 -0
- queryview-0.0.2/docs/export-import.md +80 -0
- queryview-0.0.2/docs/future.md +43 -0
- queryview-0.0.2/docs/gitsync.md +67 -0
- queryview-0.0.2/docs/query.md +296 -0
- queryview-0.0.2/docs/queryview.md +78 -0
- queryview-0.0.2/docs/remote.md +72 -0
- queryview-0.0.2/docs/workspace.md +54 -0
- queryview-0.0.2/e2e/conftest.py +162 -0
- queryview-0.0.2/e2e/test_app.py +47 -0
- queryview-0.0.2/e2e/test_dashboard.py +110 -0
- queryview-0.0.2/e2e/test_drivers.py +141 -0
- queryview-0.0.2/e2e/test_explorer.py +66 -0
- queryview-0.0.2/e2e/test_export_import.py +48 -0
- queryview-0.0.2/e2e/test_gitsync.py +62 -0
- queryview-0.0.2/e2e/test_query.py +394 -0
- queryview-0.0.2/e2e/test_remote.py +65 -0
- queryview-0.0.2/e2e/test_workspaces.py +44 -0
- queryview-0.0.2/frontend/.gitignore +24 -0
- queryview-0.0.2/frontend/README.md +73 -0
- queryview-0.0.2/frontend/eslint.config.js +22 -0
- queryview-0.0.2/frontend/index.html +13 -0
- queryview-0.0.2/frontend/package.json +37 -0
- queryview-0.0.2/frontend/public/favicon.svg +1 -0
- queryview-0.0.2/frontend/src/App.tsx +350 -0
- queryview-0.0.2/frontend/src/CellViewModal.tsx +62 -0
- queryview-0.0.2/frontend/src/ComplexCell.tsx +50 -0
- queryview-0.0.2/frontend/src/DashboardView.tsx +281 -0
- queryview-0.0.2/frontend/src/ExplorerView.tsx +328 -0
- queryview-0.0.2/frontend/src/ExportImportControls.tsx +91 -0
- queryview-0.0.2/frontend/src/FieldPickers.tsx +153 -0
- queryview-0.0.2/frontend/src/GitSyncControls.tsx +0 -0
- queryview-0.0.2/frontend/src/QueryView.tsx +1439 -0
- queryview-0.0.2/frontend/src/ResultsTable.tsx +54 -0
- queryview-0.0.2/frontend/src/Toast.tsx +19 -0
- queryview-0.0.2/frontend/src/WorkspaceSwitcher.tsx +211 -0
- queryview-0.0.2/frontend/src/cellView.test.ts +104 -0
- queryview-0.0.2/frontend/src/cellView.ts +38 -0
- queryview-0.0.2/frontend/src/compactNumber.test.ts +43 -0
- queryview-0.0.2/frontend/src/compactNumber.ts +27 -0
- queryview-0.0.2/frontend/src/complexCellParsing.test.ts +221 -0
- queryview-0.0.2/frontend/src/complexCellParsing.ts +177 -0
- queryview-0.0.2/frontend/src/drivers.ts +64 -0
- queryview-0.0.2/frontend/src/gitsync.test.ts +20 -0
- queryview-0.0.2/frontend/src/gitsync.ts +99 -0
- queryview-0.0.2/frontend/src/index.css +166 -0
- queryview-0.0.2/frontend/src/main.tsx +10 -0
- queryview-0.0.2/frontend/src/presentation.test.ts +14 -0
- queryview-0.0.2/frontend/src/presentation.ts +28 -0
- queryview-0.0.2/frontend/src/promptSuggestions.test.ts +100 -0
- queryview-0.0.2/frontend/src/promptSuggestions.ts +70 -0
- queryview-0.0.2/frontend/src/queryParams.test.ts +93 -0
- queryview-0.0.2/frontend/src/queryParams.ts +79 -0
- queryview-0.0.2/frontend/src/sessionLock.test.ts +30 -0
- queryview-0.0.2/frontend/src/sessionLock.ts +17 -0
- queryview-0.0.2/frontend/src/tsv.ts +8 -0
- queryview-0.0.2/frontend/src/workspace.test.ts +30 -0
- queryview-0.0.2/frontend/src/workspace.ts +63 -0
- queryview-0.0.2/frontend/src/yamlio.test.ts +29 -0
- queryview-0.0.2/frontend/src/yamlio.ts +76 -0
- queryview-0.0.2/frontend/tsconfig.app.json +25 -0
- queryview-0.0.2/frontend/tsconfig.json +7 -0
- queryview-0.0.2/frontend/tsconfig.node.json +24 -0
- queryview-0.0.2/frontend/vite.config.ts +17 -0
- queryview-0.0.2/package-lock.json +3830 -0
- queryview-0.0.2/package.json +22 -0
- queryview-0.0.2/pyproject.toml +83 -0
- queryview-0.0.2/pyrightconfig.json +13 -0
- queryview-0.0.2/scripts/dev.sh +27 -0
- queryview-0.0.2/scripts/setup.sh +19 -0
- queryview-0.0.2/scripts/setup_browser.sh +78 -0
- queryview-0.0.2/scripts/setup_clickhouse.sh +72 -0
- queryview-0.0.2/scripts/setup_postgres.sh +102 -0
- queryview-0.0.2/uv.lock +1536 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npx vitest *)",
|
|
5
|
+
"Bash(npx tsc *)",
|
|
6
|
+
"Bash(npx eslint *)",
|
|
7
|
+
"mcp__plugin_devpowers_github__pull_request_read",
|
|
8
|
+
"mcp__plugin_devpowers_github__list_pull_requests",
|
|
9
|
+
"mcp__plugin_devpowers_github__get_job_logs"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"extraKnownMarketplaces": {
|
|
13
|
+
"devpowers": {
|
|
14
|
+
"source": {
|
|
15
|
+
"source": "github",
|
|
16
|
+
"repo": "kolodkin/devpowers"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"enabledPlugins": {
|
|
21
|
+
"devpowers@devpowers": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Start git daemon
|
|
2
|
+
description: >-
|
|
3
|
+
Start a loopback `git daemon` that serves — and accepts pushes into — bare
|
|
4
|
+
repos under a base path, so e2e tests exercise git sync against a real
|
|
5
|
+
git:// remote with no auth. Creates an empty bare repo and exports
|
|
6
|
+
GIT_SYNC_REMOTE pointing at it; `--detach` keeps the daemon alive across
|
|
7
|
+
steps.
|
|
8
|
+
|
|
9
|
+
runs:
|
|
10
|
+
using: composite
|
|
11
|
+
steps:
|
|
12
|
+
- name: Start git daemon
|
|
13
|
+
shell: bash
|
|
14
|
+
run: |
|
|
15
|
+
GIT_DAEMON_BASE="$RUNNER_TEMP/gitsrv"
|
|
16
|
+
mkdir -p "$GIT_DAEMON_BASE"
|
|
17
|
+
git init --bare -b main "$GIT_DAEMON_BASE/queryview.git"
|
|
18
|
+
echo "GIT_SYNC_REMOTE=git://127.0.0.1:9418/queryview.git" >> "$GITHUB_ENV"
|
|
19
|
+
git daemon --reuseaddr --listen=127.0.0.1 --port=9418 \
|
|
20
|
+
--base-path="$GIT_DAEMON_BASE" --export-all \
|
|
21
|
+
--enable=receive-pack --detach
|
|
22
|
+
for i in $(seq 1 30); do
|
|
23
|
+
if (exec 3<>/dev/tcp/127.0.0.1/9418) 2>/dev/null; then
|
|
24
|
+
exec 3>&-
|
|
25
|
+
echo "git daemon is ready"
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
sleep 1
|
|
29
|
+
done
|
|
30
|
+
echo "git daemon failed to become ready" >&2
|
|
31
|
+
exit 1
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
FORCE_JAVASCRIPT_ACTIONS_RUNNER_NODE_VERSION: node24
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lint:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 15
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup uv
|
|
22
|
+
uses: astral-sh/setup-uv@v5
|
|
23
|
+
|
|
24
|
+
# pre-commit's pyright hook resolves imports from the project's .venv
|
|
25
|
+
# (see pyrightconfig.json), so sync it first.
|
|
26
|
+
- name: Install backend + test dependencies
|
|
27
|
+
run: uv sync --frozen --group test
|
|
28
|
+
|
|
29
|
+
- name: Run pre-commit
|
|
30
|
+
uses: pre-commit/action@v3.0.1
|
|
31
|
+
|
|
32
|
+
test-backend:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
timeout-minutes: 15
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
|
|
38
|
+
- name: Setup uv
|
|
39
|
+
uses: astral-sh/setup-uv@v5
|
|
40
|
+
|
|
41
|
+
- name: Install backend + test dependencies
|
|
42
|
+
run: uv sync --frozen --group test
|
|
43
|
+
|
|
44
|
+
- name: Run backend unit tests
|
|
45
|
+
run: uv run --frozen pytest backend/queryview
|
|
46
|
+
|
|
47
|
+
test-e2e:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
environment: CICD
|
|
50
|
+
permissions:
|
|
51
|
+
contents: write
|
|
52
|
+
checks: write
|
|
53
|
+
services:
|
|
54
|
+
clickhouse:
|
|
55
|
+
image: clickhouse/clickhouse-server:24
|
|
56
|
+
ports:
|
|
57
|
+
- 8123:8123
|
|
58
|
+
# Probe the HTTP interface (8123) — what the backend actually uses —
|
|
59
|
+
# so the readiness gate matches the app.
|
|
60
|
+
options: >-
|
|
61
|
+
--health-cmd "wget --spider -q localhost:8123/ping"
|
|
62
|
+
--health-interval 10s
|
|
63
|
+
--health-timeout 5s
|
|
64
|
+
--health-retries 5
|
|
65
|
+
--add-host=host.docker.internal:host-gateway
|
|
66
|
+
postgres:
|
|
67
|
+
image: postgres:16
|
|
68
|
+
env:
|
|
69
|
+
POSTGRES_PASSWORD: postgres
|
|
70
|
+
POSTGRES_HOST_AUTH_METHOD: trust
|
|
71
|
+
ports:
|
|
72
|
+
- 5432:5432
|
|
73
|
+
options: >-
|
|
74
|
+
--health-cmd "pg_isready -U postgres"
|
|
75
|
+
--health-interval 10s
|
|
76
|
+
--health-timeout 5s
|
|
77
|
+
--health-retries 5
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@v4
|
|
80
|
+
|
|
81
|
+
- name: Setup Node
|
|
82
|
+
uses: actions/setup-node@v4
|
|
83
|
+
with:
|
|
84
|
+
node-version: 22
|
|
85
|
+
cache: npm
|
|
86
|
+
cache-dependency-path: package-lock.json
|
|
87
|
+
|
|
88
|
+
- name: Setup uv
|
|
89
|
+
uses: astral-sh/setup-uv@v5
|
|
90
|
+
|
|
91
|
+
- name: Install backend + test dependencies
|
|
92
|
+
run: uv sync --frozen --group test
|
|
93
|
+
|
|
94
|
+
- name: Install frontend dependencies
|
|
95
|
+
run: npm ci
|
|
96
|
+
|
|
97
|
+
- name: Install Playwright browser
|
|
98
|
+
run: uv run --frozen --group test playwright install --with-deps chromium
|
|
99
|
+
|
|
100
|
+
- name: Build frontend
|
|
101
|
+
run: npm run build -w frontend
|
|
102
|
+
|
|
103
|
+
- name: Start git daemon
|
|
104
|
+
uses: ./.github/actions/start-git-daemon
|
|
105
|
+
|
|
106
|
+
- name: Start backend (serving built SPA)
|
|
107
|
+
run: |
|
|
108
|
+
SERVE_STATIC=1 uv run --frozen queryview-backend > backend.log 2>&1 &
|
|
109
|
+
echo $! > backend.pid
|
|
110
|
+
for i in $(seq 1 30); do
|
|
111
|
+
if curl -sf http://localhost:8000/api/health > /dev/null; then
|
|
112
|
+
echo "Backend is up"; exit 0
|
|
113
|
+
fi
|
|
114
|
+
sleep 1
|
|
115
|
+
done
|
|
116
|
+
echo "Backend failed to start"; cat backend.log; exit 1
|
|
117
|
+
|
|
118
|
+
- name: Run Playwright e2e tests
|
|
119
|
+
id: e2e
|
|
120
|
+
env:
|
|
121
|
+
BASE_URL: http://localhost:8000
|
|
122
|
+
run: |
|
|
123
|
+
uv run --frozen --group test pytest e2e \
|
|
124
|
+
--tracing retain-on-failure \
|
|
125
|
+
--output test-results \
|
|
126
|
+
--html=report/index.html --self-contained-html
|
|
127
|
+
|
|
128
|
+
- name: Stop backend
|
|
129
|
+
if: always()
|
|
130
|
+
run: |
|
|
131
|
+
if [ -f backend.pid ]; then
|
|
132
|
+
kill "$(cat backend.pid)" 2>/dev/null || true
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
- name: Upload trace artifacts
|
|
136
|
+
if: ${{ !cancelled() }}
|
|
137
|
+
uses: actions/upload-artifact@v4
|
|
138
|
+
with:
|
|
139
|
+
name: playwright-traces
|
|
140
|
+
path: test-results/
|
|
141
|
+
retention-days: 1
|
|
142
|
+
if-no-files-found: ignore
|
|
143
|
+
|
|
144
|
+
- name: Check if report exists
|
|
145
|
+
id: check-report
|
|
146
|
+
if: always()
|
|
147
|
+
run: |
|
|
148
|
+
if [ -f "report/index.html" ]; then
|
|
149
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
150
|
+
echo "E2E report is present"
|
|
151
|
+
else
|
|
152
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
153
|
+
echo "E2E report missing"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
- name: Deploy E2E Report
|
|
157
|
+
if: always() && steps.check-report.outputs.exists == 'true'
|
|
158
|
+
# Publishing to the external artifact-view repo needs DEPLOY_KEY; don't
|
|
159
|
+
# fail the PR check when that publish can't run — the step still surfaces
|
|
160
|
+
# the error as a warning annotation.
|
|
161
|
+
continue-on-error: true
|
|
162
|
+
uses: peaceiris/actions-gh-pages@v4
|
|
163
|
+
with:
|
|
164
|
+
deploy_key: ${{ secrets.DEPLOY_KEY }}
|
|
165
|
+
external_repository: kolodkin/artifact-view
|
|
166
|
+
publish_dir: ./report
|
|
167
|
+
destination_dir: queryview-/e2e-report/${{ github.run_number }}
|
|
168
|
+
publish_branch: gh-pages
|
|
169
|
+
keep_files: true
|
|
170
|
+
|
|
171
|
+
- name: Create E2E Report check
|
|
172
|
+
if: always()
|
|
173
|
+
uses: LouisBrunner/checks-action@v2.0.0
|
|
174
|
+
env:
|
|
175
|
+
REPORT_URL: https://${{ github.repository_owner }}.github.io/artifact-view/queryview-/e2e-report/${{ github.run_number }}
|
|
176
|
+
with:
|
|
177
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
178
|
+
name: E2E Report
|
|
179
|
+
conclusion: ${{ steps.e2e.outcome }}
|
|
180
|
+
details_url: ${{ env.REPORT_URL }}
|
|
181
|
+
output: |
|
|
182
|
+
{"title":"${{ steps.e2e.outcome == 'success' && 'Tests passed' || 'Tests failed' }}","summary":"**[View Full E2E Report](${{ env.REPORT_URL }})**"}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Simpler analog of aaiclick's publish.yaml: build the SPA into the wheel,
|
|
4
|
+
# smoke-test the installed package, then publish via PyPI trusted publishing
|
|
5
|
+
# and cut a GitHub release. Like aaiclick, a multi-arch container image is
|
|
6
|
+
# built from the release wheel, pushed to GHCR as an rc tag, smoke-tested,
|
|
7
|
+
# and promoted (same digests) to vX.Y.Z / latest after publish. Repeated
|
|
8
|
+
# steps are YAML-anchored at first use and aliased below.
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
inputs:
|
|
13
|
+
tag:
|
|
14
|
+
description: "Release tag (must match vX.Y.Z)"
|
|
15
|
+
required: true
|
|
16
|
+
type: string
|
|
17
|
+
pre-release:
|
|
18
|
+
description: "Mark as pre-release on GitHub"
|
|
19
|
+
required: false
|
|
20
|
+
type: boolean
|
|
21
|
+
default: false
|
|
22
|
+
skip-e2e:
|
|
23
|
+
description: "Skip the Playwright e2e release gate (emergency only)"
|
|
24
|
+
required: false
|
|
25
|
+
type: boolean
|
|
26
|
+
default: false
|
|
27
|
+
|
|
28
|
+
env:
|
|
29
|
+
FORCE_JAVASCRIPT_ACTIONS_RUNNER_NODE_VERSION: node24
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
build:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- name: Validate tag format
|
|
36
|
+
run: |
|
|
37
|
+
if ! echo "${{ inputs.tag }}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
38
|
+
echo "Error: tag '${{ inputs.tag }}' does not match vX.Y.Z"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
- name: Checkout code
|
|
43
|
+
uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
- name: Setup uv
|
|
46
|
+
uses: astral-sh/setup-uv@v5
|
|
47
|
+
|
|
48
|
+
- name: Setup Node
|
|
49
|
+
uses: actions/setup-node@v4
|
|
50
|
+
with:
|
|
51
|
+
node-version: 22
|
|
52
|
+
cache: npm
|
|
53
|
+
cache-dependency-path: package-lock.json
|
|
54
|
+
|
|
55
|
+
- name: Build SPA bundle
|
|
56
|
+
run: |
|
|
57
|
+
npm ci
|
|
58
|
+
npm run build -w frontend
|
|
59
|
+
test -f frontend/dist/index.html \
|
|
60
|
+
|| { echo "Vite build did not produce frontend/dist/index.html"; exit 1; }
|
|
61
|
+
|
|
62
|
+
- name: Build package (SPA bundled as queryview/static)
|
|
63
|
+
run: |
|
|
64
|
+
cp -r frontend/dist backend/queryview/static
|
|
65
|
+
VERSION="${{ inputs.tag }}"
|
|
66
|
+
SETUPTOOLS_SCM_PRETEND_VERSION="${VERSION#v}" uv build
|
|
67
|
+
|
|
68
|
+
- name: Verify wheel contains SPA bundle
|
|
69
|
+
run: |
|
|
70
|
+
wheel=$(ls dist/queryview-*.whl | head -n 1)
|
|
71
|
+
unzip -l "$wheel" | grep -q "queryview/static/index.html" \
|
|
72
|
+
|| { echo "Built wheel $wheel does not contain queryview/static/index.html"; exit 1; }
|
|
73
|
+
|
|
74
|
+
- name: Upload dist artifacts
|
|
75
|
+
uses: actions/upload-artifact@v4
|
|
76
|
+
with:
|
|
77
|
+
name: dist
|
|
78
|
+
path: dist/
|
|
79
|
+
|
|
80
|
+
- name: Export pinned test dependencies
|
|
81
|
+
run: uv export --frozen --group test --no-emit-project -o requirements-test.txt
|
|
82
|
+
|
|
83
|
+
- name: Upload requirements artifact
|
|
84
|
+
uses: actions/upload-artifact@v4
|
|
85
|
+
with:
|
|
86
|
+
name: requirements
|
|
87
|
+
path: requirements-test.txt
|
|
88
|
+
|
|
89
|
+
build-rc-image:
|
|
90
|
+
needs: build
|
|
91
|
+
runs-on: ${{ matrix.runner }}
|
|
92
|
+
timeout-minutes: 30
|
|
93
|
+
permissions:
|
|
94
|
+
contents: read
|
|
95
|
+
packages: write
|
|
96
|
+
strategy:
|
|
97
|
+
fail-fast: true
|
|
98
|
+
matrix:
|
|
99
|
+
include:
|
|
100
|
+
- platform: amd64
|
|
101
|
+
runner: ubuntu-latest
|
|
102
|
+
- platform: arm64
|
|
103
|
+
runner: ubuntu-24.04-arm
|
|
104
|
+
env:
|
|
105
|
+
IMAGE: ghcr.io/kolodkin/queryview
|
|
106
|
+
steps:
|
|
107
|
+
- name: Checkout code
|
|
108
|
+
uses: actions/checkout@v4
|
|
109
|
+
|
|
110
|
+
- name: Download dist artifacts
|
|
111
|
+
uses: actions/download-artifact@v4
|
|
112
|
+
with:
|
|
113
|
+
name: dist
|
|
114
|
+
path: dist/
|
|
115
|
+
|
|
116
|
+
# The image pip-installs queryview==<version>. The wheel is not on PyPI
|
|
117
|
+
# yet (publish is gated on this very image), so serve the artifact from
|
|
118
|
+
# a local pypiserver; it redirects unknown packages to pypi.org, so
|
|
119
|
+
# dependencies resolve through the same index URL.
|
|
120
|
+
- name: Serve wheel via local pypiserver
|
|
121
|
+
run: |
|
|
122
|
+
docker run -d --name pypiserver -p 8080:8080 \
|
|
123
|
+
pypiserver/pypiserver:v2.3.2 run -p 8080 -a . -P . --server gunicorn
|
|
124
|
+
for i in $(seq 1 30); do
|
|
125
|
+
curl -fsS http://localhost:8080/simple/ >/dev/null 2>&1 && break
|
|
126
|
+
if [ "$i" -eq 30 ]; then
|
|
127
|
+
echo "pypiserver did not come up in time" >&2
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
sleep 2
|
|
131
|
+
done
|
|
132
|
+
for f in dist/*.whl dist/*.tar.gz; do
|
|
133
|
+
curl -fsSL -F "content=@${f}" -F ":action=file_upload" http://localhost:8080/
|
|
134
|
+
done
|
|
135
|
+
|
|
136
|
+
- &ghcr-login
|
|
137
|
+
name: Log in to GHCR
|
|
138
|
+
uses: docker/login-action@v3
|
|
139
|
+
with:
|
|
140
|
+
registry: ghcr.io
|
|
141
|
+
username: ${{ github.actor }}
|
|
142
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
143
|
+
|
|
144
|
+
# Native per-arch builds (no QEMU). --network=host lets the build reach
|
|
145
|
+
# the pypiserver on localhost; pip trusts localhost for plain HTTP.
|
|
146
|
+
- name: Build & push per-arch rc image
|
|
147
|
+
run: |
|
|
148
|
+
VERSION="${{ inputs.tag }}"
|
|
149
|
+
PEP440="${VERSION#v}"
|
|
150
|
+
ARCH_TAG="${VERSION}-rc-${{ matrix.platform }}"
|
|
151
|
+
docker build --network=host -f docker/Dockerfile \
|
|
152
|
+
--build-arg QUERYVIEW_VERSION="${PEP440}" \
|
|
153
|
+
--build-arg PIP_INDEX_URL="http://localhost:8080/simple/" \
|
|
154
|
+
-t "${IMAGE}:${ARCH_TAG}" .
|
|
155
|
+
docker push "${IMAGE}:${ARCH_TAG}"
|
|
156
|
+
|
|
157
|
+
merge-rc-image:
|
|
158
|
+
needs: build-rc-image
|
|
159
|
+
runs-on: ubuntu-latest
|
|
160
|
+
permissions:
|
|
161
|
+
packages: write
|
|
162
|
+
steps:
|
|
163
|
+
- *ghcr-login
|
|
164
|
+
|
|
165
|
+
- name: Assemble multi-arch rc manifest
|
|
166
|
+
run: |
|
|
167
|
+
REF="ghcr.io/kolodkin/queryview"
|
|
168
|
+
TAG="${{ inputs.tag }}-rc"
|
|
169
|
+
docker buildx imagetools create -t "${REF}:${TAG}" \
|
|
170
|
+
"${REF}:${TAG}-amd64" "${REF}:${TAG}-arm64"
|
|
171
|
+
|
|
172
|
+
# Run the exact rc image that will be promoted and verify the containerized
|
|
173
|
+
# server comes up: /api/health, the bundled SPA at /, and the index.html
|
|
174
|
+
# fallback for client-side routes.
|
|
175
|
+
test-image:
|
|
176
|
+
needs: merge-rc-image
|
|
177
|
+
runs-on: ubuntu-latest
|
|
178
|
+
permissions:
|
|
179
|
+
packages: read
|
|
180
|
+
steps:
|
|
181
|
+
- *ghcr-login
|
|
182
|
+
|
|
183
|
+
- name: Smoke-test rc image
|
|
184
|
+
run: |
|
|
185
|
+
docker run -d --name queryview -p 8000:8000 \
|
|
186
|
+
"ghcr.io/kolodkin/queryview:${{ inputs.tag }}-rc"
|
|
187
|
+
for i in $(seq 1 30); do
|
|
188
|
+
curl -sf http://localhost:8000/api/health > /dev/null && break
|
|
189
|
+
if [ "$i" -eq 30 ]; then
|
|
190
|
+
echo "Server failed to start"; docker logs queryview; exit 1
|
|
191
|
+
fi
|
|
192
|
+
sleep 1
|
|
193
|
+
done
|
|
194
|
+
curl -sf http://localhost:8000/ | grep -q "<!doctype html>" \
|
|
195
|
+
|| { echo "/ did not serve the bundled SPA"; docker logs queryview; exit 1; }
|
|
196
|
+
test "$(curl -sf -o /dev/null -w '%{http_code}' http://localhost:8000/some/client/route)" = "200" \
|
|
197
|
+
|| { echo "SPA fallback route did not return 200"; docker logs queryview; exit 1; }
|
|
198
|
+
docker rm -f queryview
|
|
199
|
+
|
|
200
|
+
# Install the built wheel into a clean venv (no repo checkout) and verify the
|
|
201
|
+
# packaged server comes up: /api/health, the bundled SPA at /, and the
|
|
202
|
+
# index.html fallback for client-side routes.
|
|
203
|
+
test-package:
|
|
204
|
+
needs: build
|
|
205
|
+
runs-on: ubuntu-latest
|
|
206
|
+
steps:
|
|
207
|
+
- name: Setup uv
|
|
208
|
+
uses: astral-sh/setup-uv@v5
|
|
209
|
+
|
|
210
|
+
- name: Download dist artifacts
|
|
211
|
+
uses: actions/download-artifact@v4
|
|
212
|
+
with:
|
|
213
|
+
name: dist
|
|
214
|
+
path: dist/
|
|
215
|
+
|
|
216
|
+
- name: Install built wheel
|
|
217
|
+
run: |
|
|
218
|
+
uv venv --python 3.11
|
|
219
|
+
uv pip install dist/queryview-*.whl
|
|
220
|
+
|
|
221
|
+
- name: Smoke-test installed package
|
|
222
|
+
working-directory: ${{ runner.temp }}
|
|
223
|
+
run: |
|
|
224
|
+
DB_PATH="$PWD/queryview.db" "$GITHUB_WORKSPACE/.venv/bin/queryview" > server.log 2>&1 &
|
|
225
|
+
pid=$!
|
|
226
|
+
for i in $(seq 1 30); do
|
|
227
|
+
curl -sf http://localhost:8000/api/health > /dev/null && break
|
|
228
|
+
if [ "$i" -eq 30 ]; then
|
|
229
|
+
echo "Server failed to start"; cat server.log; exit 1
|
|
230
|
+
fi
|
|
231
|
+
sleep 1
|
|
232
|
+
done
|
|
233
|
+
curl -sf http://localhost:8000/ | grep -q "<!doctype html>" \
|
|
234
|
+
|| { echo "/ did not serve the bundled SPA"; cat server.log; exit 1; }
|
|
235
|
+
test "$(curl -sf -o /dev/null -w '%{http_code}' http://localhost:8000/some/client/route)" = "200" \
|
|
236
|
+
|| { echo "SPA fallback route did not return 200"; cat server.log; exit 1; }
|
|
237
|
+
kill "$pid"
|
|
238
|
+
|
|
239
|
+
# Run the packaged backend test suite against the installed wheel. No
|
|
240
|
+
# checkout — the tests ship inside the queryview package, so this exercises
|
|
241
|
+
# exactly the bits users install.
|
|
242
|
+
test-package-pytest:
|
|
243
|
+
needs: build
|
|
244
|
+
runs-on: ubuntu-latest
|
|
245
|
+
steps:
|
|
246
|
+
- name: Setup uv
|
|
247
|
+
uses: astral-sh/setup-uv@v5
|
|
248
|
+
|
|
249
|
+
- name: Create virtual environment
|
|
250
|
+
run: uv venv --python 3.11
|
|
251
|
+
|
|
252
|
+
- name: Download dist artifacts
|
|
253
|
+
uses: actions/download-artifact@v4
|
|
254
|
+
with:
|
|
255
|
+
name: dist
|
|
256
|
+
path: dist/
|
|
257
|
+
|
|
258
|
+
- name: Download requirements artifact
|
|
259
|
+
uses: actions/download-artifact@v4
|
|
260
|
+
with:
|
|
261
|
+
name: requirements
|
|
262
|
+
|
|
263
|
+
- name: Install pinned dependencies and built wheel
|
|
264
|
+
run: |
|
|
265
|
+
uv pip install -r requirements-test.txt
|
|
266
|
+
uv pip install --no-deps --no-index --find-links dist/ queryview
|
|
267
|
+
|
|
268
|
+
- name: Run packaged tests against the installed wheel
|
|
269
|
+
working-directory: ${{ runner.temp }}
|
|
270
|
+
env:
|
|
271
|
+
VIRTUAL_ENV: ${{ github.workspace }}/.venv
|
|
272
|
+
run: uv run --no-project pytest --pyargs queryview
|
|
273
|
+
|
|
274
|
+
# Playwright e2e against the installed wheel: server and SPA are the exact
|
|
275
|
+
# bytes being published (no Node/frontend build — the wheel ships the SPA).
|
|
276
|
+
# The checkout supplies only e2e/, uv.lock, and the git-daemon action.
|
|
277
|
+
# Runs after the packaged pytest gate so the expensive leg starts only once
|
|
278
|
+
# the cheap one passes.
|
|
279
|
+
test-package-e2e:
|
|
280
|
+
needs: [build, test-package-pytest]
|
|
281
|
+
if: ${{ !inputs.skip-e2e }}
|
|
282
|
+
runs-on: ubuntu-latest
|
|
283
|
+
services:
|
|
284
|
+
clickhouse:
|
|
285
|
+
image: clickhouse/clickhouse-server:24
|
|
286
|
+
ports:
|
|
287
|
+
- 8123:8123
|
|
288
|
+
# Probe the HTTP interface (8123) — what the backend actually uses —
|
|
289
|
+
# so the readiness gate matches the app.
|
|
290
|
+
options: >-
|
|
291
|
+
--health-cmd "wget --spider -q localhost:8123/ping"
|
|
292
|
+
--health-interval 10s
|
|
293
|
+
--health-timeout 5s
|
|
294
|
+
--health-retries 5
|
|
295
|
+
--add-host=host.docker.internal:host-gateway
|
|
296
|
+
postgres:
|
|
297
|
+
image: postgres:16
|
|
298
|
+
env:
|
|
299
|
+
POSTGRES_PASSWORD: postgres
|
|
300
|
+
POSTGRES_HOST_AUTH_METHOD: trust
|
|
301
|
+
ports:
|
|
302
|
+
- 5432:5432
|
|
303
|
+
options: >-
|
|
304
|
+
--health-cmd "pg_isready -U postgres"
|
|
305
|
+
--health-interval 10s
|
|
306
|
+
--health-timeout 5s
|
|
307
|
+
--health-retries 5
|
|
308
|
+
steps:
|
|
309
|
+
- uses: actions/checkout@v4
|
|
310
|
+
|
|
311
|
+
- name: Setup uv
|
|
312
|
+
uses: astral-sh/setup-uv@v5
|
|
313
|
+
|
|
314
|
+
- name: Install test dependencies (project not installed)
|
|
315
|
+
run: uv sync --frozen --group test --no-install-project
|
|
316
|
+
|
|
317
|
+
- name: Download dist artifacts
|
|
318
|
+
uses: actions/download-artifact@v4
|
|
319
|
+
with:
|
|
320
|
+
name: dist
|
|
321
|
+
path: dist/
|
|
322
|
+
|
|
323
|
+
- name: Install built wheel
|
|
324
|
+
run: uv pip install --no-deps dist/queryview-*.whl
|
|
325
|
+
|
|
326
|
+
- name: Install Playwright browser
|
|
327
|
+
run: uv run --no-sync playwright install --with-deps chromium
|
|
328
|
+
|
|
329
|
+
- name: Start git daemon
|
|
330
|
+
uses: ./.github/actions/start-git-daemon
|
|
331
|
+
|
|
332
|
+
- name: Start backend from the installed wheel
|
|
333
|
+
run: |
|
|
334
|
+
SERVE_STATIC=1 .venv/bin/queryview > backend.log 2>&1 &
|
|
335
|
+
echo $! > backend.pid
|
|
336
|
+
for i in $(seq 1 30); do
|
|
337
|
+
if curl -sf http://localhost:8000/api/health > /dev/null; then
|
|
338
|
+
echo "Backend is up"; exit 0
|
|
339
|
+
fi
|
|
340
|
+
sleep 1
|
|
341
|
+
done
|
|
342
|
+
echo "Backend failed to start"; cat backend.log; exit 1
|
|
343
|
+
|
|
344
|
+
- name: Run Playwright e2e tests
|
|
345
|
+
env:
|
|
346
|
+
BASE_URL: http://localhost:8000
|
|
347
|
+
run: |
|
|
348
|
+
uv run --no-sync pytest e2e \
|
|
349
|
+
--tracing retain-on-failure \
|
|
350
|
+
--output test-results
|
|
351
|
+
|
|
352
|
+
- name: Stop backend
|
|
353
|
+
if: always()
|
|
354
|
+
run: |
|
|
355
|
+
if [ -f backend.pid ]; then
|
|
356
|
+
kill "$(cat backend.pid)" 2>/dev/null || true
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
- name: Upload trace artifacts
|
|
360
|
+
if: ${{ !cancelled() }}
|
|
361
|
+
uses: actions/upload-artifact@v4
|
|
362
|
+
with:
|
|
363
|
+
name: playwright-traces-release
|
|
364
|
+
path: test-results/
|
|
365
|
+
retention-days: 1
|
|
366
|
+
if-no-files-found: ignore
|
|
367
|
+
|
|
368
|
+
publish:
|
|
369
|
+
needs: [test-package, test-package-pytest, test-package-e2e, test-image]
|
|
370
|
+
if: >-
|
|
371
|
+
always()
|
|
372
|
+
&& needs.test-package.result == 'success'
|
|
373
|
+
&& needs.test-package-pytest.result == 'success'
|
|
374
|
+
&& needs.test-image.result == 'success'
|
|
375
|
+
&& (needs.test-package-e2e.result == 'success'
|
|
376
|
+
|| needs.test-package-e2e.result == 'skipped')
|
|
377
|
+
runs-on: ubuntu-latest
|
|
378
|
+
permissions:
|
|
379
|
+
contents: write
|
|
380
|
+
id-token: write
|
|
381
|
+
steps:
|
|
382
|
+
- name: Checkout code
|
|
383
|
+
uses: actions/checkout@v4
|
|
384
|
+
|
|
385
|
+
- name: Download dist artifacts
|
|
386
|
+
uses: actions/download-artifact@v4
|
|
387
|
+
with:
|
|
388
|
+
name: dist
|
|
389
|
+
path: dist/
|
|
390
|
+
|
|
391
|
+
- name: Publish to PyPI
|
|
392
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
393
|
+
|
|
394
|
+
- name: Create and push tag
|
|
395
|
+
run: |
|
|
396
|
+
git config user.name "github-actions[bot]"
|
|
397
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
398
|
+
git tag ${{ inputs.tag }}
|
|
399
|
+
git push origin ${{ inputs.tag }}
|
|
400
|
+
|
|
401
|
+
- name: Create GitHub release
|
|
402
|
+
env:
|
|
403
|
+
GH_TOKEN: ${{ github.token }}
|
|
404
|
+
run: |
|
|
405
|
+
gh release create ${{ inputs.tag }} dist/* \
|
|
406
|
+
--title "${{ inputs.tag }}" \
|
|
407
|
+
--generate-notes \
|
|
408
|
+
${{ inputs.pre-release && '--prerelease' || '' }}
|
|
409
|
+
|
|
410
|
+
promote-image:
|
|
411
|
+
needs: [publish]
|
|
412
|
+
runs-on: ubuntu-latest
|
|
413
|
+
permissions:
|
|
414
|
+
packages: write
|
|
415
|
+
steps:
|
|
416
|
+
- *ghcr-login
|
|
417
|
+
|
|
418
|
+
# Retag the exact multi-arch manifest the smoke gate tested — no
|
|
419
|
+
# rebuild, no wait-for-PyPI. Tested digests ARE the published digests.
|
|
420
|
+
- name: Promote rc manifest
|
|
421
|
+
run: |
|
|
422
|
+
REF="ghcr.io/kolodkin/queryview"
|
|
423
|
+
TAGS=(-t "${REF}:${{ inputs.tag }}")
|
|
424
|
+
if [ "${{ inputs.pre-release }}" != "true" ]; then
|
|
425
|
+
TAGS+=(-t "${REF}:latest")
|
|
426
|
+
fi
|
|
427
|
+
docker buildx imagetools create "${TAGS[@]}" "${REF}:${{ inputs.tag }}-rc"
|
|
428
|
+
|
|
429
|
+
# Best-effort: GITHUB_TOKEN may lack package-version delete rights;
|
|
430
|
+
# leftover -rc tags are harmless and useful for debugging.
|
|
431
|
+
- name: Clean up rc tags
|
|
432
|
+
continue-on-error: true
|
|
433
|
+
env:
|
|
434
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
435
|
+
run: |
|
|
436
|
+
for suffix in rc rc-amd64 rc-arm64; do
|
|
437
|
+
TAG="${{ inputs.tag }}-${suffix}"
|
|
438
|
+
VERSION_ID=$(gh api "/user/packages/container/queryview/versions" --paginate \
|
|
439
|
+
--jq ".[] | select(.metadata.container.tags[]? == \"${TAG}\") | .id" | head -n1)
|
|
440
|
+
if [ -n "$VERSION_ID" ]; then
|
|
441
|
+
gh api -X DELETE "/user/packages/container/queryview/versions/${VERSION_ID}" || true
|
|
442
|
+
fi
|
|
443
|
+
done
|