|
| 1 | +# Packaging & release: tag-triggered automation |
| 2 | + |
| 3 | +Release mechanics for both halves of the ladder that reach this rung: an **analyzer** |
| 4 | +(`codeanalyzer-<lang>`) cutting its own distribution, and an **SDK** (`python-sdk`, |
| 5 | +`typescript-sdk`, …) picking up a new analyzer release via a pin bump and cutting its own. Neither |
| 6 | +is a manual, one-off act — both are `git tag vX.Y.Z && git push origin vX.Y.Z` triggering a CI |
| 7 | +workflow that does the rest. Never publish by hand; if the tag-push doesn't produce the artifact, |
| 8 | +fix the workflow rather than uploading around it. |
| 9 | + |
| 10 | +## Analyzer release: the thin-PyPI-binary model |
| 11 | + |
| 12 | +The analyzer repo builds its self-contained binary for every supported platform, then a **tag- |
| 13 | +triggered** `.github/workflows/release.yml` publishes it three ways in one coordinated run. The |
| 14 | +reference implementation is `codeanalyzer-ts` (`packaging/python/` + `.github/workflows/release.yml`) |
| 15 | +— mirror it for any new analyzer. |
| 16 | + |
| 17 | +1. **Thin PyPI package `codeanalyzer-<lang>`** — one platform-tagged wheel per OS/arch, each |
| 18 | + carrying the prebuilt binary under `_bin/` and exposing `bin_path()`. This is what the Python SDK |
| 19 | + `pip`-depends on. |
| 20 | +2. **Raw binaries as GitHub Release assets**, with a SHA256 checksum per artifact — for consumers |
| 21 | + that can't `pip install`, and what the TypeScript SDK downloads directly. |
| 22 | +3. **A Homebrew formula** (`Formula/codeanalyzer-<lang>.rb`) pushed to the shared tap |
| 23 | + `codellm-devkit/homebrew-tap`, so end users get `brew install codeanalyzer-<lang>`. |
| 24 | + |
| 25 | +**Two wheel shapes.** Compiled-binary analyzers (TS, Go, Rust, C++, JVM) ship a platform-tagged |
| 26 | +wheel (`py3-none-<platform>`) carrying the binary, no usable sdist. A **Python** analyzer (e.g. |
| 27 | +`codeanalyzer-python`) is the exception: the wheel carries importable Python code, is platform- |
| 28 | +independent, and is imported in-process — no binary, no `bin_path()`. |
| 29 | + |
| 30 | +**Build strategy** is a per-language decision, not a detail: single-host cross-compilation (Bun, |
| 31 | +Go, Rust — one CI job emits every platform binary) when the toolchain cross-compiles cleanly, or a |
| 32 | +native-runner build matrix (GraalVM `native-image`, clang with per-target sysroots — one CI leg per |
| 33 | +`(os, arch)` fanning in to one publish job) when it can't. State which one applies in the analyzer |
| 34 | +README's Architecture & Tooling section. |
| 35 | + |
| 36 | +### The release workflow, in order |
| 37 | + |
| 38 | +Trigger on `v*.*.*` tags (+ `workflow_dispatch`); `permissions: contents: write` (+ `id-token: write` |
| 39 | +for PyPI OIDC): |
| 40 | + |
| 41 | +1. Verify the tag version matches the analyzer manifest; gate on the analyzer's own test suite |
| 42 | + **first** — on failure, delete the just-pushed tag (`git push --delete origin <tag>`) so a broken |
| 43 | + build leaves no half-released version behind. |
| 44 | +2. Build the platform wheels (single-host: one job runs the per-target loop; matrix: one leg per |
| 45 | + `(os, arch)`, then a fan-in job). |
| 46 | +3. `auditwheel repair` Linux wheels to a manylinux policy (cover musllinux or document it |
| 47 | + unsupported); emit a SHA256 checksum per artifact. |
| 48 | +4. **Smoke-test every wheel before publishing** — install on a clean runner, run |
| 49 | + `codeanalyzer-<lang> --help` or a tiny fixture analysis. |
| 50 | +5. Extract raw binaries into `release-bins/`. |
| 51 | +6. Publish the GitHub Release with the binaries + checksums as assets. |
| 52 | +7. Publish the wheels to PyPI as `codeanalyzer-<lang>` — Trusted Publishing (OIDC), no long-lived |
| 53 | + token; `skip-existing: true` so a re-run is idempotent. |
| 54 | +8. **Separate `homebrew` job** (`needs: release`, fed `release-bins/` via artifact — not a rebuild): |
| 55 | + generate the formula, push it to `codellm-devkit/homebrew-tap` with a `HOMEBREW_TAP_TOKEN` PAT. |
| 56 | + Isolating this job matters: it's the step most likely to fail (cross-repo PAT), and if it rode |
| 57 | + inside the publish job a token failure would mark an otherwise-successful PyPI+Release run red |
| 58 | + and unrecoverable-without-a-new-tag. |
| 59 | + |
| 60 | +For a **Python analyzer**, drop steps 2–5 (compile/extract) and publish one pure wheel + sdist. |
| 61 | +Ship no sdist for compiled analyzers, or one whose build fails with a clear "no prebuilt binary for |
| 62 | +this platform" message — never let pip silently fall back to a from-source compile. |
| 63 | + |
| 64 | +Tag the release `vX.Y.Z` with real notes: a hand-curated *Keep a Changelog* block (`### Added` / |
| 65 | +`### Changed` — mark **BREAKING** inline / `### Fixed`) plus an auto-generated "Detailed Changes" |
| 66 | +block. Don't ship a release with an empty body. |
| 67 | + |
| 68 | +## SDK release: pin bump, then its own release |
| 69 | + |
| 70 | +An SDK never re-publishes an analyzer's artifact — it depends on it. Once an analyzer release |
| 71 | +exists (PyPI + Release binaries, above), the SDK's release is a two-step act: |
| 72 | + |
| 73 | +1. **Bump the pin** to the just-released version — the Python SDK's `pyproject.toml` |
| 74 | + `dependencies` (`codeanalyzer-<lang>==X.Y.Z`) and its `[tool.backend-versions]` table; the |
| 75 | + TypeScript SDK's equivalent pin to the same tag. This is its own PR, reviewed and merged before |
| 76 | + any tag. |
| 77 | +2. **Cut the SDK's own release** the same tag-triggered way: a `chore(release): X.Y.Z` PR bumps the |
| 78 | + SDK's own version + changelog (this alone ships nothing), then `git tag vX.Y.Z && git push |
| 79 | + origin vX.Y.Z` triggers the SDK's release workflow — its own test suite, its own PyPI/npm |
| 80 | + publish, and (if wired) a docs-repo dispatch to regenerate the API reference. A version bump PR |
| 81 | + with no tag pushed is a no-op release-wise; don't mistake merging it for shipping. |
| 82 | + |
| 83 | +A pin bump with no analyzer release behind it, or an analyzer release with no consumer ever bumping |
| 84 | +the pin, both leave the fix unreachable by users — "the fix is released" and "the fix is |
| 85 | +consumable" are different claims; the pin is the gate between them. |
| 86 | + |
| 87 | +## Version lockstep (the one rule that bites) |
| 88 | + |
| 89 | +Keep the version identical across all of these for a given analyzer release, or pip resolves a |
| 90 | +mismatched binary: |
| 91 | + |
| 92 | +- the analyzer manifest (`package.json` version, or the build version for Go/Rust/JVM); |
| 93 | +- `packaging/python/pyproject.toml`'s `project.version` **and** `__init__.py`'s `__version__`; |
| 94 | +- the Python SDK pin — both `dependencies` and `[tool.backend-versions]`; |
| 95 | +- the TypeScript SDK pin — the Release tag/binary filename it downloads; |
| 96 | +- the Homebrew formula's `version` and asset URLs/checksums — the release workflow regenerates this |
| 97 | + every tag; don't hand-edit it. |
| 98 | + |
| 99 | +## How each SDK consumes the artifact |
| 100 | + |
| 101 | +- **Python SDK** — resolves the binary in priority order: `analysis_backend_path` → |
| 102 | + `$CODEANALYZER_<LANG>_BIN` → `codeanalyzer_<lang>.bin_path()` (the pip package) → an in-tree |
| 103 | + bundled fallback. (Python analyzer: `import codeanalyzer_<lang>`, run in-process.) |
| 104 | +- **TypeScript SDK** — cannot consume a PyPI wheel; resolves `analysisBackendPath` → |
| 105 | + `$CODEANALYZER_<LANG>_BIN` → the GitHub Release asset binary. |
| 106 | + |
| 107 | +This is the contract `cldk-sdk-frontend` wires against — publish accordingly, and don't change the |
| 108 | +resolution order without updating both sides. |
0 commit comments