From 388fd52f8c81503ea15291fd68235addef4f36d7 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Wed, 30 Sep 2026 10:12:36 +0200 Subject: [PATCH] Add CI/release workflows, install.sh, maturin packaging, README updates Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01DXG5T8iM4nb28TDrpEX17w --- .github/workflows/ci.yml | 43 ++++++++++ .github/workflows/release.yml | 146 ++++++++++++++++++++++++++++++++++ README.md | 12 +++ install.sh | 27 +++++++ pyproject.toml | 21 +++++ 5 files changed, 249 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100755 install.sh create mode 100644 pyproject.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..28f6746 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +# Pushes to main run this via release.yml's `test` job (workflow_call), which +# gates the auto-release on it -- so no separate `push: main` trigger here. +on: + pull_request: + workflow_call: + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --workspace --locked + + # Proves the exact install path users get: build the wheel and + # `uv tool install` it, then run the installed binary. + wheel-smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: astral-sh/setup-uv@v9.0.0 + - run: uvx maturin build --release --out dist + - run: uv tool install dist/*.whl + - run: duplicatecode --version + - run: duplicatecode scan dataset --skip-tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b37e268 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,146 @@ +name: Release + +# Auto-release: every push to main runs the full CI suite, then publishes iff +# Cargo.toml's [workspace.package] version has no matching `vX.Y.Z` tag yet. +# A release is just "bump the version in a PR and merge it". Publishes: +# - wheels + sdist to PyPI -> `uv tool install duplicatecode` +# - standalone binaries (+ SHA256SUMS) on the GitHub Release -> install.sh +on: + push: + branches: [main] + workflow_dispatch: + inputs: + dry_run: + description: "Report whether a release would happen without building/publishing anything" + type: boolean + default: false + +# Two quick merges must not race each other into publishing the same version. +concurrency: + group: release + cancel-in-progress: false + +jobs: + test: + uses: ./.github/workflows/ci.yml + + plan: + name: Detect version bump + needs: test + runs-on: ubuntu-latest + outputs: + release: ${{ steps.check.outputs.release }} + version: ${{ steps.check.outputs.version }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - id: check + run: | + VERSION=$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/') + if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then + RELEASE=false + else + RELEASE=true + fi + echo "release=$RELEASE" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "duplicatecode: release=$RELEASE version=$VERSION" + + # Every target builds natively on a matching runner (tree-sitter compiles C, + # so no cross-compilation). Linux wheels are manylinux for portability. + build: + needs: plan + if: needs.plan.outputs.release == 'true' && (github.event_name != 'workflow_dispatch' || inputs.dry_run != true) + strategy: + matrix: + include: + - { name: linux-x86_64, runner: ubuntu-latest, target: x86_64-unknown-linux-gnu, manylinux: auto } + - { name: linux-aarch64, runner: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, manylinux: auto } + - { name: macos-x86_64, runner: macos-15-intel, target: x86_64-apple-darwin, manylinux: "off" } + - { name: macos-aarch64, runner: macos-latest, target: aarch64-apple-darwin, manylinux: "off" } + - { name: windows-x86_64, runner: windows-latest, target: x86_64-pc-windows-msvc, manylinux: "off" } + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v7 + - uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux }} + args: --release --out dist + - uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.name }} + path: dist + + sdist: + needs: plan + if: needs.plan.outputs.release == 'true' && (github.event_name != 'workflow_dispatch' || inputs.dry_run != true) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: dist + + publish: + needs: [plan, build, sdist] + runs-on: ubuntu-latest + environment: pypi + permissions: + contents: write # push the vX.Y.Z tag + create the GitHub Release + id-token: write # PyPI trusted publishing (OIDC) -- no stored API token + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist + # A re-run after a partial failure must not fail on files that + # already uploaded -- PyPI rejects re-uploads of the same filename. + skip-existing: true + - name: Package standalone binaries from the wheels + run: | + mkdir -p assets tmp + for whl in dist/*.whl; do + case "$whl" in + *manylinux*x86_64*) target=x86_64-unknown-linux-gnu ;; + *manylinux*aarch64*) target=aarch64-unknown-linux-gnu ;; + *macosx*x86_64*) target=x86_64-apple-darwin ;; + *macosx*arm64*) target=aarch64-apple-darwin ;; + *win_amd64*) target=x86_64-pc-windows-msvc ;; + *) echo "unknown wheel platform: $whl" >&2; exit 1 ;; + esac + rm -rf tmp/* + unzip -q -j "$whl" '*.data/scripts/*' -d tmp + if [ "$target" = x86_64-pc-windows-msvc ]; then + (cd tmp && zip -q "../assets/duplicatecode-$target.zip" duplicatecode.exe) + else + chmod +x tmp/duplicatecode + tar -czf "assets/duplicatecode-$target.tar.gz" -C tmp duplicatecode + fi + done + (cd assets && sha256sum * > SHA256SUMS) + cp dist/*.whl assets/ + ls -l assets + - name: Tag release + run: | + git tag "v${{ needs.plan.outputs.version }}" + git push origin "v${{ needs.plan.outputs.version }}" + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="v${{ needs.plan.outputs.version }}" + gh release create "$TAG" assets/* --title "duplicatecode $TAG" --generate-notes diff --git a/README.md b/README.md index b2e3852..1c4eb0f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,18 @@ checked against existing source. - `crates/duplicatecode-cli` – `duplicatecode` CLI - `dataset/` – benchmark: coding tasks + diverging implementations (see `dataset/README.md`) +## Install + +```sh +uv tool install duplicatecode # from PyPI (prebuilt binary, no Rust needed) +uvx duplicatecode scan . # or run without installing +curl -fsSL https://raw.githubusercontent.com/bmsuisse/duplicatecode/main/install.sh | sh # standalone binary +``` + +Binaries for Linux/macOS/Windows (x86_64, plus aarch64 on Linux/macOS) are also attached to each +[GitHub Release](https://github.com/bmsuisse/duplicatecode/releases). Releases are automatic: bump +`version` in `[workspace.package]` in `Cargo.toml`, merge to `main`, and CI tags and publishes it. + ## Usage ```sh diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..c8fff03 --- /dev/null +++ b/install.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Installs the latest duplicatecode release binary into ~/.local/bin (or $INSTALL_DIR). +# Usage: curl -fsSL https://raw.githubusercontent.com/bmsuisse/duplicatecode/main/install.sh | sh +# Prefer `uv tool install duplicatecode` if you have uv. +set -eu + +REPO="bmsuisse/duplicatecode" +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) target=x86_64-unknown-linux-gnu ;; + Linux-aarch64 | Linux-arm64) target=aarch64-unknown-linux-gnu ;; + Darwin-x86_64) target=x86_64-apple-darwin ;; + Darwin-arm64) target=aarch64-apple-darwin ;; + *) echo "unsupported platform: $(uname -s)-$(uname -m)" >&2; exit 1 ;; +esac + +url="https://github.com/$REPO/releases/latest/download/duplicatecode-$target.tar.gz" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +echo "Downloading $url" +curl -fsSL "$url" | tar -xz -C "$tmp" +mkdir -p "$INSTALL_DIR" +install -m 755 "$tmp/duplicatecode" "$INSTALL_DIR/duplicatecode" +echo "Installed to $INSTALL_DIR/duplicatecode" +case ":$PATH:" in *":$INSTALL_DIR:"*) ;; *) echo "Note: add $INSTALL_DIR to your PATH" ;; esac diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2e1a215 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["maturin>=1.7,<2"] +build-backend = "maturin" + +[project] +name = "duplicatecode" +dynamic = ["version"] +description = "Static (LLM-free) detection of duplicate/similar code in Python and TypeScript" +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Rust", + "Environment :: Console", +] + +# Ships the Rust CLI as a binary in a py3-none- wheel, so +# `uv tool install duplicatecode` needs neither Rust nor a specific Python. +[tool.maturin] +bindings = "bin" +manifest-path = "crates/duplicatecode-cli/Cargo.toml"