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: - uses: actions/checkout@v6 with: fetch-depth: 0 fetch-tags: true - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail release_notes_file="$(mktemp)" generated_notes_file="$(mktemp)" changelog_entries_file="$(mktemp)" changelog_notes_file="$(mktemp)" updated_notes_file="$(mktemp)" trap 'rm -f "$release_notes_file" "$generated_notes_file" "$changelog_entries_file" "$changelog_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. ## FreeBSD release assets - `CLIProxyAPI__freebsd_aarch64_no-plugin.tar.gz` is the FreeBSD arm64 build. It is built without CGO and does not support dynamic library plugins. EOF git fetch --force --tags previous_tag="" if previous_tag_value="$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" 2>/dev/null)"; then previous_tag="$previous_tag_value" changelog_range="${previous_tag}..${GITHUB_REF_NAME}" else changelog_range="$GITHUB_REF_NAME" fi git log --reverse --pretty=format:'- %s (%h)' "$changelog_range" | grep -Ev '^- (docs:|test:)' > "$changelog_entries_file" || true gh api "repos/${GH_REPO}/releases/generate-notes" \ -f tag_name="$GITHUB_REF_NAME" \ --jq .body > "$generated_notes_file" if [[ ! -s "$generated_notes_file" ]]; then if [[ -n "$previous_tag" ]]; then printf '**Full Changelog**: https://github.com/%s/compare/%s...%s\n' "$GH_REPO" "$previous_tag" "$GITHUB_REF_NAME" > "$generated_notes_file" else printf '**Full Changelog**: https://github.com/%s/commits/%s\n' "$GH_REPO" "$GITHUB_REF_NAME" > "$generated_notes_file" fi fi { if [[ -s "$changelog_entries_file" ]]; then printf '## Changelog\n\n' cat "$changelog_entries_file" printf '\n\n' fi cat "$generated_notes_file" } > "$changelog_notes_file" { cat "$release_notes_file" printf '\n' cat "$changelog_notes_file" } > "$updated_notes_file" if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then gh release edit "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file "$updated_notes_file" else gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file "$updated_notes_file" fi 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 ${{ matrix.target }} needs: prepare-release runs-on: ubuntu-latest env: TARGET: ${{ matrix.target }} GOARCH: ${{ matrix.goarch }} ASSET_ARCH: ${{ matrix.asset_arch }} ASSET_SUFFIX: ${{ matrix.asset_suffix }} strategy: fail-fast: false matrix: include: - target: freebsd-amd64 goarch: amd64 asset_arch: amd64 asset_suffix: '' cgo_enabled: true - target: freebsd-arm64-no-plugin goarch: arm64 asset_arch: aarch64 asset_suffix: _no-plugin cgo_enabled: false 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: Prepare FreeBSD output shell: bash run: | set -euo pipefail rm -rf "dist/${TARGET}" - name: Install FreeBSD cross-build dependencies if: ${{ matrix.cgo_enabled }} run: | set -euo pipefail sudo apt-get update sudo apt-get install -y clang lld wget - name: Build FreeBSD binary with CGO if: ${{ matrix.cgo_enabled }} 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: Build FreeBSD no-plugin binary if: ${{ !matrix.cgo_enabled }} shell: bash run: | set -euo pipefail mkdir -p "dist/${TARGET}/bin" CGO_ENABLED=0 GOOS=freebsd GOARCH="$GOARCH" go build -buildvcs=false \ -ldflags="-s -w -X main.Version=${RELEASE_VERSION} -X main.Commit=${COMMIT} -X main.BuildDate=${BUILD_DATE}" \ -o "dist/${TARGET}/bin/cli-proxy-api" ./cmd/server/ - name: Package FreeBSD archive shell: bash run: | set -euo pipefail archive_dir="dist/${TARGET}/archive" archive_name="CLIProxyAPI_${RELEASE_VERSION}_freebsd_${ASSET_ARCH}${ASSET_SUFFIX}.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: ${{ 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 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