penguin/tinyproxy

An L4 proxy designed to act as a tiny transparent shim

.github/workflows/cmake-multi-platform.yml

raw ยท 17164 bytes

name: tinyproxy CI

on:
  workflow_dispatch:
  push:
    branches: ["master"]
    tags: ["v*"]
  pull_request:
    branches: ["master"]

concurrency:
  group: tinyproxy-${{ github.ref }}
  cancel-in-progress: false

env:
  ARTIFACT_RETENTION_DAYS: 30
  LIBEVENT_VERSION: 2.1.12-stable
  LIBEVENT_TARBALL: libevent-2.1.12-stable.tar.gz
  LIBEVENT_URL: https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz

jobs:
  libevent-source:
    name: Prepare libevent source
    runs-on: ubuntu-latest

    steps:
      - name: Cache libevent tarball
        id: cache-libevent-tarball
        uses: actions/cache@v4
        with:
          path: .cache/libevent/libevent-2.1.12-stable.tar.gz
          key: libevent-tarball-2.1.12-stable

      - name: Download libevent tarball
        if: steps.cache-libevent-tarball.outputs.cache-hit != 'true'
        run: |
          set -eu
          mkdir -p .cache/libevent
          curl -fL --retry 5 --retry-delay 2 --retry-all-errors \
            "$LIBEVENT_URL" \
            -o ".cache/libevent/$LIBEVENT_TARBALL"

      - name: Upload libevent tarball
        uses: actions/upload-artifact@v4
        with:
          name: libevent-tarball
          path: .cache/libevent/libevent-2.1.12-stable.tar.gz

  linux-musl-x86_64:
    name: Linux musl x86_64 static
    runs-on: ubuntu-latest
    needs: libevent-source
    container:
      image: &alpine_image alpine:3.23

    steps:
      - &checkout
        uses: actions/checkout@v4

      - &download_libevent_source
        name: Download libevent tarball
        uses: actions/download-artifact@v4
        with:
          name: libevent-tarball
          path: .cache/libevent

      - &extract_libevent
        name: Extract libevent source
        run: |
          set -eu
          rm -rf libevent
          mkdir -p libevent
          tar xf .cache/libevent/libevent-2.1.12-stable.tar.gz \
            -C libevent \
            --strip-components=1

      - &install_linux_deps
        name: Install deps
        run: |
          apk add --no-cache \
            build-base \
            python3 \
            strace \
            haproxy

      - &set_build_version
        name: Set Build Version
        id: set_build_version
        run: |
          if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
            BUILD_VER="${GITHUB_REF_NAME}"
          else
            BUILD_VER="dev-${GITHUB_SHA::12}"
          fi
          echo "BUILD_VER=${BUILD_VER}" >> "$GITHUB_ENV"
          echo "build_ver=${BUILD_VER}" >> "$GITHUB_OUTPUT"
          echo "Build version: ${BUILD_VER}"

      - &build_static
        name: Build
        shell: sh
        run: |
          set -eu

          mkdir -p dist/debug dist/prod

          make clean all \
            STATIC=1 \
            EXTRA_CFLAGS="-Og -g3 -Wpedantic" \
            LIBEVENT_SRC=./libevent \
            VERSION="${BUILD_VER}-debug"

          cp bin/tinyproxy dist/debug/tinyproxy

          make clean all \
            STATIC=1 \
            LIBEVENT_SRC=./libevent \
            VERSION="$BUILD_VER"

          cp bin/tinyproxy dist/prod/tinyproxy
          strip dist/prod/tinyproxy

      - &verify_static
        name: Verify static binary
        shell: sh
        run: |
          set -eu

          apk add --no-cache pax-utils

          file dist/prod/tinyproxy
          scanelf -n dist/prod/tinyproxy

          if scanelf -n dist/prod/tinyproxy | grep -q 'lib'; then
            echo "prod binary has dynamic library dependencies"
            exit 1
          fi

          file dist/debug/tinyproxy
          scanelf -n dist/debug/tinyproxy

          if scanelf -n dist/debug/tinyproxy | grep -q 'lib'; then
            echo "debug binary has dynamic library dependencies"
            exit 1
          fi

      - &test
        name: Test
        run: make test

      - &package_name
        name: Set package names
        shell: sh
        run: |
          os="$(printf '%s' '${{ runner.os }}' | tr '[:upper:]' '[:lower:]')"
          arch="$(printf '%s' '${{ runner.arch }}' | tr '[:upper:]' '[:lower:]')"

          echo "TINYPROXY_PACKAGE=tinyproxy-${os}-${arch}" >> "$GITHUB_ENV"

      - &package_unix
        name: Package artifact
        shell: sh
        run: |
          mkdir -p dist/tinyproxy
          cp dist/prod/tinyproxy dist/tinyproxy/
          cp -r examples dist/tinyproxy/

          tar -C dist -czf "${TINYPROXY_PACKAGE}.tar.gz" tinyproxy

          mkdir -p dist/tinyproxy-debug
          cp dist/debug/tinyproxy dist/tinyproxy-debug/
          cp -r examples dist/tinyproxy-debug/

          tar -C dist -czf "${TINYPROXY_PACKAGE}-debug.tar.gz" tinyproxy-debug

      - &upload_unix
        name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: ${{ env.TINYPROXY_PACKAGE }}
          path: |
            ${{ env.TINYPROXY_PACKAGE }}.tar.gz
            ${{ env.TINYPROXY_PACKAGE }}-debug.tar.gz
          if-no-files-found: error
          retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }}

  linux-musl-arm64:
    name: Linux musl arm64 static
    runs-on: ubuntu-24.04-arm
    needs: libevent-source
    container:
      image: *alpine_image

    permissions:
      contents: read

    steps:
      - uses: laverdet/alpine-arm64@7f0f72ee2f71eb2324e5888e8b6e42b1b53e6160
        if: runner.arch == 'ARM64'

      - *checkout
      - *download_libevent_source
      - *extract_libevent
      - *install_linux_deps
      - *set_build_version
      - *build_static
      - *verify_static
      - *test
      - *package_name
      - *package_unix
      - *upload_unix

  linux-riscv64:
    name: Linux riscv64 (test only)
    runs-on: ubuntu-latest
    continue-on-error: true

    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4
        with:
          platforms: riscv64

      - name: Build and test under riscv64
        run: |
          docker run --rm \
            --platform linux/riscv64 \
            -v "$PWD:/work" \
            -w /work \
            riscv64/debian:unstable \
            sh -euxc '
              apt-get update
              apt-get install -y --no-install-recommends \
                build-essential \
                libevent-dev \
                pkg-config \
                python3 \
                ca-certificates

              make clean all

              make test \
                CONCURRENCY=20 \
                TOTAL=20 \
                FD_LIMIT=512 \
                LARGE_ROUNDTRIP_SIZE=4096 \
                SEQUENTIAL_CONNECTIONS=20
            '

  freebsd-amd64:
    name: FreeBSD amd64
    runs-on: ubuntu-latest

    steps:
      - *checkout
      - *set_build_version

      - name: Build and test on FreeBSD
        uses: vmactions/freebsd-vm@v1
        with:
          release: "14.2"
          usesh: true
          prepare: |
            pkg install -y gmake pkgconf libevent python3 pcre2 haproxy
          run: |
            set -eu

            BUILD_VER="${{ steps.set_build_version.outputs.build_ver }}"
            echo "Build version: ${BUILD_VER}"

            gmake clean all EXTRA_CFLAGS="-Og -g3 -Wpedantic"

            # BSD is running in a VM. Lower the requirements.
            #   GitHub Actions runner VM
            #   -> FreeBSD VM/emulator
            gmake test \
              CONCURRENCY=100 TOTAL=100 FD_LIMIT=2560 \
              LARGE_ROUNDTRIP_SIZE=65536 SEQUENTIAL_CONNECTIONS=100

            gmake clean all VERSION="${BUILD_VER}"

            mkdir -p dist/tinyproxy
            cp bin/tinyproxy dist/tinyproxy/
            cp -r examples dist/tinyproxy/

            tar -C dist -czf tinyproxy-freebsd-amd64.tar.gz tinyproxy

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: tinyproxy-freebsd-amd64
          path: tinyproxy-freebsd-amd64.tar.gz
          if-no-files-found: error
          retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }}

  macos:
    name: macOS build
    runs-on: macos-15

    steps:
      - *checkout

      - name: Install deps
        run: |
          brew list libevent >/dev/null 2>&1 || brew install libevent
          brew list haproxy >/dev/null 2>&1 || brew install haproxy

      - *set_build_version

      - name: Build
        run: |
          set -eu

          mkdir -p dist/debug dist/prod

          make clean all \
            STATIC=0 \
            EXTRA_CFLAGS="-Og -g3 -Wpedantic" \
            VERSION="${BUILD_VER}-debug"
          cp bin/tinyproxy dist/debug/tinyproxy

          make clean all \
            STATIC=0 \
            EXTRA_CFLAGS="-Og -g3 -Wpedantic" \
            VERSION="$BUILD_VER"
          cp bin/tinyproxy dist/prod/tinyproxy
          strip dist/prod/tinyproxy

      - *test
      - *package_name
      - *package_unix
      - *upload_unix

  windows:
    name: Windows build
    runs-on: windows-2022
    continue-on-error: true

    steps:
      - *checkout

      - name: Set up MSYS2
        uses: msys2/setup-msys2@v2
        with:
          msystem: UCRT64
          update: true
          install: >-
            base-devel
            mingw-w64-ucrt-x86_64-gcc
            mingw-w64-ucrt-x86_64-pkgconf
            mingw-w64-ucrt-x86_64-libevent
            mingw-w64-ucrt-x86_64-python

      - name: Set Build Version
        shell: msys2 {0}
        run: |
          if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
            BUILD_VER="${GITHUB_REF_NAME}"
          else
            BUILD_VER="dev-${GITHUB_SHA::12}"
          fi
          echo "BUILD_VER=${BUILD_VER}" >> "$GITHUB_ENV"
          echo "Build version: ${BUILD_VER}"

      - name: Build
        shell: msys2 {0}
        run: |
          make clean
          make all VERSION="${BUILD_VER}"

      - name: Test
        shell: msys2 {0}
        run: make test

      - name: Package artifact
        shell: msys2 {0}
        run: |
          set -eu

          mkdir -p dist/tinyproxy

          if [ -f bin/tinyproxy.exe ]; then
            cp bin/tinyproxy.exe dist/tinyproxy/
          else
            cp bin/tinyproxy dist/tinyproxy/tinyproxy.exe
          fi

          copy_deps() {
            local file="$1"

            ldd "$file" |
              awk '
                /=>/ {
                  dll = $3
                  if (dll ~ /^\/ucrt64\/bin\//) {
                    print dll
                  }
                }
                /^[[:space:]]*\/ucrt64\/bin\// {
                  print $1
                }
              ' |
              sort -u |
              while read -r dll; do
                base="$(basename "$dll")"

                if [ ! -f "dist/tinyproxy/$base" ]; then
                  echo "Copying DLL: $dll"
                  cp "$dll" dist/tinyproxy/
                  copy_deps "dist/tinyproxy/$base"
                fi
              done
          }

          echo "Runtime DLL dependencies:"
          ldd dist/tinyproxy/tinyproxy.exe || true

          copy_deps dist/tinyproxy/tinyproxy.exe

          echo "Packaged files:"
          find dist/tinyproxy -maxdepth 1 -type f -printf '%f\n' | sort

          powershell.exe -NoProfile -Command \
            "Compress-Archive -Path dist/tinyproxy -DestinationPath tinyproxy-windows-x64.zip -Force"

      - name: Upload CI artifact
        uses: actions/upload-artifact@v4
        with:
          name: tinyproxy-windows-x64
          path: tinyproxy-windows-x64.zip
          if-no-files-found: error
          retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }}

  release:
    name: GitHub Release
    runs-on: ubuntu-latest

    if: startsWith(github.ref, 'refs/tags/v')

    needs:
      - linux-musl-x86_64
      - linux-musl-arm64
      - freebsd-amd64
      - macos
      - windows

    permissions:
      contents: write

    steps:
      - name: Download CI artifacts
        uses: actions/download-artifact@v4
        with:
          path: release-assets
          pattern: tinyproxy-*
          merge-multiple: true

      - name: Show release assets
        run: |
          find release-assets -type f -maxdepth 2 -print

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
        run: |
          set -eu

          tag="${GITHUB_REF_NAME}"

          body_file="$(mktemp)"

          if printf '%s\n' "$tag" | grep -Eq '^v0\.'; then
            cat > "$body_file" <<'EOF'
          > [!WARNING]
          > This is an experimental v0.x release.
          >
          > APIs, config formats, behavior, and compatibility may change without notice.
          EOF
          else
            : > "$body_file"
          fi

          if ! gh release view "$tag" >/dev/null 2>&1; then
            gh release create "$tag" \
              --title "$tag" \
              --notes-file "$body_file" \
              --generate-notes
          fi

          echo "Assets currently on release:"
          gh release view "$tag" --json assets --jq '.assets[].name'

          for asset in release-assets/*; do
            name="$(basename "$asset")"

            existing="$(
              gh release view "$tag" --json assets --jq '.assets[].name' |
                awk -v target="$name" 'tolower($0) == tolower(target) { print; exit }'
            )"

            if [ -n "$existing" ]; then
              echo "Deleting existing release asset: $existing"
              gh release delete-asset "$tag" "$existing" -y
            fi

            echo "Uploading release asset: $name"
            gh release upload "$tag" "$asset"
          done

  docker-min:
    name: Docker image (minimal)
    runs-on: ubuntu-latest

    if: startsWith(github.ref, 'refs/tags/v')

    needs:
      - linux-musl-x86_64
      - linux-musl-arm64

    permissions:
      contents: read
      packages: write

    steps:
      - *checkout

      - name: Download Linux artifacts
        uses: actions/download-artifact@v4
        with:
          path: docker-artifacts
          pattern: tinyproxy-linux-*
          merge-multiple: true

      - name: Prepare Docker rootfs
        run: |
          mkdir -p rootfs-amd64 rootfs-arm64

          mkdir -p tmp/amd64 tmp/arm64

          tar -xzf docker-artifacts/tinyproxy-linux-x64.tar.gz -C tmp/amd64
          tar -xzf docker-artifacts/tinyproxy-linux-arm64.tar.gz -C tmp/arm64

          mkdir -p rootfs-amd64/usr/bin rootfs-amd64/etc
          mkdir -p rootfs-arm64/usr/bin rootfs-arm64/etc

          cp tmp/amd64/tinyproxy/tinyproxy rootfs-amd64/usr/bin/tinyproxy

          cp tmp/arm64/tinyproxy/tinyproxy rootfs-arm64/usr/bin/tinyproxy

          chmod 0755 rootfs-amd64/usr/bin/tinyproxy
          chmod 0755 rootfs-arm64/usr/bin/tinyproxy

          file rootfs-amd64/usr/bin/tinyproxy
          file rootfs-arm64/usr/bin/tinyproxy

      - &set_image_name
        name: Set image name
        run: |
          image="ghcr.io/${GITHUB_REPOSITORY}"
          echo "IMAGE_NAME=${image,,}" >> "$GITHUB_ENV"

      - &setup_buildx
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - &login_ghcr
        name: Login to GHCR
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ github.token }}

      - name: Build and push image
        uses: docker/build-push-action@v7
        with:
          context: .
          file: ./dockerfiles/github-minimal.Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ${{ env.IMAGE_NAME }}:${{ github.ref_name }}
          labels: |
            org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
            org.opencontainers.image.version=${{ github.ref_name }}
            org.opencontainers.image.revision=${{ github.sha }}

  docker-perf:
    name: Docker image (performance)
    runs-on: ubuntu-latest

    if: startsWith(github.ref, 'refs/tags/v')

    needs:
      - libevent-source

    permissions:
      contents: read
      packages: write

    steps:
      - *checkout
      - *download_libevent_source

      - name: Stage libevent tarball for Docker build
        run: |
          cp ".cache/libevent/${LIBEVENT_TARBALL}" "./${LIBEVENT_TARBALL}"

      - *set_image_name
      - *set_build_version
      - *setup_buildx
      - *login_ghcr

      - name: Build and push image
        uses: docker/build-push-action@v7
        with:
          context: .
          file: ./dockerfiles/github-perf.Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          build-args: |
            LIBEVENT_TARBALL=${{ env.LIBEVENT_TARBALL }}
            BUILD_VER=${{ env.BUILD_VER }}
          tags: |
            ${{ env.IMAGE_NAME }}:${{ github.ref_name }}-debian-slim
          labels: |
            org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
            org.opencontainers.image.version=${{ github.ref_name }}-debian-slim
            org.opencontainers.image.revision=${{ github.sha }}