Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/configuration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"categories": [
{ "title": "### 🚀 Features", "labels": ["feature"] },
{ "title": "### 🐛 Fixes", "labels": ["fix"] },
{ "title": "### ♻️ Refactoring", "labels": ["refactor"] },
{ "title": "### 📝 Documentation", "labels": ["docs"] },
{ "title": "### 🧪 Tests", "labels": ["test"] },
{ "title": "### 🔧 Maintenance", "labels": ["ci", "chore"] },
{ "title": "### 📦 Other", "labels": [] }
],
"ignore_labels": ["ignore-for-release"],
"sort": { "order": "ASC", "on_property": "mergedAt" },
"template": "#{{CHANGELOG}}",
"pr_template": "- #{{TITLE}} (##{{NUMBER}}) @#{{AUTHOR}}",
"empty_template": "- _No user-facing changes._",
"max_pull_requests": 200,
"max_back_track_time_days": 365
}
347 changes: 347 additions & 0 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
/analysis.json
/graph.cypher
/coverage.out
/release-bins/

# Packaging build outputs. The per-platform binary under _bin/ and the README
# copied in as the wheel's long description are both produced by
# packaging/python/build_wheels.sh and never committed.
/packaging/python/dist/
/packaging/python/README.md
/packaging/python/src/codeanalyzer_iac/_bin/*
!/packaging/python/src/codeanalyzer_iac/_bin/.gitignore
.DS_Store

# SCHEMA_DECISIONS.md is repository content. A global ignore of `.claude`
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build sync-schema test race vet schema-check fuzz-smoke test-live test-live-head
.PHONY: build wheels sync-schema test race vet schema-check fuzz-smoke test-live test-live-head

# VERSION is the one version the binary reports: --version prints it and every
# analysis document is stamped with it. Override it for a release build.
Expand All @@ -7,6 +7,13 @@ VERSION ?= 0.1.0-dev
build:
go build -ldflags "-X main.version=$(VERSION)" -o caniac ./cmd/codeanalyzer-iac

# The five platform-tagged PyPI wheels, cross-compiled from this host. VERSION
# must be a PEP 440 version here, not the 0.1.0-dev default, because it is the
# wheel version as well as the stamped main.version. Release CI sets it from the
# git tag; locally: `make wheels VERSION=0.1.0rc0`.
wheels:
PKG_VERSION=$(VERSION) packaging/python/build_wheels.sh

sync-schema:
cp schema.json internal/contract/schema.json
cp schema.neo4j.json internal/contract/schema.neo4j.json
Expand Down
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,26 @@ The root `schema.json` (2.0.0) and `schema.neo4j.json` (1.0.0) are copied
byte-for-byte from that revision. Run `make sync-schema` before testing after a
contract update.

## Install and build
## Install

Every release publishes the same self-contained `caniac` binary three ways. It
has no runtime dependency of any kind.

```sh
pip install codeanalyzer-iac # the platform wheel; puts `caniac` on PATH
brew install codellm-devkit/tap/codeanalyzer-iac
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/codellm-devkit/codeanalyzer-iac/releases/latest/download/caniac-installer.sh | sh
```

The PyPI package is the one CLDK's Python SDK depends on:
`codeanalyzer_iac.bin_path()` returns the bundled executable. The raw
per-platform binaries, a `SHA256SUMS` manifest and the version-locked
`schema.neo4j.json` are also attached to each [GitHub
Release](https://github.com/codellm-devkit/codeanalyzer-iac/releases) for direct
download.

## Build

```sh
make build # -o caniac, version stamped from VERSION (0.1.0-dev)
Expand All @@ -32,6 +51,23 @@ binary named `codeanalyzer-iac`, and the two behave identically. Building needs
the Go version declared in `go.mod` and nothing else. Running needs nothing at
all: the Helm renderer is the pinned `helm.sh/helm/v4` SDK, compiled in.

Nothing in the dependency tree needs cgo, so one host cross-compiles every
released target — `linux/amd64`, `linux/arm64`, `darwin/amd64`, `darwin/arm64`
and `windows/amd64` — with `CGO_ENABLED=0` and the stock toolchain. That is why
the release pipeline is a single job and why the Linux binaries are static
enough to carry a `manylinux_2_17` wheel tag. `make wheels VERSION=X.Y.Z`
reproduces all five wheels locally.

One version reaches every artifact: the git tag `vX.Y.Z` is the only source, and
it becomes `codeanalyzer_iac.__version__`, the `-X main.version` linker stamp,
what `caniac --version` prints, and `analyzer.version` in every emitted analysis
document. The release workflow fails before publishing if they disagree.

The tag must be an already normalized PEP 440 release version — `v0.1.0` or
`v0.1.0rc1`, never `v0.1.0-rc1` or `v0.1.0-dev`. The workflow rejects anything
else before it builds, and `make wheels` applies the same rule to `VERSION`, so
the wheel filename, the Homebrew `version` field and the tag cannot drift apart.

## Filesystem analysis

```sh
Expand Down Expand Up @@ -421,6 +457,7 @@ make test # go test ./... — offline, no cluster, no database required
make race # go test -race ./...
make schema-check # contract files, embedded copies and generated catalog agree
make fuzz-smoke # 10s over each parser fuzz target
make wheels VERSION=0.1.0rc0 # the five platform wheels the release publishes
```

`go test ./...` is network-independent and needs no services. The graph parity
Expand Down
88 changes: 88 additions & 0 deletions packaging/homebrew/generate_formula.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# Generate the Homebrew formula for the `caniac` (codeanalyzer-iac) binary.
#
# This is the non-Rust equivalent of what cargo-dist does automatically: it takes
# the per-platform binaries that the release already publishes as GitHub Release
# assets, computes their sha256, and emits a formula that downloads + installs the
# matching binary for the user's platform.
#
# It reads the binaries straight from the release-bins/ dir built by release.yml,
# so the checksums are guaranteed to match the bytes that were uploaded. The
# download URLs point at the GitHub Release assets of the SAME tag.
#
# Usage:
# REPO=codellm-devkit/codeanalyzer-iac VERSION=0.2.0 \
# ./generate_formula.sh ../../release-bins > codeanalyzer-iac.rb
#
set -euo pipefail

BINS_DIR="${1:?usage: generate_formula.sh <release-bins-dir>}"
REPO="${REPO:?set REPO, e.g. codellm-devkit/codeanalyzer-iac}"
VERSION="${VERSION:?set VERSION, e.g. 0.2.0}"
BASE_URL="https://github.com/${REPO}/releases/download/v${VERSION}"

# shasum on macOS, sha256sum on the Linux runner; both print "<digest> <file>".
sha() {
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | cut -d' ' -f1
else
sha256sum "$1" | cut -d' ' -f1
fi
}

# Map each release asset (named by Python wheel platform tag) to its Homebrew
# os/arch block. Windows is intentionally omitted -- Homebrew is macOS/Linux only.
asset_macos_arm="caniac-macosx_11_0_arm64"
asset_macos_intel="caniac-macosx_11_0_x86_64"
asset_linux_intel="caniac-manylinux_2_17_x86_64"
asset_linux_arm="caniac-manylinux_2_17_aarch64"

for a in "$asset_macos_arm" "$asset_macos_intel" "$asset_linux_intel" "$asset_linux_arm"; do
[[ -f "$BINS_DIR/$a" ]] || { echo "missing expected binary: $BINS_DIR/$a" >&2; exit 1; }
done

cat <<FORMULA
# This file is auto-generated by packaging/homebrew/generate_formula.sh on release.
# Do not edit by hand -- changes will be overwritten on the next tag.
class CodeanalyzerIac < Formula
desc "CLDK infrastructure-as-code analyzer (caniac) -- emits canonical CLDK analysis.json"
homepage "https://github.com/${REPO}"
version "${VERSION}"
license "Apache-2.0"

on_macos do
on_arm do
url "${BASE_URL}/${asset_macos_arm}"
sha256 "$(sha "$BINS_DIR/$asset_macos_arm")"
end
on_intel do
url "${BASE_URL}/${asset_macos_intel}"
sha256 "$(sha "$BINS_DIR/$asset_macos_intel")"
end
end

on_linux do
on_arm do
url "${BASE_URL}/${asset_linux_arm}"
sha256 "$(sha "$BINS_DIR/$asset_linux_arm")"
end
on_intel do
url "${BASE_URL}/${asset_linux_intel}"
sha256 "$(sha "$BINS_DIR/$asset_linux_intel")"
end
end

def install
# The release asset is a bare executable; Homebrew stages it under its
# original (platform-tagged) name. Install it as the canonical "caniac".
bin.install Dir["caniac-*"].first => "caniac"
end

test do
# caniac --version prints "caniac version <v>", the same string it stamps on
# analyzer.version in every analysis document.
assert_match "caniac version #{version}", shell_output("#{bin}/caniac --version")
end
end
FORMULA
71 changes: 71 additions & 0 deletions packaging/install/caniac-installer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/bin/sh
# caniac installer — downloads the prebuilt codeanalyzer-iac (`caniac`) binary for your
# platform from the GitHub Release and installs it. Mirrors the cargo-dist installer pattern.
#
# Usage:
# curl --proto '=https' --tlsv1.2 -LsSf https://github.com/codellm-devkit/codeanalyzer-iac/releases/latest/download/caniac-installer.sh | sh
#
# Environment overrides:
# CANIAC_INSTALL_DIR install location (default: ~/.local/bin)
# CANIAC_VERSION release tag, e.g. v0.3.0 (default: latest)
set -eu

REPO="codellm-devkit/codeanalyzer-iac"
INSTALL_DIR="${CANIAC_INSTALL_DIR:-$HOME/.local/bin}"
VERSION="${CANIAC_VERSION:-latest}"

os="$(uname -s)"
arch="$(uname -m)"

# Map the host platform to the published Release asset name (see packaging/python/build_wheels.sh
# targets and packaging/homebrew/generate_formula.sh).
case "$os" in
Darwin)
case "$arch" in
arm64 | aarch64) asset="caniac-macosx_11_0_arm64" ;;
x86_64) asset="caniac-macosx_11_0_x86_64" ;;
*) echo "caniac: unsupported macOS architecture: $arch" >&2; exit 1 ;;
esac
;;
Linux)
case "$arch" in
x86_64) asset="caniac-manylinux_2_17_x86_64" ;;
aarch64 | arm64) asset="caniac-manylinux_2_17_aarch64" ;;
*) echo "caniac: unsupported Linux architecture: $arch" >&2; exit 1 ;;
esac
;;
*)
echo "caniac: unsupported OS '$os'. Try: pip install codeanalyzer-iac" >&2
exit 1
;;
esac

if [ "$VERSION" = "latest" ]; then
url="https://github.com/$REPO/releases/latest/download/$asset"
else
url="https://github.com/$REPO/releases/download/$VERSION/$asset"
fi

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

echo "caniac: downloading $asset ($VERSION)..."
if command -v curl >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -fLsS "$url" -o "$tmp/caniac"
elif command -v wget >/dev/null 2>&1; then
wget -q "$url" -O "$tmp/caniac"
else
echo "caniac: need curl or wget to download" >&2
exit 1
fi

chmod +x "$tmp/caniac"
mkdir -p "$INSTALL_DIR"
mv "$tmp/caniac" "$INSTALL_DIR/caniac"
echo "caniac: installed to $INSTALL_DIR/caniac"

# PATH hint when the install dir isn't already on PATH.
case ":$PATH:" in
*":$INSTALL_DIR:"*) ;;
*) echo "caniac: add it to your PATH: export PATH=\"$INSTALL_DIR:\$PATH\"" ;;
esac
114 changes: 114 additions & 0 deletions packaging/python/build_wheels.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
#
# Build platform-tagged Python wheels for the caniac (codeanalyzer-iac) binary.
#
# For each target: cross-compile the binary with the Go toolchain, build a (pure)
# wheel with hatchling, then retag it from `py3-none-any` to the matching platform
# tag with `wheel tags`. The binary is python-agnostic, so each platform needs
# exactly one wheel (py3-none-<platform>), not one per Python version.
#
# Requirements on the build host:
# - go (the version in go.mod) -- cross-compiles all targets from
# one host because the analyzer is CGO-free (CGO_ENABLED=0)
# - python -m pip install build wheel hatchling twine
# (hatchling is the build backend; --no-isolation means it must be installed)
#
# Usage:
# ./build_wheels.sh # build all targets into ./dist
# twine upload dist/*.whl # publish
#
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$HERE/../.." && pwd)" # codeanalyzer-iac repo root (has go.mod)
# Version comes from the environment (the release workflow sets it from the git
# tag); the literal is only a local-dev fallback. It is written into __init__.py,
# which is hatch's single source of truth for the wheel version, and stamped into
# the binary as main.version, which is what `caniac --version` prints and what
# every analysis document carries as analyzer.version.
PKG_VERSION="${PKG_VERSION:-0.1.0}"

# The same shape the release workflow requires of a tag. It has to be an already
# normalized PEP 440 version, because the wheel filename below is spelled out
# rather than read back from hatchling -- 0.1.0-dev or 0.1.0-rc1 would be
# normalized to something else and the `wheel tags` call would miss the file.
if [[ ! "$PKG_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$ ]]; then
echo "PKG_VERSION='$PKG_VERSION' is not a normalized PEP 440 release version." >&2
echo "Use X.Y.Z, optionally with aN, bN or rcN: 0.1.0, 0.1.0rc1." >&2
echo "Hint: 'make wheels' needs VERSION set, e.g. make wheels VERSION=0.1.0rc1." >&2
exit 1
fi

WHEEL_STEM="codeanalyzer_iac-${PKG_VERSION}-py3-none-any.whl"
BIN_DIR="$HERE/src/codeanalyzer_iac/_bin"
INIT_PY="$HERE/src/codeanalyzer_iac/__init__.py"

# Remove built binaries from _bin/ but keep the tracked .gitignore (and the dir),
# so a local build leaves the working tree pristine.
clean_bin() { mkdir -p "$BIN_DIR"; find "$BIN_DIR" -mindepth 1 ! -name '.gitignore' -delete; }

# Stamp $PKG_VERSION into __init__.py for the build, restoring the original on
# exit so the working tree stays pristine (mirrors the _bin cleanup below).
ORIG_INIT="$(cat "$INIT_PY")" # $(...) strips the trailing newline; restore re-adds it
restore_init() { printf '%s\n' "$ORIG_INIT" > "$INIT_PY"; }
trap restore_init EXIT
python - "$INIT_PY" "$PKG_VERSION" <<'PY'
import re, sys
path, version = sys.argv[1], sys.argv[2]
text = open(path).read()
new, n = re.subn(r'__version__ = "[^"]*"', f'__version__ = "{version}"', text)
if n != 1:
raise SystemExit(f"expected exactly one __version__ assignment in {path}, found {n}")
open(path, "w").write(new)
print(f">>> stamped __version__ = {version}")
PY

# "GOOS/GOARCH" : "wheel platform tag". The Go binaries are static (CGO_ENABLED=0),
# so the Linux ones satisfy manylinux_2_17 with no shared-library dependencies at all.
TARGETS=(
"darwin/arm64:macosx_11_0_arm64"
"darwin/amd64:macosx_11_0_x86_64"
"linux/amd64:manylinux_2_17_x86_64"
"linux/arm64:manylinux_2_17_aarch64"
"windows/amd64:win_amd64"
)

rm -rf "$HERE/dist"
mkdir -p "$HERE/dist"

# The wheel's long description (the PyPI page) is the repo root README — copy it in so there is a
# single source of truth. It is gitignored and removed on exit (see cleanup) to keep the tree pristine.
cp "$REPO_ROOT/README.md" "$HERE/README.md"

for entry in "${TARGETS[@]}"; do
target="${entry%%:*}"
plat="${entry##*:}"
goos="${target%%/*}"
goarch="${target##*/}"
ext=""
[[ "$goos" == windows ]] && ext=".exe"

echo ">>> [$target] compiling -> wheel ($plat)"

clean_bin

# -trimpath keeps the build reproducible; -s -w drops the symbol table and DWARF,
# which is most of the binary size and nothing a released analyzer needs.
( cd "$REPO_ROOT" && CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" \
go build -trimpath -ldflags "-s -w -X main.version=$PKG_VERSION" \
-o "$BIN_DIR/caniac$ext" ./cmd/codeanalyzer-iac )

# Build a pure wheel (py3-none-any), then retag to the platform.
python -m build --wheel --no-isolation -o "$HERE/dist" "$HERE"
python -m wheel tags --remove --platform-tag "$plat" "$HERE/dist/$WHEEL_STEM"
done

# Clean the working binary + copied README so the tree stays pristine.
clean_bin
rm -f "$HERE/README.md"

echo
echo ">>> Built wheels:"
ls -lh "$HERE/dist"/*.whl
echo
echo "Publish with: twine upload $HERE/dist/*.whl"
Loading
Loading