name: release
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: write packages: write
env:
CARGO_TERM_COLOR: always
REGISTRY_IMAGE: ghcr.io/jmcgrath207/conproxy
HELM_OCI: oci://ghcr.io/jmcgrath207/charts
jobs:
cross:
name: cross (${{ matrix.target }})
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
description: x86_64 Linux (musl, static)
- target: aarch64-unknown-linux-gnu
description: aarch64 Linux (glibc)
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross + host linker (build-scripts use .cargo/config.toml clang)
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends clang lld
cargo install cross --locked
- uses: Swatinem/rust-cache@v2
with:
shared-key: release-cross-${{ matrix.target }}-${{ runner.os }}
cache-on-failure: true
- name: Cross build (release bundle)
env:
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: clang
run: cross build --release --features release --target ${{ matrix.target }} --bin conproxy
- name: Upload cross artifact
uses: actions/upload-artifact@v4
with:
name: conproxy-${{ matrix.target }}
path: target/${{ matrix.target }}/release/conproxy
if-no-files-found: error
retention-days: 30
aarch64-smoke:
name: aarch64 smoke
needs: cross
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-24.04-arm
timeout-minutes: 10
steps:
- name: Download aarch64 artifact
uses: actions/download-artifact@v4
with:
name: conproxy-aarch64-unknown-linux-gnu
path: bin
- name: Smoke --version / --help
run: |
chmod +x bin/conproxy
bin/conproxy --version
bin/conproxy --help 2>&1 | head -20
container:
name: container (${{ matrix.platform }})
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
tag_suffix: linux-amd64
runner: ubuntu-latest
description: x86_64
- platform: linux/arm64
tag_suffix: linux-arm64
runner: ubuntu-24.04-arm
description: aarch64
steps:
- uses: actions/checkout@v4
- name: Set VERSION from tag
id: v
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build + push (per arch, multi-arch manifest assembled in publish job)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: ${{ matrix.platform }}
push: true
tags: |
${{ env.REGISTRY_IMAGE }}:${{ steps.v.outputs.VERSION }}-${{ matrix.tag_suffix }}
labels: |
org.opencontainers.image.title=conproxy
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ steps.v.outputs.VERSION }}
cache-from: type=gha,scope=container-${{ matrix.tag_suffix }}
cache-to: type=gha,scope=container-${{ matrix.tag_suffix }},mode=max
- name: Smoke image
run: |
docker run --rm ${{ env.REGISTRY_IMAGE }}:${{ steps.v.outputs.VERSION }}-${{ matrix.tag_suffix }} --version
docker run --rm ${{ env.REGISTRY_IMAGE }}:${{ steps.v.outputs.VERSION }}-${{ matrix.tag_suffix }} --help 2>&1 | head -20
container-publish:
name: container (manifest)
needs: container
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Set VERSION from tag
id: v
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create + push multi-arch manifest
run: |
docker buildx imagetools create \
--tag ${{ env.REGISTRY_IMAGE }}:${{ steps.v.outputs.VERSION }} \
--tag ${{ env.REGISTRY_IMAGE }}:v${{ steps.v.outputs.VERSION }} \
--tag ${{ env.REGISTRY_IMAGE }}:latest \
${{ env.REGISTRY_IMAGE }}:${{ steps.v.outputs.VERSION }}-linux-amd64 \
${{ env.REGISTRY_IMAGE }}:${{ steps.v.outputs.VERSION }}-linux-arm64
- name: Inspect manifest
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.v.outputs.VERSION }}
helm:
name: helm
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Set VERSION from tag
id: v
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Install helm
uses: azure/setup-helm@v4
with:
version: v3.16.2
- name: Stamp chart version + appVersion
working-directory: deploy/helm/conproxy
run: |
VERSION=${{ steps.v.outputs.VERSION }}
sed -i.bak -E "s/^version:.*/version: ${VERSION}/" Chart.yaml
sed -i.bak -E 's/^appVersion:.*/appVersion: "'"${VERSION}"'"/' Chart.yaml
rm -f Chart.yaml.bak
cat Chart.yaml
- name: helm lint
working-directory: deploy/helm/conproxy
run: helm lint .
- name: helm template (smoke)
working-directory: deploy/helm/conproxy
run: |
helm template test . --set hostIP=127.0.0.1 > /tmp/rendered.yaml
echo "--- rendered ---"; head -30 /tmp/rendered.yaml
- name: helm package
working-directory: deploy/helm/conproxy
run: |
helm package . --version ${{ steps.v.outputs.VERSION }} \
--app-version ${{ steps.v.outputs.VERSION }} -d dist/
ls -la dist/
- name: Log in to GHCR (helm)
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io \
--username ${{ github.actor }} --password-stdin
- name: helm push (OCI)
working-directory: deploy/helm/conproxy
run: |
helm push dist/conproxy-${{ steps.v.outputs.VERSION }}.tgz ${{ env.HELM_OCI }}
- name: Upload chart artifact
uses: actions/upload-artifact@v4
with:
name: conproxy-chart
path: deploy/helm/conproxy/dist/conproxy-${{ steps.v.outputs.VERSION }}.tgz
if-no-files-found: error
retention-days: 30
version-check:
name: version-check
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.v.outputs.VERSION }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Assert tag matches crate versions
id: v
run: |
TAG="${GITHUB_REF_NAME#v}"
ROOT=$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import json,sys; pkgs=json.load(sys.stdin)["packages"]; print(next(p["version"] for p in pkgs if p["name"]=="conproxy"))')
PY=$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import json,sys; pkgs=json.load(sys.stdin)["packages"]; print(next(p["version"] for p in pkgs if p["name"]=="conproxy-python"))')
SDK=$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import json,sys; pkgs=json.load(sys.stdin)["packages"]; print(next(p["version"] for p in pkgs if p["name"]=="conproxy-sdk"))')
echo "tag=$TAG root=$ROOT python=$PY sdk=$SDK"
test "$TAG" = "$ROOT" && test "$TAG" = "$PY" && test "$TAG" = "$SDK"
echo "VERSION=$TAG" >> "$GITHUB_OUTPUT"
crates-io:
name: crates-io
needs: version-check
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2
with:
shared-key: release-crates-${{ runner.os }}
cache-on-failure: true
- name: Dry-run publish
run: |
cargo publish --dry-run -p conproxy
cargo publish --dry-run -p conproxy-sdk
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
cargo publish -p conproxy --no-verify
cargo publish -p conproxy-sdk --no-verify
pypi-sdist:
name: pypi-sdist
needs: version-check
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
working-directory: sdk/python
- uses: actions/upload-artifact@v4
with:
name: pypi-sdist
path: sdk/python/dist/*
if-no-files-found: error
retention-days: 30
pypi-build:
name: pypi (${{ matrix.platform.name }})
needs: version-check
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ${{ matrix.platform.runner }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
platform:
- name: manylinux2014-x86_64
target: x86_64
runner: ubuntu-latest
manylinux: '2014'
- name: manylinux2014-aarch64
target: aarch64
runner: ubuntu-24.04-arm
manylinux: '2014'
- name: musllinux_1_2-x86_64
target: x86_64-unknown-linux-musl
runner: ubuntu-latest
manylinux: musllinux_1_2
- name: musllinux_1_2-aarch64
target: aarch64-unknown-linux-musl
runner: ubuntu-24.04-arm
manylinux: musllinux_1_2
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: ${{ matrix.platform.manylinux }}
args: --release --out dist
working-directory: sdk/python
sccache: 'true'
docker-options: >-
-e CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=cc
-e CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=
before-script-linux: |
set -euo pipefail
PROTOC_VERSION=28.3
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ZIP="protoc-${PROTOC_VERSION}-linux-x86_64.zip" ;;
aarch64) ZIP="protoc-${PROTOC_VERSION}-linux-aarch_64.zip" ;;
*) echo "unsupported arch ${ARCH}"; exit 1 ;;
esac
if command -v yum >/dev/null 2>&1; then
yum install -y unzip
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache unzip curl
elif command -v apt-get >/dev/null 2>&1; then
apt-get update
apt-get install -y --no-install-recommends unzip curl
fi
curl -fsSL -o "/tmp/${ZIP}" \
"https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${ZIP}"
unzip -o "/tmp/${ZIP}" -d /usr/local
rm -f "/tmp/${ZIP}"
protoc --version
- uses: actions/upload-artifact@v4
with:
name: pypi-wheel-${{ matrix.platform.name }}
path: sdk/python/dist/*
if-no-files-found: error
retention-days: 30
pypi-publish:
name: pypi-publish
needs: [pypi-build, pypi-sdist]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 15
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
path: dist
pattern: pypi-*
merge-multiple: true
- name: List packages
run: ls -la dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
github-release:
name: github-release
needs: [cross, helm, container-publish, pypi-build, pypi-sdist]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
- uses: actions/download-artifact@v4
with:
path: artifacts
pattern: conproxy-*
merge-multiple: false
- uses: actions/download-artifact@v4
with:
path: artifacts/pypi
pattern: pypi-*
merge-multiple: true
- name: List artifacts
run: find artifacts -maxdepth 3 -type f | sort
- name: Set VERSION
id: v
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Rename binaries for release assets
run: |
mv artifacts/conproxy-x86_64-unknown-linux-musl/conproxy \
artifacts/conproxy-x86_64-unknown-linux-musl/conproxy-x86_64-unknown-linux-musl
mv artifacts/conproxy-aarch64-unknown-linux-gnu/conproxy \
artifacts/conproxy-aarch64-unknown-linux-gnu/conproxy-aarch64-unknown-linux-gnu
chmod +x artifacts/conproxy-*/conproxy-*
- name: Resolve previous tag for compare link
id: prev
run: |
# Find the most recent semver tag (excluding the current one).
PREV=$(git tag --list 'v*' --sort=-v:refname \
| grep -v "^${GITHUB_REF_NAME}$" \
| head -n 1 || true)
if [ -n "$PREV" ]; then
echo "PREV=$PREV" >> "$GITHUB_OUTPUT"
echo "COMPARE=https://github.com/${{ github.repository }}/compare/${PREV}...${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
else
echo "PREV=" >> "$GITHUB_OUTPUT"
echo "COMPARE=" >> "$GITHUB_OUTPUT"
fi
- name: Create release + attach assets
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
generate_release_notes: true
prerelease: ${{ contains(github.ref_name, '-') }}
make_latest: ${{ !contains(github.ref_name, '-') }}
body: |
## conproxy ${{ steps.v.outputs.VERSION }}
${{ steps.prev.outputs.COMPARE && format('**Compare:** [{0}…{1}]({2})', steps.prev.outputs.PREV, github.ref_name, steps.prev.outputs.COMPARE) || '**First release.**' }}
**Stability**
- **Shipped:** exact cache, MCP suite, all backends except Pinecone / Milvus
- **Experimental:** Pinecone, Milvus, peer mesh (no mTLS — see README)
**Install**
```bash
# cargo (crates.io)
cargo install conproxy --locked --features release
# or: cargo add conproxy / cargo add conproxy-sdk
# pypi
pip install conproxy
# docker (multi-arch: amd64 + arm64)
docker pull ${{ env.REGISTRY_IMAGE }}:${{ steps.v.outputs.VERSION }}
# helm (OCI)
helm install conproxy ${{ env.HELM_OCI }}/conproxy \
--version ${{ steps.v.outputs.VERSION }}
```
**Docs:** [README](https://github.com/${{ github.repository }}#readme) · [Benchmarks](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/docs/benchmarks.md)
files: |
artifacts/conproxy-x86_64-unknown-linux-musl/conproxy-x86_64-unknown-linux-musl
artifacts/conproxy-aarch64-unknown-linux-gnu/conproxy-aarch64-unknown-linux-gnu
artifacts/conproxy-chart/conproxy-${{ steps.v.outputs.VERSION }}.tgz
artifacts/pypi/*
fail_on_unmatched_files: true