name: release on: push: # run only against tags tags: - '*' permissions: contents: write env: GH_REPO: ${{ github.repository }} GO_VERSION: '1.26.4' jobs: prepare-release: runs-on: ubuntu-latest steps: - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail release_notes_file="$(mktemp)" current_notes_file="$(mktemp)" updated_notes_file="$(mktemp)" trap 'rm -f "$release_notes_file" "$current_notes_file" "$updated_notes_file"' EXIT cat > "$release_notes_file" <<'EOF' ## Linux release assets - `CLIProxyAPI__linux_.tar.gz` is the default Linux build. It supports dynamic library plugins and is built against a GLIBC 2.17 baseline. - `CLIProxyAPI__linux__no-plugin.tar.gz` is the portable Linux build for musl-based or older systems such as OpenWrt. It does not support dynamic library plugins. EOF if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then gh release edit "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" else gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes fi gh release view "$GITHUB_REF_NAME" --json body -q .body > "$current_notes_file" { cat "$release_notes_file" printf '\n' awk ' /^$/ { skip = 1; next } /^$/ { skip = 0; next } !skip { print } ' "$current_notes_file" } > "$updated_notes_file" gh release edit "$GITHUB_REF_NAME" --notes-file "$updated_notes_file" build-hosted: name: build ${{ matrix.target }} needs: prepare-release runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - target: darwin-amd64 runner: macos-15-intel goos: darwin goarch: amd64 asset_arch: amd64 archive_format: tar.gz - target: darwin-arm64 runner: macos-15 goos: darwin goarch: arm64 asset_arch: aarch64 archive_format: tar.gz - target: windows-amd64 runner: windows-latest goos: windows goarch: amd64 asset_arch: amd64 archive_format: zip - target: windows-arm64 runner: windows-11-arm goos: windows goarch: arm64 asset_arch: aarch64 archive_format: zip steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Refresh models catalog shell: bash run: | set -euo pipefail git fetch --depth 1 https://github.com/router-for-me/models.git main git show FETCH_HEAD:models.json > internal/registry/models/models.json - name: Fetch tags shell: bash run: git fetch --force --tags - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true - uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: go-${{ runner.os }}-${{ runner.arch }}-${{ matrix.target }}-${{ hashFiles('go.sum') }} restore-keys: | go-${{ runner.os }}-${{ runner.arch }}-${{ matrix.target }}- go-${{ runner.os }}-${{ runner.arch }}- - name: Generate Build Metadata shell: bash run: | set -euo pipefail echo "RELEASE_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" echo "COMMIT=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV" echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" - name: Build archive shell: bash env: TARGET: ${{ matrix.target }} GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} ASSET_ARCH: ${{ matrix.asset_arch }} ARCHIVE_FORMAT: ${{ matrix.archive_format }} run: | set -euo pipefail binary_name="cli-proxy-api" if [[ "$GOOS" == "windows" ]]; then binary_name="cli-proxy-api.exe" fi archive_dir="dist/${TARGET}/archive" archive_name="CLIProxyAPI_${RELEASE_VERSION}_${GOOS}_${ASSET_ARCH}.${ARCHIVE_FORMAT}" rm -rf "dist/${TARGET}" mkdir -p "$archive_dir" CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" go build \ -ldflags="-s -w -X main.Version=${RELEASE_VERSION} -X main.Commit=${COMMIT} -X main.BuildDate=${BUILD_DATE}" \ -o "$archive_dir/$binary_name" ./cmd/server/ cp LICENSE README.md README_CN.md config.example.yaml "$archive_dir/" if [[ "$ARCHIVE_FORMAT" == "zip" ]]; then powershell -NoProfile -Command "Compress-Archive -Path '${archive_dir}/*' -DestinationPath 'dist/${archive_name}' -Force" else tar -C "$archive_dir" -czf "dist/$archive_name" "$binary_name" LICENSE README.md README_CN.md config.example.yaml fi - uses: actions/upload-artifact@v4 with: name: ${{ matrix.target }} path: dist/CLIProxyAPI_* if-no-files-found: error - name: Upload release assets shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail shopt -s nullglob assets=(dist/CLIProxyAPI_*.tar.gz dist/CLIProxyAPI_*.zip) if [[ ${#assets[@]} -eq 0 ]]; then printf 'expected archive assets, found %s\n' "${#assets[@]}" >&2 printf '%s\n' "${assets[@]}" >&2 exit 1 fi gh release upload "$GITHUB_REF_NAME" "${assets[@]}" --clobber - name: Refresh release checksums continue-on-error: true shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT gh release download "$GITHUB_REF_NAME" \ --pattern 'CLIProxyAPI_*.tar.gz' \ --pattern 'CLIProxyAPI_*.zip' \ --dir "$tmp_dir" \ --clobber ( cd "$tmp_dir" shopt -s nullglob archives=(CLIProxyAPI_*.tar.gz CLIProxyAPI_*.zip) if [[ ${#archives[@]} -eq 0 ]]; then echo "No release archives found" exit 0 fi if command -v sha256sum >/dev/null 2>&1; then sha256sum "${archives[@]}" | sort -k2 > checksums.txt else shasum -a 256 "${archives[@]}" | sort -k2 > checksums.txt fi ) gh release upload "$GITHUB_REF_NAME" "$tmp_dir/checksums.txt" --clobber build-linux-glibc: name: build linux-${{ matrix.goarch }} glibc needs: prepare-release runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - target: linux-amd64 runner: ubuntu-latest goarch: amd64 asset_arch: amd64 manylinux_image: quay.io/pypa/manylinux2014_x86_64 - target: linux-arm64 runner: ubuntu-24.04-arm goarch: arm64 asset_arch: aarch64 manylinux_image: quay.io/pypa/manylinux2014_aarch64 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Refresh models catalog shell: bash run: | set -euo pipefail git fetch --depth 1 https://github.com/router-for-me/models.git main git show FETCH_HEAD:models.json > internal/registry/models/models.json - name: Fetch tags shell: bash run: git fetch --force --tags - name: Generate Build Metadata shell: bash run: | set -euo pipefail echo "RELEASE_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" echo "COMMIT=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV" echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" - name: Build archive shell: bash env: TARGET: ${{ matrix.target }} GOARCH: ${{ matrix.goarch }} ASSET_ARCH: ${{ matrix.asset_arch }} MANYLINUX_IMAGE: ${{ matrix.manylinux_image }} run: | set -euo pipefail archive_dir="dist/${TARGET}/archive" archive_name="CLIProxyAPI_${RELEASE_VERSION}_linux_${ASSET_ARCH}.tar.gz" rm -rf "dist/${TARGET}" mkdir -p "$archive_dir" docker run --rm \ -v "$PWD:/src" \ -w /src \ -e GO_VERSION \ -e GOARCH \ -e RELEASE_VERSION \ -e COMMIT \ -e BUILD_DATE \ "$MANYLINUX_IMAGE" \ bash -euo pipefail -c ' go_archive="go${GO_VERSION}.linux-${GOARCH}.tar.gz" curl -fsSL "https://go.dev/dl/${go_archive}" -o "/tmp/${go_archive}" rm -rf /usr/local/go tar -C /usr/local -xzf "/tmp/${go_archive}" export PATH="/usr/local/go/bin:${PATH}" CGO_ENABLED=1 GOOS=linux GOARCH="${GOARCH}" go build -buildvcs=false \ -ldflags="-s -w -X main.Version=${RELEASE_VERSION} -X main.Commit=${COMMIT} -X main.BuildDate=${BUILD_DATE}" \ -o "'"$archive_dir"'/cli-proxy-api" ./cmd/server/ glibc_versions="$(readelf --version-info "'"$archive_dir"'/cli-proxy-api" | sed -n "s/.*Name: GLIBC_\([0-9.]*\).*/\1/p" | sort -Vu)" if [[ -n "${glibc_versions}" ]]; then printf "GLIBC versions:\n%s\n" "${glibc_versions}" max_glibc="$(printf "%s\n" "${glibc_versions}" | sort -V | tail -n 1)" if [[ "$(printf "2.17\n%s\n" "${max_glibc}" | sort -V | tail -n 1)" != "2.17" ]]; then printf "linux ${GOARCH} binary requires GLIBC_%s, expected GLIBC_2.17 or older\n" "${max_glibc}" >&2 exit 1 fi fi ' cp LICENSE README.md README_CN.md config.example.yaml "$archive_dir/" tar -C "$archive_dir" -czf "dist/$archive_name" cli-proxy-api LICENSE README.md README_CN.md config.example.yaml - uses: actions/upload-artifact@v4 with: name: ${{ matrix.target }} path: dist/CLIProxyAPI_* if-no-files-found: error - name: Upload release assets shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail shopt -s nullglob assets=(dist/CLIProxyAPI_*.tar.gz) if [[ ${#assets[@]} -eq 0 ]]; then printf 'expected archive assets, found %s\n' "${#assets[@]}" >&2 printf '%s\n' "${assets[@]}" >&2 exit 1 fi gh release upload "$GITHUB_REF_NAME" "${assets[@]}" --clobber - name: Refresh release checksums continue-on-error: true shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT gh release download "$GITHUB_REF_NAME" \ --pattern 'CLIProxyAPI_*.tar.gz' \ --pattern 'CLIProxyAPI_*.zip' \ --dir "$tmp_dir" \ --clobber ( cd "$tmp_dir" shopt -s nullglob archives=(CLIProxyAPI_*.tar.gz CLIProxyAPI_*.zip) if [[ ${#archives[@]} -eq 0 ]]; then echo "No release archives found" exit 0 fi if command -v sha256sum >/dev/null 2>&1; then sha256sum "${archives[@]}" | sort -k2 > checksums.txt else shasum -a 256 "${archives[@]}" | sort -k2 > checksums.txt fi ) gh release upload "$GITHUB_REF_NAME" "$tmp_dir/checksums.txt" --clobber build-linux-no-plugin: name: build linux-${{ matrix.goarch }} no-plugin needs: prepare-release runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - target: linux-amd64-no-plugin goarch: amd64 asset_arch: amd64 - target: linux-arm64-no-plugin goarch: arm64 asset_arch: aarch64 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Refresh models catalog shell: bash run: | set -euo pipefail git fetch --depth 1 https://github.com/router-for-me/models.git main git show FETCH_HEAD:models.json > internal/registry/models/models.json - name: Fetch tags shell: bash run: git fetch --force --tags - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true - uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: go-linux-no-plugin-${{ matrix.goarch }}-${{ hashFiles('go.sum') }} restore-keys: | go-linux-no-plugin-${{ matrix.goarch }}- go-linux-no-plugin- - name: Generate Build Metadata shell: bash run: | set -euo pipefail echo "RELEASE_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" echo "COMMIT=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV" echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" - name: Build archive shell: bash env: TARGET: ${{ matrix.target }} GOARCH: ${{ matrix.goarch }} ASSET_ARCH: ${{ matrix.asset_arch }} run: | set -euo pipefail archive_dir="dist/${TARGET}/archive" archive_name="CLIProxyAPI_${RELEASE_VERSION}_linux_${ASSET_ARCH}_no-plugin.tar.gz" rm -rf "dist/${TARGET}" mkdir -p "$archive_dir" CGO_ENABLED=0 GOOS=linux GOARCH="$GOARCH" go build -buildvcs=false \ -ldflags="-s -w -X main.Version=${RELEASE_VERSION} -X main.Commit=${COMMIT} -X main.BuildDate=${BUILD_DATE}" \ -o "$archive_dir/cli-proxy-api" ./cmd/server/ if readelf -l "$archive_dir/cli-proxy-api" | grep -q 'Requesting program interpreter'; then readelf -l "$archive_dir/cli-proxy-api" >&2 echo "no-plugin linux binary must not require a dynamic interpreter" >&2 exit 1 fi cp LICENSE README.md README_CN.md config.example.yaml "$archive_dir/" tar -C "$archive_dir" -czf "dist/$archive_name" cli-proxy-api LICENSE README.md README_CN.md config.example.yaml - uses: actions/upload-artifact@v4 with: name: ${{ matrix.target }} path: dist/CLIProxyAPI_* if-no-files-found: error - name: Upload release assets shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail shopt -s nullglob assets=(dist/CLIProxyAPI_*.tar.gz) if [[ ${#assets[@]} -eq 0 ]]; then printf 'expected archive assets, found %s\n' "${#assets[@]}" >&2 printf '%s\n' "${assets[@]}" >&2 exit 1 fi gh release upload "$GITHUB_REF_NAME" "${assets[@]}" --clobber - name: Refresh release checksums continue-on-error: true shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT gh release download "$GITHUB_REF_NAME" \ --pattern 'CLIProxyAPI_*.tar.gz' \ --pattern 'CLIProxyAPI_*.zip' \ --dir "$tmp_dir" \ --clobber ( cd "$tmp_dir" shopt -s nullglob archives=(CLIProxyAPI_*.tar.gz CLIProxyAPI_*.zip) if [[ ${#archives[@]} -eq 0 ]]; then echo "No release archives found" exit 0 fi if command -v sha256sum >/dev/null 2>&1; then sha256sum "${archives[@]}" | sort -k2 > checksums.txt else shasum -a 256 "${archives[@]}" | sort -k2 > checksums.txt fi ) gh release upload "$GITHUB_REF_NAME" "$tmp_dir/checksums.txt" --clobber build-freebsd: name: build freebsd-${{ matrix.goarch }} needs: prepare-release runs-on: ubuntu-latest env: TARGET: freebsd-${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }} ASSET_ARCH: ${{ matrix.asset_arch }} strategy: fail-fast: false matrix: include: - goarch: amd64 asset_arch: amd64 - goarch: arm64 asset_arch: aarch64 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Refresh models catalog run: | git fetch --depth 1 https://github.com/router-for-me/models.git main git show FETCH_HEAD:models.json > internal/registry/models/models.json - name: Fetch tags run: git fetch --force --tags - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache: true - uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: go-freebsd-${{ matrix.goarch }}-${{ hashFiles('go.sum') }} restore-keys: | go-freebsd-${{ matrix.goarch }}- go-freebsd- - name: Generate Build Metadata id: metadata run: | set -euo pipefail release_version="${GITHUB_REF_NAME#v}" commit="$(git rev-parse --short HEAD)" build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "RELEASE_VERSION=$release_version" >> "$GITHUB_ENV" echo "COMMIT=$commit" >> "$GITHUB_ENV" echo "BUILD_DATE=$build_date" >> "$GITHUB_ENV" echo "release_version=$release_version" >> "$GITHUB_OUTPUT" echo "commit=$commit" >> "$GITHUB_OUTPUT" echo "build_date=$build_date" >> "$GITHUB_OUTPUT" - name: Install FreeBSD cross-build dependencies run: | set -euo pipefail rm -rf "dist/${TARGET}" sudo apt-get update sudo apt-get install -y clang lld wget - name: Build FreeBSD binary timeout-minutes: 45 uses: go-cross/cgo-actions@v1 with: dir: . packages: ./cmd/server/ targets: ${{ env.TARGET }} out-dir: dist/${{ env.TARGET }}/bin output: cli-proxy-api flags: >- -ldflags=-s -w -X main.Version=${{ steps.metadata.outputs.release_version }} -X main.Commit=${{ steps.metadata.outputs.commit }} -X main.BuildDate=${{ steps.metadata.outputs.build_date }} - name: Package FreeBSD archive shell: bash run: | set -euo pipefail archive_dir="dist/${TARGET}/archive" archive_name="CLIProxyAPI_${RELEASE_VERSION}_freebsd_${ASSET_ARCH}.tar.gz" mkdir -p "$archive_dir" echo "Packaging ${archive_name}" cp "dist/${TARGET}/bin/cli-proxy-api" "$archive_dir/cli-proxy-api" cp LICENSE README.md README_CN.md config.example.yaml "$archive_dir/" tar -C "$archive_dir" -czf "dist/$archive_name" cli-proxy-api LICENSE README.md README_CN.md config.example.yaml - uses: actions/upload-artifact@v4 with: name: freebsd-${{ matrix.goarch }} path: dist/CLIProxyAPI_* if-no-files-found: error - name: Upload release assets shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail shopt -s nullglob assets=(dist/CLIProxyAPI_*.tar.gz) if [[ ${#assets[@]} -eq 0 ]]; then printf 'expected archive assets, found %s\n' "${#assets[@]}" >&2 printf '%s\n' "${assets[@]}" >&2 exit 1 fi gh release upload "$GITHUB_REF_NAME" "${assets[@]}" --clobber - name: Refresh release checksums continue-on-error: true shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT gh release download "$GITHUB_REF_NAME" \ --pattern 'CLIProxyAPI_*.tar.gz' \ --pattern 'CLIProxyAPI_*.zip' \ --dir "$tmp_dir" \ --clobber ( cd "$tmp_dir" shopt -s nullglob archives=(CLIProxyAPI_*.tar.gz CLIProxyAPI_*.zip) if [[ ${#archives[@]} -eq 0 ]]; then echo "No release archives found" exit 0 fi if command -v sha256sum >/dev/null 2>&1; then sha256sum "${archives[@]}" | sort -k2 > checksums.txt else shasum -a 256 "${archives[@]}" | sort -k2 > checksums.txt fi ) gh release upload "$GITHUB_REF_NAME" "$tmp_dir/checksums.txt" --clobber publish-checksums: runs-on: ubuntu-latest if: always() needs: - build-hosted - build-linux-glibc - build-linux-no-plugin - build-freebsd steps: - uses: actions/download-artifact@v4 with: path: dist merge-multiple: true - name: Publish final checksums env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail cd dist shopt -s nullglob archives=(CLIProxyAPI_*.tar.gz CLIProxyAPI_*.zip) if [[ ${#archives[@]} -eq 0 ]]; then echo "No release archives found" exit 0 fi sha256sum "${archives[@]}" | sort -k2 > checksums.txt gh release upload "$GITHUB_REF_NAME" checksums.txt --clobber