name: CI on: push: branches: [main] tags: ["v*"] pull_request: branches: [main] env: CARGO_TERM_COLOR: always jobs: frontend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '20' cache: 'npm' cache-dependency-path: crates/proxy/admin-ui/package-lock.json - name: Install dependencies working-directory: crates/proxy/admin-ui run: npm ci --legacy-peer-deps - name: TypeScript check working-directory: crates/proxy/admin-ui run: npx tsc --noEmit - name: Lint working-directory: crates/proxy/admin-ui run: npm run lint - name: Build working-directory: crates/proxy/admin-ui run: npm run build - name: Upload dist uses: actions/upload-artifact@v7 with: name: admin-ui-dist path: crates/proxy/admin-ui/dist/ test: needs: frontend runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Download dist uses: actions/download-artifact@v8 with: name: admin-ui-dist path: crates/proxy/admin-ui/dist/ - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 - name: Check formatting run: cargo fmt --check - name: Clippy run: cargo clippy -- -D warnings - name: Build run: cargo build - name: Test run: cargo test # Optimizer ONNX scorer (opt-in feature). Compile + lint + model-free lib tests so # the feature-gated code cannot rot. NOT `cargo test` on the whole workspace: the # parity integration test (optimize-scorer/tests/parity.rs) is intentionally not # #[ignore] and panics without the ~170MB model artifact, so only the lib unit tests # (which need no artifact) run here. `ort` downloads an onnxruntime binary at build. - name: Clippy (optimizer-onnx) run: cargo clippy -p anyllm_proxy --features optimizer-onnx -- -D warnings - name: Test optimizer-onnx (lib only, no model artifact) run: cargo test -p anyllm_optimize_scorer --features onnx --lib - name: Security audit run: | cargo install cargo-audit --locked --quiet cargo audit build-release: name: Build (${{ matrix.target }}) needs: test if: startsWith(github.ref, 'refs/tags/v') strategy: fail-fast: false matrix: include: # Linux x86_64 - os: ubuntu-latest target: x86_64-unknown-linux-gnu binary: anyllm-proxy deb_arch: amd64 # Linux ARM64 (native GitHub runner) - os: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu binary: anyllm-proxy deb_arch: arm64 # macOS Apple Silicon - os: macos-latest target: aarch64-apple-darwin binary: anyllm-proxy # macOS Intel - os: macos-latest target: x86_64-apple-darwin binary: anyllm-proxy # Windows x86_64 - os: windows-2025 target: x86_64-pc-windows-msvc binary: anyllm-proxy.exe runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - name: Download dist uses: actions/download-artifact@v8 with: name: admin-ui-dist path: crates/proxy/admin-ui/dist/ - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.target }} - name: Build run: cargo build --release -p anyllm_proxy --target ${{ matrix.target }} - name: Upload artifact uses: actions/upload-artifact@v7 with: name: anyllm_proxy-${{ matrix.target }} path: target/${{ matrix.target }}/release/${{ matrix.binary }} - name: Build deb package if: matrix.deb_arch != '' run: | cargo install cargo-deb --locked --quiet cargo deb -p anyllm_proxy --no-build --no-strip --target ${{ matrix.target }} - name: Upload deb artifact if: matrix.deb_arch != '' uses: actions/upload-artifact@v7 with: name: anyllm-proxy-deb-${{ matrix.deb_arch }} path: target/${{ matrix.target }}/debian/*.deb test-deb: name: Test deb (${{ matrix.arch }}) needs: build-release if: startsWith(github.ref, 'refs/tags/v') strategy: fail-fast: false matrix: include: - arch: amd64 os: ubuntu-latest - arch: arm64 os: ubuntu-24.04-arm runs-on: ${{ matrix.os }} steps: - name: Download deb uses: actions/download-artifact@v8 with: name: anyllm-proxy-deb-${{ matrix.arch }} path: ./deb - name: Inspect package run: | dpkg-deb --info ./deb/*.deb dpkg-deb --contents ./deb/*.deb - name: Lint package run: | sudo apt-get update -qq sudo apt-get install -y -qq lintian lintian --no-tag-display-limit ./deb/*.deb || true - name: Install package run: | sudo dpkg -i ./deb/*.deb || true sudo apt-get install -f -y - name: Verify binary run: | test -x /usr/bin/anyllm-proxy file /usr/bin/anyllm-proxy | grep -q "ELF" - name: Verify systemd unit run: | systemd-analyze verify /lib/systemd/system/anyllm-proxy.service - name: Verify postinst artifacts run: | getent passwd anyllm test -d /var/lib/anyllm stat -c '%U:%G' /var/lib/anyllm | grep -q 'anyllm:anyllm' - name: Verify config file run: | test -f /etc/default/anyllm-proxy create-release: name: Create GitHub Release needs: [test-deb] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v6 - name: Extract changelog section id: changelog run: | VERSION="${{ github.ref_name }}" VERSION="${VERSION#v}" # Extract lines between the matching version header and the next version header. NOTES=$(awk "/^## \[${VERSION}\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md) if [ -z "$NOTES" ]; then NOTES="See CHANGELOG.md for details." fi printf '%s' "$NOTES" > release_notes.txt - name: Create release env: GH_TOKEN: ${{ github.token }} run: | gh release create "${{ github.ref_name }}" \ --repo "${{ github.repository }}" \ --notes-file release_notes.txt \ --title "anyllm-proxy ${{ github.ref_name }}" \ || echo "Release already exists, continuing" release-assets: name: Upload release assets needs: [create-release, build-release] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: contents: write steps: - name: Download amd64 deb uses: actions/download-artifact@v8 with: name: anyllm-proxy-deb-amd64 path: ./debs - name: Download arm64 deb uses: actions/download-artifact@v8 with: name: anyllm-proxy-deb-arm64 path: ./debs - name: Upload debs to GitHub Release env: GH_TOKEN: ${{ github.token }} run: | ls -la ./debs/ gh release upload "${{ github.ref_name }}" ./debs/*.deb --repo "${{ github.repository }}" --clobber # Linux + Windows binaries are built by build-release but only uploaded as # CI artifacts; package them into release archives (tar.gz / zip) matching # the macOS tarball naming produced by brew-release. - name: Download linux x86_64 binary uses: actions/download-artifact@v8 with: name: anyllm_proxy-x86_64-unknown-linux-gnu path: ./bins/linux-x86_64 - name: Download linux arm64 binary uses: actions/download-artifact@v8 with: name: anyllm_proxy-aarch64-unknown-linux-gnu path: ./bins/linux-arm64 - name: Download windows x86_64 binary uses: actions/download-artifact@v8 with: name: anyllm_proxy-x86_64-pc-windows-msvc path: ./bins/windows-x86_64 - name: Package binary archives run: | VERSION="${{ github.ref_name }}" VERSION="${VERSION#v}" chmod +x ./bins/linux-x86_64/anyllm-proxy ./bins/linux-arm64/anyllm-proxy mkdir -p dist tar -czf "dist/anyllm-proxy-${VERSION}-linux-x86_64.tar.gz" -C ./bins/linux-x86_64 anyllm-proxy tar -czf "dist/anyllm-proxy-${VERSION}-linux-arm64.tar.gz" -C ./bins/linux-arm64 anyllm-proxy ( cd ./bins/windows-x86_64 && zip -q "${GITHUB_WORKSPACE}/dist/anyllm-proxy-${VERSION}-windows-x86_64.zip" anyllm-proxy.exe ) ls -la dist/ - name: Upload binary archives to GitHub Release env: GH_TOKEN: ${{ github.token }} run: | gh release upload "${{ github.ref_name }}" dist/*.tar.gz dist/*.zip --repo "${{ github.repository }}" --clobber publish: name: Publish to crates.io needs: [test, build-release] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 # Publish in dependency order. --no-verify skips re-building from the # packed tarball; the test job already verified the build. # Sleeps give the crates.io index time to propagate before dependents publish. # exit 101 = version already exists; the per-step shell swallows it so # re-runs are idempotent without masking real failures. - name: Publish anyllm_translate run: cargo publish -p anyllm_translate || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for index propagation run: sleep 30 - name: Publish anyllm_providers run: cargo publish -p anyllm_providers --no-verify || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for index propagation run: sleep 30 - name: Publish anyllm_client run: cargo publish -p anyllm_client --no-verify || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for index propagation run: sleep 30 - name: Publish anyllm_batch_engine run: cargo publish -p anyllm_batch_engine --no-verify || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for index propagation run: sleep 30 - name: Publish anyllm_pxpipe run: cargo publish -p anyllm_pxpipe --no-verify || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for index propagation run: sleep 30 - name: Publish anyllm_rtk run: cargo publish -p anyllm_rtk --no-verify || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for index propagation run: sleep 30 - name: Publish anyllm_optimize_core run: cargo publish -p anyllm_optimize_core --no-verify || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for index propagation run: sleep 30 - name: Publish anyllm_optimize_passes run: cargo publish -p anyllm_optimize_passes --no-verify || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for index propagation run: sleep 30 - name: Publish anyllm_optimize_scorer run: cargo publish -p anyllm_optimize_scorer --no-verify || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - name: Wait for index propagation run: sleep 30 - name: Publish anyllm_proxy run: cargo publish -p anyllm_proxy --no-verify || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; } env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} brew-release: name: Homebrew tap release needs: [create-release] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: contents: write steps: - name: Download arm64 macOS binary uses: actions/download-artifact@v8 with: name: anyllm_proxy-aarch64-apple-darwin path: ./macos/arm64 - name: Download x86_64 macOS binary uses: actions/download-artifact@v8 with: name: anyllm_proxy-x86_64-apple-darwin path: ./macos/x86_64 - name: Create versioned tarballs id: tarballs run: | VERSION="${{ github.ref_name }}" VERSION="${VERSION#v}" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" chmod +x ./macos/arm64/anyllm-proxy ./macos/x86_64/anyllm-proxy mkdir -p dist tar -czf "dist/anyllm-proxy-${VERSION}-macos-arm64.tar.gz" -C ./macos/arm64 anyllm-proxy tar -czf "dist/anyllm-proxy-${VERSION}-macos-x86_64.tar.gz" -C ./macos/x86_64 anyllm-proxy - name: Upload macOS tarballs to GitHub Release env: GH_TOKEN: ${{ github.token }} run: | VERSION="${{ steps.tarballs.outputs.version }}" gh release upload "${{ github.ref_name }}" \ "dist/anyllm-proxy-${VERSION}-macos-arm64.tar.gz" \ "dist/anyllm-proxy-${VERSION}-macos-x86_64.tar.gz" \ --repo "${{ github.repository }}" --clobber - name: Compute SHA256 id: sha run: | VERSION="${{ steps.tarballs.outputs.version }}" ARM64=$(sha256sum "dist/anyllm-proxy-${VERSION}-macos-arm64.tar.gz" | cut -d' ' -f1) X86=$(sha256sum "dist/anyllm-proxy-${VERSION}-macos-x86_64.tar.gz" | cut -d' ' -f1) echo "arm64=${ARM64}" >> "$GITHUB_OUTPUT" echo "x86_64=${X86}" >> "$GITHUB_OUTPUT" - name: Update Homebrew tap env: HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} run: | if [ -z "${HOMEBREW_TAP_TOKEN}" ]; then echo "HOMEBREW_TAP_TOKEN not set, skipping Homebrew tap update" exit 0 fi VERSION="${{ steps.tarballs.outputs.version }}" ARM64_SHA="${{ steps.sha.outputs.arm64 }}" X86_SHA="${{ steps.sha.outputs.x86_64 }}" git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/whit3rabbit/homebrew-tap.git" cd homebrew-tap { printf '%s\n' 'cask "anyllm-proxy" do' printf '%s\n' ' arch arm: "arm64", intel: "x86_64"' printf '%s\n' '' printf " version \"%s\"\n" "${VERSION}" printf " sha256 arm: \"%s\",\n" "${ARM64_SHA}" printf " intel: \"%s\"\n" "${X86_SHA}" printf '%s\n' '' printf '%s\n' ' url "https://github.com/whit3rabbit/anyllm-proxy/releases/download/v#{version}/anyllm-proxy-#{version}-macos-#{arch}.tar.gz"' printf '%s\n' ' name "anyllm-proxy"' printf '%s\n' ' desc "HTTP proxy translating Anthropic Messages API and OpenAI Chat Completions to any backend"' printf '%s\n' ' homepage "https://github.com/whit3rabbit/anyllm-proxy"' printf '%s\n' '' printf '%s\n' ' binary "anyllm-proxy"' printf '%s\n' 'end' } > Casks/anyllm-proxy.rb git config user.email "github-actions[bot]@users.noreply.github.com" git config user.name "github-actions[bot]" git add Casks/anyllm-proxy.rb git diff --staged --quiet || git commit -m "chore: update anyllm-proxy to ${VERSION}" git push