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
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
146 changes: 146 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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-<platform> wheel, so
# `uv tool install duplicatecode` needs neither Rust nor a specific Python.
[tool.maturin]
bindings = "bin"
manifest-path = "crates/duplicatecode-cli/Cargo.toml"
Loading