From 500717c7f1b5766227e41715cfaf54db30b3e8b9 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 11 Feb 2026 15:13:49 -0600 Subject: [PATCH 1/3] ci: add PyPI release workflow for ggsql Python package Adds a GitHub Actions workflow to build and publish the ggsql Python package to PyPI. Uses PyO3 abi3-py310 so one wheel per platform covers all Python 3.10+. Workflow structure: - `generate` job: runs tree-sitter generate on a regular runner, uploads parser artifacts - Build jobs (linux, macos, windows, sdist): download parser artifacts, build wheels with maturin - `publish` + `github-release`: deploy on tag push Build details: - Linux: manylinux_2_28 for x86_64 and aarch64 - macOS: macos-latest for both x86_64 (cross-compile) and aarch64 - Windows: x64 - build.rs skips parser generation when parser.c already exists (provided by the generate job artifact) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/python-release.yml | 195 +++++++++++++++++++++++ ggsql-python/Cargo.toml | 2 +- tree-sitter-ggsql/bindings/rust/build.rs | 34 ++-- 3 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/python-release.yml diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml new file mode 100644 index 00000000..e3279b48 --- /dev/null +++ b/.github/workflows/python-release.yml @@ -0,0 +1,195 @@ +name: Python Release + +on: + push: + tags: + - "py/v*" + workflow_dispatch: + +permissions: + contents: read + +env: + # Must match the abi3-py3XX feature in ggsql-python/Cargo.toml. + # abi3 wheels built against 3.10 are forward-compatible with all later + # Python versions, so this should be the minimum supported version. + PYTHON_VERSION: "3.10" + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install tree-sitter-cli + run: npm install -g tree-sitter-cli + + - name: Generate parser + working-directory: tree-sitter-ggsql + run: tree-sitter generate + + - uses: actions/upload-artifact@v4 + with: + name: tree-sitter-generated + path: tree-sitter-ggsql/src/ + + linux: + needs: generate + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: tree-sitter-generated + path: tree-sitter-ggsql/src/ + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --interpreter python${{ env.PYTHON_VERSION }} + working-directory: ggsql-python + # manylinux2014's cross-compiler is too old for the `ring` crate's + # aarch64 assembly (missing __ARM_ARCH). 2_28 (AlmaLinux 8, gcc 8+) + # provides a new enough toolchain. + manylinux: 2_28 + + - uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.target }} + path: ggsql-python/dist + + macos: + needs: generate + runs-on: ${{ matrix.runner }} + strategy: + matrix: + include: + - target: x86_64 + runner: macos-latest + - target: aarch64 + runner: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: tree-sitter-generated + path: tree-sitter-ggsql/src/ + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --interpreter python${{ env.PYTHON_VERSION }} + working-directory: ggsql-python + + - uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.target }} + path: ggsql-python/dist + + windows: + needs: generate + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: tree-sitter-generated + path: tree-sitter-ggsql/src/ + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: x64 + args: --release --out dist --interpreter python${{ env.PYTHON_VERSION }} + working-directory: ggsql-python + + - uses: actions/upload-artifact@v4 + with: + name: wheels-windows-x64 + path: ggsql-python/dist + + sdist: + needs: generate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: tree-sitter-generated + path: tree-sitter-ggsql/src/ + + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + working-directory: ggsql-python + + - uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: ggsql-python/dist + + publish: + needs: [linux, macos, windows, sdist] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + merge-multiple: true + path: dist + + - name: List wheels + run: ls -lh dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + github-release: + needs: publish + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + merge-multiple: true + path: dist + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true diff --git a/ggsql-python/Cargo.toml b/ggsql-python/Cargo.toml index 8f73e6f8..6c69076e 100644 --- a/ggsql-python/Cargo.toml +++ b/ggsql-python/Cargo.toml @@ -10,7 +10,7 @@ name = "_ggsql" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.26", features = ["extension-module"] } +pyo3 = { version = "0.26", features = ["extension-module", "abi3-py310"] } polars = { workspace = true, features = ["ipc"] } ggsql = { path = "../src", default-features = false, features = ["duckdb", "vegalite"] } diff --git a/tree-sitter-ggsql/bindings/rust/build.rs b/tree-sitter-ggsql/bindings/rust/build.rs index 6072306f..73ea5862 100644 --- a/tree-sitter-ggsql/bindings/rust/build.rs +++ b/tree-sitter-ggsql/bindings/rust/build.rs @@ -84,26 +84,36 @@ fn main() { // CARGO_MANIFEST_DIR points to tree-sitter-ggsql/ where Cargo.toml and grammar.js live let grammar_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let src_dir = grammar_dir.join("src"); + let parser_c = src_dir.join("parser.c"); - let tree_sitter = find_tree_sitter().unwrap_or_else(|| { - panic!("tree-sitter-cli not found. Please install it: npm install -g tree-sitter-cli"); - }); + // Only regenerate if parser.c doesn't exist (e.g., after a fresh grammar change). + // The generated files are committed to the repo following standard tree-sitter + // practice, so most builds (including CI) skip this step entirely. + if !parser_c.exists() { + let tree_sitter = find_tree_sitter().unwrap_or_else(|| { + panic!( + "tree-sitter-cli not found and src/parser.c does not exist. \ + Either run `tree-sitter generate` first, or install tree-sitter-cli: \ + npm install -g tree-sitter-cli" + ); + }); - let generate_result = run_tree_sitter(&tree_sitter, &grammar_dir); + let generate_result = run_tree_sitter(&tree_sitter, &grammar_dir); - match generate_result { - Ok(status) if status.success() => {} - Ok(status) => { - panic!("tree-sitter generate failed with status: {}", status); - } - Err(e) => { - panic!("Failed to run tree-sitter generate: {}", e); + match generate_result { + Ok(status) if status.success() => {} + Ok(status) => { + panic!("tree-sitter generate failed with status: {}", status); + } + Err(e) => { + panic!("Failed to run tree-sitter generate: {}", e); + } } } // The generated files are in the grammar_dir/src directory cc::Build::new() .include(&src_dir) - .file(src_dir.join("parser.c")) + .file(&parser_c) .compile("tree-sitter-ggsql"); } From a13128fb931313decc77ef137e0d7f08809012ee Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 13 Feb 2026 10:31:30 -0600 Subject: [PATCH 2/3] Improve comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tree-sitter-ggsql/bindings/rust/build.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tree-sitter-ggsql/bindings/rust/build.rs b/tree-sitter-ggsql/bindings/rust/build.rs index 73ea5862..9b11d918 100644 --- a/tree-sitter-ggsql/bindings/rust/build.rs +++ b/tree-sitter-ggsql/bindings/rust/build.rs @@ -86,9 +86,11 @@ fn main() { let src_dir = grammar_dir.join("src"); let parser_c = src_dir.join("parser.c"); - // Only regenerate if parser.c doesn't exist (e.g., after a fresh grammar change). - // The generated files are committed to the repo following standard tree-sitter - // practice, so most builds (including CI) skip this step entirely. + // Only regenerate if parser.c doesn't exist (e.g., after a fresh checkout or when + // the generated files have not been produced yet). While tree-sitter projects + // typically commit generated files to the repo, this project's CI workflow + // explicitly runs `tree-sitter generate` to ensure the committed and generated + // sources stay consistent, rather than relying solely on the committed output. if !parser_c.exists() { let tree_sitter = find_tree_sitter().unwrap_or_else(|| { panic!( From 4ac70728e8c4f4ccbca3ebbb82eeb815b27fe844 Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 17 Feb 2026 19:01:01 -0600 Subject: [PATCH 3/3] fix: use env var to skip parser generation in CI instead of file check Restore default behavior of always regenerating parser.c on local builds so grammar changes are picked up automatically. CI build jobs set GGSQL_SKIP_GENERATE=1 to use pre-generated parser artifacts instead. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/python-release.yml | 8 ++++++++ tree-sitter-ggsql/bindings/rust/build.rs | 26 +++++++++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index e3279b48..e6c4b958 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -43,6 +43,8 @@ jobs: strategy: matrix: target: [x86_64, aarch64] + env: + GGSQL_SKIP_GENERATE: "1" steps: - uses: actions/checkout@v4 @@ -81,6 +83,8 @@ jobs: runner: macos-latest - target: aarch64 runner: macos-latest + env: + GGSQL_SKIP_GENERATE: "1" steps: - uses: actions/checkout@v4 @@ -108,6 +112,8 @@ jobs: windows: needs: generate runs-on: windows-latest + env: + GGSQL_SKIP_GENERATE: "1" steps: - uses: actions/checkout@v4 @@ -135,6 +141,8 @@ jobs: sdist: needs: generate runs-on: ubuntu-latest + env: + GGSQL_SKIP_GENERATE: "1" steps: - uses: actions/checkout@v4 diff --git a/tree-sitter-ggsql/bindings/rust/build.rs b/tree-sitter-ggsql/bindings/rust/build.rs index 9b11d918..48180e7e 100644 --- a/tree-sitter-ggsql/bindings/rust/build.rs +++ b/tree-sitter-ggsql/bindings/rust/build.rs @@ -86,18 +86,24 @@ fn main() { let src_dir = grammar_dir.join("src"); let parser_c = src_dir.join("parser.c"); - // Only regenerate if parser.c doesn't exist (e.g., after a fresh checkout or when - // the generated files have not been produced yet). While tree-sitter projects - // typically commit generated files to the repo, this project's CI workflow - // explicitly runs `tree-sitter generate` to ensure the committed and generated - // sources stay consistent, rather than relying solely on the committed output. - if !parser_c.exists() { - let tree_sitter = find_tree_sitter().unwrap_or_else(|| { + // Re-run this build script if the env var changes. + println!("cargo:rerun-if-env-changed=GGSQL_SKIP_GENERATE"); + + // By default, always regenerate parser.c from grammar.js so local builds + // pick up grammar changes automatically. CI sets GGSQL_SKIP_GENERATE=1 to + // skip this step when pre-generated parser files are provided as artifacts. + let skip_generate = std::env::var("GGSQL_SKIP_GENERATE").is_ok(); + + if skip_generate { + if !parser_c.exists() { panic!( - "tree-sitter-cli not found and src/parser.c does not exist. \ - Either run `tree-sitter generate` first, or install tree-sitter-cli: \ - npm install -g tree-sitter-cli" + "GGSQL_SKIP_GENERATE is set but src/parser.c does not exist. \ + Either run `tree-sitter generate` first, or unset GGSQL_SKIP_GENERATE." ); + } + } else { + let tree_sitter = find_tree_sitter().unwrap_or_else(|| { + panic!("tree-sitter-cli not found. Please install it: npm install -g tree-sitter-cli"); }); let generate_result = run_tree_sitter(&tree_sitter, &grammar_dir);