name: Release
on:
push:
tags: ["v*"]
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
validate-version:
name: Validate version tag
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Validate tag against workspace version
id: version
run: |
TAG="${GITHUB_REF_NAME}"
TAG_VERSION="${TAG#v}"
CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 \
| jq -r '.packages[] | select(.name == "code2graph") | .version')
if [[ ! "$TAG_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)(-[a-zA-Z]+\.[0-9]+)?$ ]]; then
echo "::error::Invalid tag '$TAG'. Expected vX.Y.Z or vX.Y.Z-label.N"
exit 1
fi
TAG_BASE="${BASH_REMATCH[1]}"
CARGO_BASE=$(echo "$CARGO_VERSION" | grep -oP '^\d+\.\d+\.\d+')
if [[ "$TAG_BASE" != "$CARGO_BASE" ]]; then
echo "::error::Tag base '$TAG_BASE' != Cargo.toml version '$CARGO_BASE'"
exit 1
fi
if [[ "$TAG_VERSION" == *-* ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
echo "Releasing $TAG_VERSION"
ci:
name: Test
needs: validate-version
uses: ./.github/workflows/test.yml
publish-crates:
name: Publish to crates.io
needs: [validate-version, ci]
runs-on: ubuntu-latest
environment: crates.io
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Stamp version
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" Cargo.toml
- name: Publish code2graph
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
if curl -sf -H "User-Agent: code2graph-ci (github.com/nodedb-lab/code2graph)" \
"https://crates.io/api/v1/crates/code2graph/$VERSION" > /dev/null 2>&1; then
echo "code2graph@$VERSION already published — skipping"
else
cargo publish -p code2graph --allow-dirty --no-verify
fi
build-sdist:
name: Build sdist
needs: validate-version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Stamp version
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
PYPI_VERSION=$(echo "$VERSION" | sed -E 's/-alpha\./a/; s/-beta\./b/; s/-rc\./rc/')
sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" Cargo.toml
sed -i "s/^version = .*/version = \"$PYPI_VERSION\"/" bindings/python/pyproject.toml
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist -m bindings/python/Cargo.toml
- uses: actions/upload-artifact@v7
with:
name: wheels-sdist
path: dist
build-wheels:
name: Build wheel (${{ matrix.target }})
needs: validate-version
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
manylinux: auto
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
manylinux: auto
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
manylinux: musllinux_1_2
- os: macos-latest
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v6
- name: Stamp version
shell: bash
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
PYPI_VERSION=$(echo "$VERSION" | sed -E 's/-alpha\./a/; s/-beta\./b/; s/-rc\./rc/')
sed -i.bak "0,/^version = \".*\"/s//version = \"$VERSION\"/" Cargo.toml && rm -f Cargo.toml.bak
sed -i.bak "s/^version = .*/version = \"$PYPI_VERSION\"/" bindings/python/pyproject.toml && rm -f bindings/python/pyproject.toml.bak
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux || 'auto' }}
args: --release --out dist -m bindings/python/Cargo.toml
sccache: "true"
docker-options: ${{ contains(matrix.target, 'linux') && '-e CFLAGS=-std=gnu11' || '' }}
- uses: actions/upload-artifact@v7
with:
name: wheels-${{ matrix.target }}
path: dist
publish-pypi:
name: Publish to PyPI
needs: [validate-version, build-wheels, build-sdist]
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
pattern: wheels-*
path: dist
merge-multiple: true
- name: Publish (Trusted Publishing / OIDC)
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
build-node:
name: Build addon (${{ matrix.target }})
needs: validate-version
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
- target: aarch64-unknown-linux-gnu
runner: ubuntu-latest
napi_cross: true
- target: x86_64-unknown-linux-musl
runner: ubuntu-latest
zig: true
- target: x86_64-apple-darwin
runner: macos-latest
- target: aarch64-apple-darwin
runner: macos-latest
- target: x86_64-pc-windows-msvc
runner: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version: "22"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
prefix-key: node-${{ matrix.target }}
- name: Install zig (musl cross-compile)
if: matrix.zig
run: |
pip install ziglang
cargo install --locked cargo-zigbuild
- name: Install napi cross-toolchain (linux aarch64)
if: matrix.napi_cross
run: npm install -g @napi-rs/cross-toolchain
- name: Install deps
working-directory: bindings/node
run: npm ci
- name: Build addon
working-directory: bindings/node
env:
CFLAGS: ${{ !contains(matrix.target, 'windows') && '-std=gnu11' || '' }}
run: npx napi build --platform --release --target ${{ matrix.target }} ${{ matrix.zig && '-x' || '' }} ${{ matrix.napi_cross && '--use-napi-cross' || '' }}
- uses: actions/upload-artifact@v7
with:
name: node-${{ matrix.target }}
path: bindings/node/*.node
publish-npm:
name: Publish to npm
needs: [validate-version, build-node, publish-crates]
runs-on: ubuntu-latest
environment: npm
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
- name: Install deps
working-directory: bindings/node
run: npm ci
- name: Download addon artifacts
uses: actions/download-artifact@v8
with:
path: bindings/node/artifacts
pattern: node-*
merge-multiple: true
- name: Stamp version
working-directory: bindings/node
run: |
VERSION="${{ needs.validate-version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
- name: Assemble platform packages
working-directory: bindings/node
run: |
npx napi create-npm-dirs --npm-dir ./npm
npx napi artifacts --output-dir ./artifacts --npm-dir ./npm
npx napi prepublish -t npm --npm-dir ./npm --skip-optional-publish
- name: Publish (idempotent, with provenance)
working-directory: bindings/node
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: "true"
run: |
publish_pkg() {
local dir="$1" name ver
name=$(jq -r .name "$dir/package.json")
ver=$(jq -r .version "$dir/package.json")
if npm view "$name@$ver" version >/dev/null 2>&1; then
echo "$name@$ver already published — skipping"
else
npm publish "$dir" --access public
fi
}
for dir in npm/*/; do publish_pkg "$dir"; done
publish_pkg .
github-release:
name: GitHub release
needs: [validate-version, publish-crates, publish-pypi, publish-npm]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
prerelease: ${{ needs.validate-version.outputs.is_prerelease == 'true' }}