Skip to content

Commit c6b38ca

Browse files
committed
Harden CLI validation and release workflows
1 parent 7d58ee9 commit c6b38ca

32 files changed

Lines changed: 613 additions & 198 deletions

.github/workflows/ci.yml

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,58 @@ jobs:
4646
- name: Run tests
4747
run: uv run pytest -v
4848

49+
base-install-cli:
50+
name: Base Install CLI
51+
runs-on: ubuntu-latest
52+
steps:
53+
- uses: actions/checkout@v4
54+
55+
- name: Install uv
56+
uses: astral-sh/setup-uv@v5
57+
with:
58+
enable-cache: true
59+
60+
- name: Set up Python
61+
run: uv python install 3.12
62+
63+
- name: Smoke base CLI install
64+
run: |
65+
set -euo pipefail
66+
tmpdir="$(mktemp -d)"
67+
mkdir -p "$tmpdir/models"
68+
cat > "$tmpdir/models/models.yml" <<'YAML'
69+
models:
70+
- name: orders
71+
table: orders
72+
primary_key: id
73+
dimensions:
74+
- name: status
75+
type: categorical
76+
metrics:
77+
- name: order_count
78+
agg: count
79+
YAML
80+
81+
uv run --no-project --with . sidemantic --version
82+
uv run --no-project --with . sidemantic validate "$tmpdir/models"
83+
uv run --no-project --with . sidemantic query "SELECT order_count, status FROM orders" --models "$tmpdir/models" --dry-run
84+
85+
set +e
86+
timeout 10s uv run --no-project --with . sidemantic serve "$tmpdir/models" >"$tmpdir/serve.out" 2>"$tmpdir/serve.err"
87+
serve_status=$?
88+
set -e
89+
if [ "$serve_status" -eq 0 ]; then
90+
echo "base install unexpectedly ran sidemantic serve without the serve extra"
91+
exit 1
92+
fi
93+
if [ "$serve_status" -eq 124 ]; then
94+
echo "base install unexpectedly started sidemantic serve without the serve extra"
95+
cat "$tmpdir/serve.out"
96+
cat "$tmpdir/serve.err" >&2
97+
exit 1
98+
fi
99+
grep -q "sidemantic\\[serve\\]" "$tmpdir/serve.err"
100+
49101
update-schema:
50102
name: Update JSON Schema
51103
needs: python
@@ -267,14 +319,9 @@ jobs:
267319
steps:
268320
- uses: actions/checkout@v4
269321

270-
- name: Clone DuckDB dependencies
322+
- name: Fetch DuckDB dependencies
271323
working-directory: sidemantic-duckdb
272-
run: |
273-
# These are listed as submodules in .gitmodules but aren't tracked properly
274-
# Clone them directly at the commits the extension was built against
275-
rm -rf duckdb extension-ci-tools
276-
git clone --depth 1 --branch v1.4.2 https://github.com/duckdb/duckdb.git duckdb
277-
git clone --depth 1 --branch v1.4.2 https://github.com/duckdb/extension-ci-tools.git extension-ci-tools
324+
run: make deps DUCKDB_VERSION=v1.4.2
278325

279326
- name: Install Rust
280327
uses: dtolnay/rust-toolchain@stable

.github/workflows/duckdb-extension-release.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
workflow_dispatch:
55
inputs:
66
duckdb_version:
7-
description: "DuckDB tag used for extension build and tests"
7+
description: "DuckDB tag used for extension build and tests; currently v1.4.2 only"
88
required: true
99
default: "v1.4.2"
1010
type: string
@@ -42,12 +42,9 @@ jobs:
4242
4343
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
4444
45-
- name: Clone DuckDB dependencies
45+
- name: Fetch DuckDB dependencies
4646
working-directory: sidemantic-duckdb
47-
run: |
48-
rm -rf duckdb extension-ci-tools
49-
git clone --depth 1 --branch "${{ steps.duckdb.outputs.version }}" https://github.com/duckdb/duckdb.git duckdb
50-
git clone --depth 1 --branch "${{ steps.duckdb.outputs.version }}" https://github.com/duckdb/extension-ci-tools.git extension-ci-tools
47+
run: make deps DUCKDB_VERSION="${{ steps.duckdb.outputs.version }}"
5148

5249
- name: Install Rust
5350
uses: dtolnay/rust-toolchain@stable

.github/workflows/publish.yml

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,15 @@ jobs:
7373
- name: Update lock file
7474
run: uv lock
7575

76+
- name: Validate release tree
77+
run: |
78+
uv run ruff check . --exclude docs/_extensions --exclude sidemantic-duckdb/extension-ci-tools --exclude sidemantic-duckdb/scripts --exclude sidemantic-duckdb/duckdb --exclude sidemantic/adapters/malloy_grammar --exclude sidemantic/adapters/holistics_grammar
79+
uv run ruff format --check . --exclude docs/_extensions --exclude sidemantic-duckdb/extension-ci-tools --exclude sidemantic-duckdb/scripts --exclude sidemantic-duckdb/duckdb --exclude sidemantic/adapters/malloy_grammar --exclude sidemantic/adapters/holistics_grammar
80+
uv run pytest -v
81+
7682
- name: Build package
7783
run: uv build
7884

79-
- name: Publish to PyPI
80-
env:
81-
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
82-
run: uv publish --token $UV_PUBLISH_TOKEN
83-
8485
- name: Commit version bump and create tag
8586
run: |
8687
git config user.name "github-actions[bot]"
@@ -91,19 +92,15 @@ jobs:
9192
git push origin main
9293
git push origin "v${{ steps.version.outputs.new_version }}"
9394
95+
- name: Publish to PyPI
96+
env:
97+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
98+
run: uv publish --token $UV_PUBLISH_TOKEN
99+
94100
- name: Create GitHub Release
95101
env:
96102
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
97103
run: |
98104
gh release create "v${{ steps.version.outputs.new_version }}" \
99105
--title "v${{ steps.version.outputs.new_version }}" \
100106
--generate-notes
101-
102-
rust-binaries:
103-
name: Release Rust binaries
104-
needs: publish
105-
permissions:
106-
contents: write
107-
uses: ./.github/workflows/release-rust-binaries.yml
108-
with:
109-
tag: ${{ needs.publish.outputs.tag }}

.github/workflows/pyodide-test.yml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,31 @@ jobs:
2222
- name: Build sidemantic wheel
2323
run: uv build
2424

25+
- name: Inspect wheel metadata
26+
run: |
27+
python - <<'PY'
28+
import re
29+
from email.parser import Parser
30+
from pathlib import Path
31+
from zipfile import ZipFile
32+
33+
wheel = next(Path("dist").glob("*.whl"))
34+
with ZipFile(wheel) as archive:
35+
metadata_name = next(name for name in archive.namelist() if name.endswith(".dist-info/METADATA"))
36+
metadata = Parser().parsestr(archive.read(metadata_name).decode())
37+
38+
print(metadata)
39+
requires = metadata.get_all("Requires-Dist", [])
40+
extras = set(metadata.get_all("Provides-Extra", []))
41+
heavy_optional = ("textual", "pyarrow", "mcp", "riffq", "altair", "vl-convert-python")
42+
for requirement in requires:
43+
name = re.split(r"[<>=!~;\[ ]", requirement.strip(), maxsplit=1)[0]
44+
if name in heavy_optional:
45+
assert "extra ==" in requirement, requirement
46+
assert "serve" in extras
47+
assert "workbench" in extras
48+
PY
49+
2550
- name: Install Pyodide and dependencies
2651
run: npm install pyodide glob
2752

@@ -51,7 +76,7 @@ jobs:
5176
const wheelName = wheelPath.split('/').pop();
5277
pyodide.FS.writeFile(`/tmp/${wheelName}`, wheelData);
5378
54-
console.log('Installing missing deps and sidemantic...');
79+
console.log('Installing sidemantic with the documented Pyodide no-deps path...');
5580
await pyodide.runPythonAsync(`
5681
import micropip
5782
# Install missing pure-Python deps

.github/workflows/release-rust-binaries.yml

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010
workflow_dispatch:
1111
inputs:
1212
tag:
13-
description: Git tag for the GitHub Release to attach binaries to, for example v0.1.0.
13+
description: Git tag for the GitHub Release to attach binaries to, for example sidemantic-rs-v0.1.0.
1414
required: true
1515
type: string
1616

@@ -72,6 +72,27 @@ jobs:
7272
with:
7373
ref: ${{ needs.verify-release.outputs.tag }}
7474

75+
- name: Resolve Rust crate version
76+
id: rust_version
77+
shell: bash
78+
run: |
79+
set -euo pipefail
80+
VERSION=$(grep '^version = ' sidemantic-rs/Cargo.toml | sed 's/version = "\(.*\)"/\1/')
81+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
82+
83+
- name: Verify release tag matches crate version
84+
shell: bash
85+
env:
86+
RELEASE_TAG: ${{ needs.verify-release.outputs.tag }}
87+
RUST_VERSION: ${{ steps.rust_version.outputs.version }}
88+
run: |
89+
set -euo pipefail
90+
expected_tag="sidemantic-rs-v${RUST_VERSION}"
91+
if [ "$RELEASE_TAG" != "$expected_tag" ]; then
92+
echo "Release tag '$RELEASE_TAG' does not match Rust crate version tag '$expected_tag'" >&2
93+
exit 1
94+
fi
95+
7596
- name: Install Rust
7697
uses: dtolnay/rust-toolchain@stable
7798
with:
@@ -115,11 +136,11 @@ jobs:
115136
if: runner.os != 'Windows'
116137
env:
117138
ASSET_SUFFIX: ${{ matrix.asset_suffix }}
118-
TAG: ${{ needs.verify-release.outputs.tag }}
139+
RUST_VERSION: ${{ steps.rust_version.outputs.version }}
119140
TARGET: ${{ matrix.target }}
120141
run: |
121142
set -euo pipefail
122-
version="${TAG#v}"
143+
version="$RUST_VERSION"
123144
asset_name="sidemantic-rs-${version}-${ASSET_SUFFIX}"
124145
archive_path="dist/${asset_name}.tar.gz"
125146
checksum_path="${archive_path}.sha256"
@@ -147,14 +168,11 @@ jobs:
147168
if: runner.os == 'Windows'
148169
env:
149170
ASSET_SUFFIX: ${{ matrix.asset_suffix }}
150-
TAG: ${{ needs.verify-release.outputs.tag }}
171+
RUST_VERSION: ${{ steps.rust_version.outputs.version }}
151172
TARGET: ${{ matrix.target }}
152173
shell: pwsh
153174
run: |
154-
$version = $env:TAG
155-
if ($version.StartsWith("v")) {
156-
$version = $version.Substring(1)
157-
}
175+
$version = $env:RUST_VERSION
158176
$assetName = "sidemantic-rs-$version-$env:ASSET_SUFFIX"
159177
$archivePath = "dist/$assetName.zip"
160178
$checksumPath = "$archivePath.sha256"

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,13 @@ result = layer.sql("SELECT revenue, status FROM orders")
129129
sidemantic query "SELECT revenue FROM orders" --db data.duckdb
130130

131131
# Interactive workbench (TUI with SQL editor + charts)
132-
sidemantic workbench models/ --db data.duckdb
132+
uvx --from "sidemantic[workbench]" sidemantic workbench models/ --db data.duckdb
133133

134134
# PostgreSQL server (connect Tableau, DBeaver, etc.)
135-
sidemantic serve models/ --port 5433
135+
uvx --from "sidemantic[serve]" sidemantic serve models/ --port 5433
136136

137137
# HTTP API server (JSON or Arrow)
138-
sidemantic api-serve models/ --port 4400 --auth-token secret
138+
uvx --from "sidemantic[api]" sidemantic api-serve models/ --port 4400 --auth-token secret
139139

140140
# Validate definitions
141141
sidemantic validate models/
@@ -159,7 +159,7 @@ uvx --from "sidemantic[workbench]" sidemantic workbench --demo
159159

160160
**PostgreSQL server** (connect Tableau, DBeaver, etc.):
161161
```bash
162-
uvx sidemantic serve --demo --port 5433
162+
uvx --from "sidemantic[serve]" sidemantic serve --demo --port 5433
163163
```
164164

165165
**HTTP API server** (JSON or Arrow):
@@ -227,7 +227,7 @@ See `examples/` for more.
227227
- Multi-format adapters (Cube, MetricFlow, LookML, Hex, Rill, Superset, Omni, BSL, GoodData LDM, OSI, AtScale SML, ThoughtSpot TML, Graphene GSQL)
228228
- SQLGlot-based SQL generation and transpilation
229229
- Pydantic validation and type safety
230-
- Pre-aggregations with automatic routing
230+
- Pre-aggregations with explicit routing
231231
- Predicate pushdown for faster queries
232232
- Segments and metric-level filters
233233
- Jinja2 templating for dynamic SQL
@@ -285,7 +285,7 @@ For Cloudflare Worker + Container deployment, see [`examples/cloudflare_containe
285285
Start the API server:
286286

287287
```bash
288-
sidemantic api-serve models/ --db data.duckdb --port 4400 --auth-token secret
288+
uvx --from "sidemantic[api]" sidemantic api-serve models/ --db data.duckdb --port 4400 --auth-token secret
289289
```
290290

291291
Compile a structured semantic query:

docs/duckdb-extension.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,27 @@ LOAD sidemantic;
1414
```
1515

1616
Until community publication is complete, use a local extension artifact.
17+
Local artifacts are unsigned, so start the DuckDB CLI with unsigned-extension loading enabled:
18+
19+
```bash
20+
duckdb -unsigned
21+
```
1722

1823
## Build From Source
1924

2025
The extension build needs Rust, DuckDB extension build tooling, and Ninja.
2126

2227
```bash
2328
cd sidemantic-duckdb
24-
rm -rf duckdb extension-ci-tools
25-
git clone --depth 1 --branch v1.4.2 https://github.com/duckdb/duckdb.git duckdb
26-
git clone --depth 1 --branch v1.4.2 https://github.com/duckdb/extension-ci-tools.git extension-ci-tools
29+
make deps DUCKDB_VERSION=v1.4.2
2730
make
2831
make test
2932
```
3033

34+
`DUCKDB_VERSION` is intentionally guarded to `v1.4.2` because the repository
35+
vendors a matching `extension-ci-tools` checkout. Update both together before
36+
building against a different DuckDB tag.
37+
3138
The local loadable extension is produced at:
3239

3340
```text
@@ -36,10 +43,16 @@ sidemantic-duckdb/build/release/extension/sidemantic/sidemantic.duckdb_extension
3643

3744
Load it in the DuckDB shell built by the extension workflow:
3845

46+
```bash
47+
./build/release/duckdb -unsigned
48+
```
49+
3950
```sql
4051
LOAD 'build/release/extension/sidemantic/sidemantic.duckdb_extension';
4152
```
4253

54+
For embedded clients, set DuckDB's `allow_unsigned_extensions` database configuration before opening the connection.
55+
4356
## Runtime API
4457

4558
Load native YAML:
@@ -88,7 +101,7 @@ Use `.github/workflows/duckdb-extension-release.yml`.
88101

89102
The workflow:
90103

91-
- clones DuckDB and `extension-ci-tools` for the selected DuckDB tag,
104+
- fetches DuckDB with `make deps` and verifies the vendored `extension-ci-tools`,
92105
- builds the Rust-backed extension,
93106
- runs the DuckDB sqllogictests, including native YAML load, native SQL definition file load, relationship rewrite, semantic select, persistence, and invalid-version coverage,
94107
- uploads a Linux extension artifact,

docs/pyodide.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Pyodide Runtime
2+
3+
Sidemantic's browser/WASM path is a no-dependency wheel install. Install the Pyodide-compatible runtime packages first, then install the Sidemantic wheel with dependency resolution disabled:
4+
5+
```python
6+
import micropip
7+
8+
await pyodide.loadPackage(["micropip", "pydantic", "pyyaml", "jinja2"])
9+
await micropip.install(["sqlglot", "lkml", "inflect"], deps=False)
10+
await micropip.install("emfs:/tmp/sidemantic-<version>-py3-none-any.whl", deps=False)
11+
```
12+
13+
This is intentional. The published Python package includes CLI/database dependencies that are not part of the Pyodide runtime contract. The supported Pyodide import surface is the core semantic model API, for example:
14+
15+
```python
16+
from sidemantic import Model, Dimension, Metric, Relationship
17+
from sidemantic.core.semantic_graph import SemanticGraph
18+
```
19+
20+
Optional server, workbench, chart, and database execution paths are not Pyodide targets.

0 commit comments

Comments
 (0)