Skip to content

chore(release): 0.4.3 #14

chore(release): 0.4.3

chore(release): 0.4.3 #14

Workflow file for this run

name: Release (binaries + Python wheels)
# Builds the self-contained cants binary for every supported
# platform and publishes:
# 1. the raw binaries as GitHub Release assets (for analysis_backend_path /
# $CODEANALYZER_TS_BIN / download-on-first-use use cases), and
# 2. platform-tagged Python wheels to PyPI as `codeanalyzer-typescript`.
#
# Bun cross-compiles all targets from one host, so a single Linux job suffices.
on:
push:
tags:
- "v*.*.*"
workflow_dispatch: {}
permissions:
contents: write # create GitHub Release + delete tag on failure
id-token: write # PyPI Trusted Publishing (OIDC) -- no API token needed
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
# Derive the release version from the tag (vX.Y.Z -> X.Y.Z) and require it
# to match package.json. This keeps the PyPI wheel version, the GitHub
# Release tag, and the npm version in lockstep -- a mismatch fails fast
# rather than silently publishing the wrong version to PyPI.
- name: Determine and verify version
id: ver
run: |
pkgjson="$(node -p "require('./package.json').version")"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
version="${GITHUB_REF#refs/tags/v}"
if [[ "$version" != "$pkgjson" ]]; then
echo "::error::Tag version ($version) != package.json version ($pkgjson). Bump package.json or fix the tag."
exit 1
fi
else
version="$pkgjson" # workflow_dispatch: no tag, fall back to package.json
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo ">>> Releasing version $version"
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install analyzer dependencies
run: bun install
# Keep the generated docs in lockstep with the binary being released: regenerate the
# README `cants --help` block and the Neo4j schema.json from source, and commit them back to
# main. Releases are cut from main HEAD, so this fast-forwards; it's best-effort if main moved.
- name: Sync generated docs before release (README --help + Neo4j schema)
if: startsWith(github.ref, 'refs/tags/')
run: |
bun run gen:readme
bun run gen:schema
if git diff --quiet README.md schema.neo4j.json; then
echo "Generated docs already current."
else
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add README.md schema.neo4j.json
git commit -m "docs: sync README --help and Neo4j schema for v${{ steps.ver.outputs.version }}"
git push origin HEAD:main || echo "::warning::could not push doc sync to main (diverged?)"
fi
# ----- test gate: a broken build must not produce a release -----
- name: Typecheck and test
id: test
continue-on-error: true
run: |
bun run typecheck
# Tolerate a repo with no test files (bun test exits 1 when it finds none),
# but still fail the gate on real test failures when tests DO exist.
if find . -path ./node_modules -prune -o \
\( -name '*.test.ts' -o -name '*.test.tsx' -o -name '*.test.js' -o -name '*.test.jsx' \
-o -name '*.spec.ts' -o -name '*.spec.tsx' -o -name '*.spec.js' -o -name '*.spec.jsx' \) \
-print | grep -q .; then
bun test
else
echo "No test files found — skipping bun test."
fi
- name: Delete tag on failure
if: steps.test.conclusion == 'failure' && startsWith(github.ref, 'refs/tags/')
run: |
echo "Tests failed. Deleting tag ${GITHUB_REF#refs/tags/}..."
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git push --delete origin "${GITHUB_REF#refs/tags/}"
exit 1
- name: Fail if tests failed (non-tag runs)
if: steps.test.conclusion == 'failure'
run: exit 1
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Python build tooling
# hatchling is the build backend; build_wheels.sh runs `python -m build
# --no-isolation`, so it must be present in this env (no auto-install).
run: python -m pip install --upgrade build wheel hatchling
- name: Build platform wheels (cross-compiles every target via Bun)
working-directory: packaging/python
env:
PKG_VERSION: ${{ steps.ver.outputs.version }}
run: ./build_wheels.sh
- name: Extract raw binaries from wheels (for GitHub Release)
working-directory: packaging/python
run: |
mkdir -p ../../release-bins
for whl in dist/*.whl; do
plat="$(basename "$whl" .whl | sed 's/.*-py3-none-//')"
tmp="$(mktemp -d)"
python -m zipfile -e "$whl" "$tmp"
# _bin/ now also holds schema.json, so select the binary by name (cants / cants.exe).
bin="$(find "$tmp/codeanalyzer_typescript/_bin" -type f -name 'cants*')"
ext=""; [[ "$bin" == *.exe ]] && ext=".exe"
cp "$bin" "../../release-bins/cants-${plat}${ext}"
done
ls -lh ../../release-bins
# The Neo4j schema contract is platform-independent and version-locked to this build.
# Publish it as a Release asset alongside the raw binaries (it also ships inside each wheel).
# generate_formula.sh reads only the named cants-<platform> assets, so this is harmless to it.
- name: Generate Neo4j schema contract (release asset)
run: bun run src/index.ts --emit schema > release-bins/schema.json
# Publish the cargo-dist-style install script so users can:
# curl --proto '=https' --tlsv1.2 -LsSf .../releases/latest/download/cants-installer.sh | sh
- name: Stage the install script (release asset)
run: cp packaging/install/cants-installer.sh release-bins/cants-installer.sh
- name: Build changelog (auto-generated from commits/PRs)
id: changelog
if: startsWith(github.ref, 'refs/tags/')
uses: mikepenz/release-changelog-builder-action@v5
with:
configuration: ".github/configuration.json"
failOnError: "false"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# cargo-dist-style notes: install one-liners + a download table, then the auto-generated
# changelog from the step above (kept). Indented code blocks avoid backticks in the heredoc.
- name: Compose release notes
id: notes
if: startsWith(github.ref, 'refs/tags/')
env:
VERSION: ${{ steps.ver.outputs.version }}
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
run: |
REPO="codellm-devkit/codeanalyzer-typescript"
BASE="https://github.com/$REPO/releases/download/v$VERSION"
cat > "$RUNNER_TEMP/RELEASE_BODY.md" <<EOF
## Install codeanalyzer-typescript v$VERSION
Shell script (prebuilt binary; macOS and Linux):
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/$REPO/releases/latest/download/cants-installer.sh | sh
Homebrew:
brew install codellm-devkit/homebrew-tap/codeanalyzer-typescript
PyPI:
pip install codeanalyzer-typescript==$VERSION
## Download
| File | Platform |
| --- | --- |
| [cants-macosx_11_0_arm64]($BASE/cants-macosx_11_0_arm64) | Apple Silicon macOS |
| [cants-macosx_10_12_x86_64]($BASE/cants-macosx_10_12_x86_64) | Intel macOS |
| [cants-manylinux2014_x86_64]($BASE/cants-manylinux2014_x86_64) | x86-64 Linux |
| [cants-manylinux2014_aarch64]($BASE/cants-manylinux2014_aarch64) | ARM64 Linux |
| [cants-win_amd64.exe]($BASE/cants-win_amd64.exe) | x64 Windows |
| [cants-installer.sh]($BASE/cants-installer.sh) | Shell installer |
| [schema.json]($BASE/schema.json) | Neo4j schema contract |
## Changelog
$CHANGELOG
EOF
echo "----- composed release body -----"; cat "$RUNNER_TEMP/RELEASE_BODY.md"
- name: Publish GitHub Release (raw binaries)
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: release-bins/*
body_path: ${{ runner.temp }}/RELEASE_BODY.md
# Auto-open a repo-level Discussion linked to this release, seeded with
# the same notes. Requires Discussions enabled and this category to exist.
discussion_category_name: Announcements
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Mirror the release announcement into the ORG-level discussions, which are
# backed by codellm-devkit/.github. GITHUB_TOKEN can't write cross-repo, so
# this uses a PAT/App token (ORG_DISCUSSIONS_TOKEN) with discussions:write
# on that repo, and posts via the createDiscussion GraphQL mutation. Reuses
# the already-composed RELEASE_BODY.md so org and repo posts stay identical.
- name: Announce in org-level discussions (codellm-devkit/.github)
if: startsWith(github.ref, 'refs/tags/')
continue-on-error: true # a failed org post must not fail an otherwise-good release
env:
GH_TOKEN: ${{ secrets.ORG_DISCUSSIONS_TOKEN }}
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -uo pipefail
OWNER="codellm-devkit"; REPO=".github"; CATEGORY="Announcements"
# The mutation needs GraphQL node IDs, not names — resolve them first.
RESP=$(gh api graphql \
-f query='query($o:String!,$r:String!){repository(owner:$o,name:$r){id discussionCategories(first:25){nodes{id name}}}}' \
-f o="$OWNER" -f r="$REPO") \
|| { echo "::warning::org discussion lookup failed — skipping org announcement."; exit 0; }
REPO_ID=$(echo "$RESP" | jq -r '.data.repository.id')
CAT_ID=$(echo "$RESP" | jq -r --arg c "$CATEGORY" '.data.repository.discussionCategories.nodes[]|select(.name==$c)|.id')
if [[ -z "$REPO_ID" || "$REPO_ID" == "null" || -z "$CAT_ID" ]]; then
echo "::warning::could not resolve $OWNER/$REPO discussion category '$CATEGORY' — skipping org announcement."
exit 0
fi
gh api graphql \
-f query='mutation($rid:ID!,$cid:ID!,$t:String!,$b:String!){createDiscussion(input:{repositoryId:$rid,categoryId:$cid,title:$t,body:$b}){discussion{url}}}' \
-f rid="$REPO_ID" -f cid="$CAT_ID" \
-f t="codeanalyzer-typescript v$VERSION" \
-f b="$(cat "$RUNNER_TEMP/RELEASE_BODY.md")"
- name: Publish wheels to PyPI (Trusted Publishing / OIDC)
if: startsWith(github.ref, 'refs/tags/')
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packaging/python/dist
# Make re-runs idempotent: a version already on PyPI is skipped rather
# than failing the whole job with a 400 (PyPI rejects duplicate uploads).
skip-existing: true
# Hand the raw binaries to the separate `homebrew` job below. Passing them
# as an artifact (rather than re-downloading the GitHub Release) keeps the
# formula checksums byte-identical to what was just published.
- name: Upload release binaries for the homebrew job
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: release-bins
path: release-bins/
# ----- Homebrew: regenerate the formula from the just-built binaries and push
# it to the shared tap. Split into its own job (needs: release) so a tap-push
# failure -- e.g. a missing HOMEBREW_TAP_TOKEN -- is isolated from the PyPI and
# GitHub Release steps above, and can be re-run on its own without re-uploading
# wheels. This is the non-Rust equivalent of what cargo-dist does for you.
homebrew:
needs: release
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Derive version from tag
id: ver
run: echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
- name: Download release binaries
uses: actions/download-artifact@v4
with:
name: release-bins
path: release-bins
- name: Generate Homebrew formula
env:
REPO: ${{ github.repository }}
VERSION: ${{ steps.ver.outputs.version }}
run: |
./packaging/homebrew/generate_formula.sh release-bins > codeanalyzer-typescript.rb
cat codeanalyzer-typescript.rb
- name: Push formula to codellm-devkit/homebrew-tap
env:
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} # PAT with write access to homebrew-tap
VERSION: ${{ steps.ver.outputs.version }}
run: |
git clone "https://x-access-token:${TAP_TOKEN}@github.com/codellm-devkit/homebrew-tap.git" tap
mkdir -p tap/Formula
cp codeanalyzer-typescript.rb tap/Formula/codeanalyzer-typescript.rb
cd tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/codeanalyzer-typescript.rb
git commit -m "codeanalyzer-typescript ${VERSION}" || { echo "no formula change"; exit 0; }
git push