code2graph 0.0.0-beta.3

Purpose-neutral code-graph extraction: source files → symbols, references, and cross-file edges. Tree-sitter based, no storage opinion.
Documentation
name: Release

# Tag-driven release. Push a tag `vX.Y.Z` (or a prerelease `vX.Y.Z-beta.N`) whose
# base version matches `[workspace.package] version` in Cargo.toml. One synced
# version drives crates.io (code2graph), PyPI (code2graph-rs) and npm (code2graph-rs).
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
          # Intel macOS hosted runners (macos-13) are retired; cross-compile the
          # x86_64 wheel on an Apple Silicon runner (the target is set above).
          - 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"
          # tree-sitter grammars' generated parser.c uses C99 for-loop init and
          # some scanners use C11 static_assert; the manylinux cross-GCC defaults
          # to C89. Force C11 inside the build container (linux/docker only).
          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
          # Intel macOS runners are retired; cross-compile x86_64 on Apple Silicon.
          - 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
        # tree-sitter grammars need C11 (for-loop init / static_assert); the napi
        # aarch64 cross-GCC defaults to C89. Harmless on the modern gnu/clang/zig
        # targets; omitted on Windows, whose MSVC rejects -std=gnu11.
        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' }}