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.
Files changed (139) hide show
  1. queryview-0.0.2/.claude/settings.json +23 -0
  2. queryview-0.0.2/.github/actions/start-git-daemon/action.yaml +31 -0
  3. queryview-0.0.2/.github/workflows/ci.yml +182 -0
  4. queryview-0.0.2/.github/workflows/publish.yaml +443 -0
  5. queryview-0.0.2/.gitignore +39 -0
  6. queryview-0.0.2/.mcp.json +15 -0
  7. queryview-0.0.2/.pre-commit-config.yaml +23 -0
  8. queryview-0.0.2/CLAUDE.md +39 -0
  9. queryview-0.0.2/LICENSE +21 -0
  10. queryview-0.0.2/PKG-INFO +183 -0
  11. queryview-0.0.2/README.md +159 -0
  12. queryview-0.0.2/backend/alembic.ini +9 -0
  13. queryview-0.0.2/backend/queryview/__init__.py +0 -0
  14. queryview-0.0.2/backend/queryview/conftest.py +86 -0
  15. queryview-0.0.2/backend/queryview/connect.py +428 -0
  16. queryview-0.0.2/backend/queryview/dashboard_queries.py +75 -0
  17. queryview-0.0.2/backend/queryview/dashboards.py +156 -0
  18. queryview-0.0.2/backend/queryview/drivers/__init__.py +10 -0
  19. queryview-0.0.2/backend/queryview/drivers/base.py +165 -0
  20. queryview-0.0.2/backend/queryview/drivers/clickhouse.py +138 -0
  21. queryview-0.0.2/backend/queryview/drivers/duckdb.py +153 -0
  22. queryview-0.0.2/backend/queryview/drivers/postgres.py +166 -0
  23. queryview-0.0.2/backend/queryview/drivers/test_base.py +49 -0
  24. queryview-0.0.2/backend/queryview/drivers/test_clickhouse.py +53 -0
  25. queryview-0.0.2/backend/queryview/drivers/test_contract.py +77 -0
  26. queryview-0.0.2/backend/queryview/drivers/test_duckdb.py +80 -0
  27. queryview-0.0.2/backend/queryview/drivers/test_postgres.py +80 -0
  28. queryview-0.0.2/backend/queryview/gitsync.py +374 -0
  29. queryview-0.0.2/backend/queryview/main.py +740 -0
  30. queryview-0.0.2/backend/queryview/mcp_server.py +294 -0
  31. queryview-0.0.2/backend/queryview/migrations/env.py +39 -0
  32. queryview-0.0.2/backend/queryview/migrations/script.py.mako +29 -0
  33. queryview-0.0.2/backend/queryview/migrations/versions/9a536b7c0328_initial_schema.py +89 -0
  34. queryview-0.0.2/backend/queryview/migrations/versions/a1b2c3d4e5f6_connection_config_blob.py +59 -0
  35. queryview-0.0.2/backend/queryview/migrations/versions/b2c3d4e5f6a7_predefined_presentation.py +32 -0
  36. queryview-0.0.2/backend/queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py +98 -0
  37. queryview-0.0.2/backend/queryview/queries.py +159 -0
  38. queryview-0.0.2/backend/queryview/remote.py +141 -0
  39. queryview-0.0.2/backend/queryview/static/assets/index-CvnC_D68.js +47 -0
  40. queryview-0.0.2/backend/queryview/static/assets/index-Qe7bhycG.css +2 -0
  41. queryview-0.0.2/backend/queryview/static/favicon.svg +1 -0
  42. queryview-0.0.2/backend/queryview/static/index.html +14 -0
  43. queryview-0.0.2/backend/queryview/test_api_db.py +51 -0
  44. queryview-0.0.2/backend/queryview/test_api_export_import.py +85 -0
  45. queryview-0.0.2/backend/queryview/test_api_gitsync.py +83 -0
  46. queryview-0.0.2/backend/queryview/test_api_workspaces.py +44 -0
  47. queryview-0.0.2/backend/queryview/test_connect_flow.py +123 -0
  48. queryview-0.0.2/backend/queryview/test_connect_store.py +34 -0
  49. queryview-0.0.2/backend/queryview/test_dashboards.py +216 -0
  50. queryview-0.0.2/backend/queryview/test_gitsync.py +346 -0
  51. queryview-0.0.2/backend/queryview/test_main.py +18 -0
  52. queryview-0.0.2/backend/queryview/test_mcp_gitsync.py +72 -0
  53. queryview-0.0.2/backend/queryview/test_migrations.py +99 -0
  54. queryview-0.0.2/backend/queryview/test_queries.py +170 -0
  55. queryview-0.0.2/backend/queryview/test_remote.py +260 -0
  56. queryview-0.0.2/backend/queryview/test_validation.py +87 -0
  57. queryview-0.0.2/backend/queryview/test_workspaces.py +109 -0
  58. queryview-0.0.2/backend/queryview/test_yamlio.py +198 -0
  59. queryview-0.0.2/backend/queryview/validation.py +111 -0
  60. queryview-0.0.2/backend/queryview/workspaces.py +167 -0
  61. queryview-0.0.2/backend/queryview/yamlio.py +245 -0
  62. queryview-0.0.2/docker/Dockerfile +32 -0
  63. queryview-0.0.2/docs/api.md +96 -0
  64. queryview-0.0.2/docs/connect.md +208 -0
  65. queryview-0.0.2/docs/dashboard.md +107 -0
  66. queryview-0.0.2/docs/explorer.md +74 -0
  67. queryview-0.0.2/docs/export-import.md +80 -0
  68. queryview-0.0.2/docs/future.md +43 -0
  69. queryview-0.0.2/docs/gitsync.md +67 -0
  70. queryview-0.0.2/docs/query.md +296 -0
  71. queryview-0.0.2/docs/queryview.md +78 -0
  72. queryview-0.0.2/docs/remote.md +72 -0
  73. queryview-0.0.2/docs/workspace.md +54 -0
  74. queryview-0.0.2/e2e/conftest.py +162 -0
  75. queryview-0.0.2/e2e/test_app.py +47 -0
  76. queryview-0.0.2/e2e/test_dashboard.py +110 -0
  77. queryview-0.0.2/e2e/test_drivers.py +141 -0
  78. queryview-0.0.2/e2e/test_explorer.py +66 -0
  79. queryview-0.0.2/e2e/test_export_import.py +48 -0
  80. queryview-0.0.2/e2e/test_gitsync.py +62 -0
  81. queryview-0.0.2/e2e/test_query.py +394 -0
  82. queryview-0.0.2/e2e/test_remote.py +65 -0
  83. queryview-0.0.2/e2e/test_workspaces.py +44 -0
  84. queryview-0.0.2/frontend/.gitignore +24 -0
  85. queryview-0.0.2/frontend/README.md +73 -0
  86. queryview-0.0.2/frontend/eslint.config.js +22 -0
  87. queryview-0.0.2/frontend/index.html +13 -0
  88. queryview-0.0.2/frontend/package.json +37 -0
  89. queryview-0.0.2/frontend/public/favicon.svg +1 -0
  90. queryview-0.0.2/frontend/src/App.tsx +350 -0
  91. queryview-0.0.2/frontend/src/CellViewModal.tsx +62 -0
  92. queryview-0.0.2/frontend/src/ComplexCell.tsx +50 -0
  93. queryview-0.0.2/frontend/src/DashboardView.tsx +281 -0
  94. queryview-0.0.2/frontend/src/ExplorerView.tsx +328 -0
  95. queryview-0.0.2/frontend/src/ExportImportControls.tsx +91 -0
  96. queryview-0.0.2/frontend/src/FieldPickers.tsx +153 -0
  97. queryview-0.0.2/frontend/src/GitSyncControls.tsx +0 -0
  98. queryview-0.0.2/frontend/src/QueryView.tsx +1439 -0
  99. queryview-0.0.2/frontend/src/ResultsTable.tsx +54 -0
  100. queryview-0.0.2/frontend/src/Toast.tsx +19 -0
  101. queryview-0.0.2/frontend/src/WorkspaceSwitcher.tsx +211 -0
  102. queryview-0.0.2/frontend/src/cellView.test.ts +104 -0
  103. queryview-0.0.2/frontend/src/cellView.ts +38 -0
  104. queryview-0.0.2/frontend/src/compactNumber.test.ts +43 -0
  105. queryview-0.0.2/frontend/src/compactNumber.ts +27 -0
  106. queryview-0.0.2/frontend/src/complexCellParsing.test.ts +221 -0
  107. queryview-0.0.2/frontend/src/complexCellParsing.ts +177 -0
  108. queryview-0.0.2/frontend/src/drivers.ts +64 -0
  109. queryview-0.0.2/frontend/src/gitsync.test.ts +20 -0
  110. queryview-0.0.2/frontend/src/gitsync.ts +99 -0
  111. queryview-0.0.2/frontend/src/index.css +166 -0
  112. queryview-0.0.2/frontend/src/main.tsx +10 -0
  113. queryview-0.0.2/frontend/src/presentation.test.ts +14 -0
  114. queryview-0.0.2/frontend/src/presentation.ts +28 -0
  115. queryview-0.0.2/frontend/src/promptSuggestions.test.ts +100 -0
  116. queryview-0.0.2/frontend/src/promptSuggestions.ts +70 -0
  117. queryview-0.0.2/frontend/src/queryParams.test.ts +93 -0
  118. queryview-0.0.2/frontend/src/queryParams.ts +79 -0
  119. queryview-0.0.2/frontend/src/sessionLock.test.ts +30 -0
  120. queryview-0.0.2/frontend/src/sessionLock.ts +17 -0
  121. queryview-0.0.2/frontend/src/tsv.ts +8 -0
  122. queryview-0.0.2/frontend/src/workspace.test.ts +30 -0
  123. queryview-0.0.2/frontend/src/workspace.ts +63 -0
  124. queryview-0.0.2/frontend/src/yamlio.test.ts +29 -0
  125. queryview-0.0.2/frontend/src/yamlio.ts +76 -0
  126. queryview-0.0.2/frontend/tsconfig.app.json +25 -0
  127. queryview-0.0.2/frontend/tsconfig.json +7 -0
  128. queryview-0.0.2/frontend/tsconfig.node.json +24 -0
  129. queryview-0.0.2/frontend/vite.config.ts +17 -0
  130. queryview-0.0.2/package-lock.json +3830 -0
  131. queryview-0.0.2/package.json +22 -0
  132. queryview-0.0.2/pyproject.toml +83 -0
  133. queryview-0.0.2/pyrightconfig.json +13 -0
  134. queryview-0.0.2/scripts/dev.sh +27 -0
  135. queryview-0.0.2/scripts/setup.sh +19 -0
  136. queryview-0.0.2/scripts/setup_browser.sh +78 -0
  137. queryview-0.0.2/scripts/setup_clickhouse.sh +72 -0
  138. queryview-0.0.2/scripts/setup_postgres.sh +102 -0
  139. 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