How to cut a new release of no.sikt:graphitron-tree-sitter-natives, when to bump which version digit, and what the release workflow verifies.

This is internal-to-the-rewrite operational documentation, not a user-facing chapter. The published artifact is consumed only by graphitron-lsp; consumers of graphitron’s generator surface never see it directly.

What the artifact is

A single jar that ships two per-platform tree-sitter libraries under lib/<os>-<arch>/: the GraphQL grammar and the tree-sitter runtime.

Platform Grammar Runtime

linux-x86_64

lib/linux-x86_64/libtree-sitter-graphql.so

lib/linux-x86_64/libtree-sitter.so

linux-aarch64

lib/linux-aarch64/libtree-sitter-graphql.so

lib/linux-aarch64/libtree-sitter.so

macos-aarch64

lib/macos-aarch64/libtree-sitter-graphql.dylib

lib/macos-aarch64/libtree-sitter.dylib

windows-x86_64

lib/windows-x86_64/tree-sitter-graphql.dll

lib/windows-x86_64/tree-sitter.dll

macOS is Apple-silicon-only. Sikt graphitron-lsp developers all run M1 or newer, the macos-13 (Intel) GitHub-hosted runner is the slowest queue on the platform, and graphitron-lsp is internal-only per the spec’s "Out of scope" list. Adding macos-x86_64 back means re-enabling that matrix entry and bumping the jar-layout assertion expectations.

The grammar binary is the vendored bkegley tree-sitter-graphql grammar (commit 5e66e96), built with the upstream tree-sitter build CLI against the pinned parser ABI. It exports the grammar’s tree_sitter_graphql entry point only, imports no ts_* symbols, and so loads independently of the runtime.

The runtime binary is the upstream tree-sitter runtime (libtree-sitter, exporting the ts_* symbols) built from the pinned source tag, not the system package. graphitron-lsp’s BundledLibraryLookup extracts both binaries from this jar and composes grammar.or(runtime) into jtreesitter’s SPI lookup, so the LSP has no native system dependency at all.

Bundling the runtime reverses the module’s original grammar-only split. That split made the runtime a system dependency (brew install tree-sitter, pacman -S, vcpkg install, or a from-source build on Debian/Ubuntu, whose apt libtree-sitter0 is 0.20.x and too old). The recurring cost of that install path, especially on Windows and Debian/Ubuntu, is what R401 retired by bundling the runtime; the tree-sitter project still does not publish a prebuilt libtree-sitter asset, so we build it ourselves on native runners from the pinned source (see "Cutting a release" below).

The grammar build uses the upstream tree-sitter build CLI rather than a hand-rolled cc / cl.exe invocation. The CLI handles MSVC export annotations on Windows uniformly, which is what gives us a working Windows DLL with the grammar entry point correctly exported (the bonede route fails this on Windows, exporting Java_org_treesitter_* JNI wrappers only). The runtime build does not use the CLI: it builds libtree-sitter from the cloned source lib/ (POSIX make; Windows MinGW-w64 make with static-linked gcc runtime), with a transitive-dep allowlist enforced as a release gate.

Versioning

Format: <runtime-version>-<build-n>. First bundled-runtime release: 0.26.9-1. The <runtime-version> portion is the actual tree-sitter runtime version shipped in the jar: the upstream source tag both the grammar ABI and the bundled libtree-sitter are built from. It must match the tree-sitter-cli-version workflow input (tag v0.26.9 for 0.26.9-1).

Before 0.26.9-1, <runtime-version> meant the parser ABI the grammar was built against and the runtime was a system dependency; from 0.26.9-1 it names the runtime we ship.

Change Version bump

Grammar update (new parser.c)

<build-n> (e.g. 0.26.9-10.26.9-2)

Build-flag change

<build-n>

Recompile / security rebuild with no source change

<build-n>

Retarget to a newer upstream tree-sitter tag

<runtime-version> resets <build-n> to 1 (e.g. 0.26.9-30.27.0-1)

Snapshots are not used. The natives module ships releases only, and a release that turns out to be broken on one platform after deploy stays on Central as a tombstone; graphitron-lsp pins to the working build that follows.

Cutting a release

Three steps, the second being the GitHub Actions trigger.

  1. Bump the version in graphitron-tree-sitter-natives/pom.xml. If the parser ABI moved, also update the matching pin in src/main/native/grammars/graphql/UPSTREAM.md and the module-level UPSTREAM.md. Commit on a normal feature branch and push.

  2. Trigger the workflow. In the GitHub Actions UI for sikt-no/graphitron, run .github/workflows/tree-sitter-natives-release.yml via workflow_dispatch. The single input is the upstream tree-sitter tag (e.g. v0.26.9); it pins both the CLI that builds the grammar and the source checkout the bundled runtime is built from, and must match the <runtime-version> portion of the pom version.

  3. Watch the three stages. The workflow only deploys if every stage succeeds:

    1. Build matrix (four native runners). Each runner builds two binaries. The grammar: download the pinned tree-sitter CLI, run tree-sitter build against the vendored grammar, assert it exports tree_sitter_graphql. The runtime: clone the pinned source tag and build libtree-sitter from lib/ (POSIX make; Windows MinGW-w64 make with -static -static-libgcc), then run the release gates: it must export ts_language_abi_version and the core ts_parser_* symbols, and its transitive deps must fall within the per-platform allowlist (ldd / otool -L / dumpbin /dependents; a stray libgcc_s_seh-1.dll or third-party .so fails the release). Symbol checks use nm -gU on POSIX and dumpbin /exports on Windows. Both binaries upload as native-<platform>.

    2. Package + deploy (ubuntu-latest). Downloads all four artifacts, stages grammar + runtime under target/classes/lib/<os>-<arch>/, runs mvn package, and asserts the produced jar contains exactly the eight lib/<os>-<arch>/…​ entries (four grammar + four runtime) and no others. Then mvn deploy signs with GPG and publishes via central-publishing-maven-plugin with autoPublish=true.

    3. Post-deploy load+parse matrix (four native runners). After deploy succeeds, each runner resolves the freshly-published jar from a clean local m2 (with retries against Central’s propagation delay), then runs a tiny FFM-based Java verifier with no system libtree-sitter installed. The verifier extracts both the grammar and the runtime from the jar, composes grammar.or(runtime), calls Language.load(symbols, "tree_sitter_graphql"), parses "type Query { hello: String }", and asserts the root is source_file with no errors. A pass with no system runtime present is the proof that the jar is self-sufficient on that platform.

If stage 3 fails on any platform, bump <build-n> and re-release. The failing version stays on Central; downstream graphitron-lsp pins to the next working <build-n>.

Why the build infrastructure looks the way it does

Three design choices worth knowing about for anyone maintaining this:

  1. Standalone pom, no parent inheritance. The natives module lives in the repo root alongside the reactor modules but its pom.xml does not declare <parent>graphitron-rewrite-parent</parent>. Trunk’s parent runs at 10-SNAPSHOT and declares no <snapshotRepository>; inheriting would publish a natives jar whose parent reference is unresolvable for any Central consumer. The release plumbing (maven-gpg-plugin, central-publishing-maven-plugin, maven-source-plugin, maven-javadoc-plugin, maven-deploy-plugin) is inlined into the module’s pom for the same reason. The parent’s <modules> list also does not include this module, so the regular reactor build (mvn install -Plocal-db) pays zero cost for it.

  2. No CI build per PR. The binaries don’t change per PR (they’re a published artifact, not a per-build output), so expanding .github/workflows/rewrite-build.yml to a four-platform matrix would mostly re-verify a stable input. Instead, the verification matrix lives in this workflow and runs against the actual published artifact at release time. In normal CI, only the linux-x86_64 arm of NativeLibraryBundleTest executes per PR (the three others are gated by @EnabledOnOs to their respective hosts and run on this workflow’s post-deploy matrix).

  3. Built with the upstream CLI, not hand-rolled compiler invocations. tree-sitter build knows how to do MSVC _declspec(dllexport) for Windows, which the upstream tree-sitter C sources don’t carry on their own. The bonede tree-sitter-graphql jar we previously evaluated ships a Windows DLL that exports Java_org_treesitter* JNI wrappers but not the bare tree_sitter_graphql entry point jtreesitter expects, which is why the natives module exists at all.

Where the binaries live before deploy

Not committed. The eight binaries (four grammar + four runtime) are produced fresh on four native runners per release and merged into the jar during the package+deploy stage. The vendored grammar sources under graphitron-tree-sitter-natives/src/main/native/grammars/graphql/ are the grammar’s only git-resident input; the runtime is built from the pinned upstream source tag cloned at release time, not vendored in git. Its provenance (source tag, per-platform build invocation and flags, the transitive-dep allowlist, and the update procedure) is recorded in the module-level UPSTREAM.md "Runtime provenance" section.