rust-web-server 18.10.0

Single-binary web server and reverse proxy: automatic HTTPS (Let's Encrypt), HTTP/1.1, HTTP/2 and HTTP/3, load balancing, auth and rate limiting, configured in one TOML file. Also a Rust library: routing, middleware, MCP server, ORM, background jobs. No hyper, no Tower; HTTP/2 and HTTP/3 framing via h2 and quinn.
Documentation
name: Packages

# deb and rpm packages (packaging/linux): build them from a release-style
# binary, install each on a systemd host — the deb on the runner itself, the
# rpm in a Fedora container booted with systemd — and require the rws service
# to start, serve, survive a reload and go away on removal. Windows: run
# rws.exe --windows-service as a real service, serve, stop.
on:
  push:
    branches: [main]
    paths: ["packaging/**", "src/main.rs", ".github/workflows/packaging.yml"]
  pull_request:
    paths: ["packaging/**", "src/main.rs", ".github/workflows/packaging.yml"]
  workflow_dispatch:

env:
  NFPM_VERSION: 2.47.0

jobs:
  linux:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: x86_64-unknown-linux-musl
      - run: sudo apt-get update && sudo apt-get install -y musl-tools
      - uses: Swatinem/rust-cache@v2
        with:
          key: packaging
      - name: Build (as the release does)
        run: |
          cargo build --release --bin rws --features "acme auth mimalloc wasm-plugins" --target x86_64-unknown-linux-musl
      - name: Install nfpm
        run: |
          curl -sSfL "https://github.com/goreleaser/nfpm/releases/download/v$NFPM_VERSION/nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz" \
            | sudo tar xz -C /usr/local/bin nfpm
      - name: Throwaway signing key
        run: |
          gpg --batch --gen-key <<'KEY'
          %no-protection
          Key-Type: RSA
          Key-Length: 3072
          Name-Real: rws CI test
          Name-Email: ci@example.invalid
          Expire-Date: 1d
          %commit
          KEY
          echo "GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/ {print $10; exit}')" >> "$GITHUB_ENV"
      - name: Package (as the release does)
        run: |
          version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name=="rust-web-server") | .version')
          gpg --batch --armor --export-secret-keys "$GPG_KEY_ID" > "$RUNNER_TEMP/signing.asc"
          RPM_SIGNING_KEY_FILE="$RUNNER_TEMP/signing.asc" \
            packaging/build-packages.sh "$version" amd64 target/x86_64-unknown-linux-musl/release/rws packages
          ls -l packages
      - name: Repositories (as the release does)
        run: |
          sudo apt-get install -y apt-utils createrepo-c
          packaging/build-repos.sh packages site http://localhost:8000
          nohup setsid python3 -m http.server 8000 --directory site >/dev/null 2>&1 &
          sleep 1 && curl -sSf localhost:8000/apt/dists/stable/InRelease | head -3
      - name: deb on the runner (systemd)
        run: |
          sudo apt-get install -y ./packages/rws_*_amd64.deb
          systemctl is-active rws
          for i in $(seq 1 20); do curl -sf localhost:7878/healthz && break; sleep 1; done
          curl -sSf localhost:7878/ | grep 'rws is running'
          test "$(stat -c %U /etc/rws/rws.config.toml)" = root
          systemd-analyze security rws.service | tail -1
          sudo systemctl reload rws && sleep 1 && systemctl is-active rws
          sudo apt-get install -y --reinstall ./packages/rws_*_amd64.deb && systemctl is-active rws
          # An upgrade keeps a service the admin disabled disabled.
          sudo systemctl disable --now rws
          sudo apt-get install -y --reinstall ./packages/rws_*_amd64.deb
          ! systemctl is-active rws
          sudo systemctl enable --now rws
          sudo apt-get remove -y rws
          ! systemctl is-active rws
          test -f /etc/rws/rws.config.toml   # remove keeps config; purge deletes it
      - name: deb from the apt repository
        run: |
          curl -sSf localhost:8000/apt/rws.gpg | sudo tee /usr/share/keyrings/rws.gpg >/dev/null
          echo "deb [signed-by=/usr/share/keyrings/rws.gpg] http://localhost:8000/apt stable main" | sudo tee /etc/apt/sources.list.d/rws.list
          sudo apt-get update -o Dir::Etc::sourcelist=/etc/apt/sources.list.d/rws.list -o Dir::Etc::sourceparts=- 
          sudo apt-get install -y rws
          systemctl is-active rws
          sudo apt-get purge -y rws
      - name: rpm in a Fedora container (systemd)
        run: |
          docker run -d --name fedora --privileged --cgroupns=host -v /sys/fs/cgroup:/sys/fs/cgroup:rw \
            --network host -v "$PWD/packages:/dist:ro" fedora:42 sh -c 'dnf install -y -q systemd curl && exec /usr/lib/systemd/systemd'
          for i in $(seq 1 120); do docker exec fedora systemctl is-system-running 2>/dev/null | grep -qE 'running|degraded' && break; sleep 2; done
          docker exec fedora sh -c 'rpm --import http://localhost:8000/rpm/rws.asc && rpm -K /dist/*.x86_64.rpm'
          docker exec fedora dnf install -y "/dist/$(basename packages/rws-*.x86_64.rpm)"
          docker exec fedora systemctl is-active rws
          docker exec fedora sh -c 'for i in $(seq 1 20); do curl -sf localhost:7878/healthz && break; sleep 1; done'
          docker exec fedora sh -c 'curl -sSf localhost:7878/ | grep "rws is running"'
          docker exec fedora dnf remove -y rws
          ! docker exec fedora systemctl is-active rws
          # From the signed repository (gpgcheck and repo_gpgcheck on):
          docker exec fedora curl -sSf -o /etc/yum.repos.d/rws.repo http://localhost:8000/rpm/rws.repo
          docker exec fedora dnf install -y rws
          docker exec fedora systemctl is-active rws
      - name: Diagnostics
        if: failure()
        run: |
          sudo journalctl -u rws --no-pager | tail -50 || true
          docker exec fedora journalctl -u rws --no-pager | tail -50 || true
      - uses: actions/upload-artifact@v4
        with:
          name: packages
          path: packages/*

  windows-service:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: ilammy/setup-nasm@v1
      - uses: Swatinem/rust-cache@v2
        with:
          key: windows-service
      - run: cargo build --release --bin rws --features "acme auth mimalloc wasm-plugins"
      - name: Install, start, serve, stop
        shell: pwsh
        run: |
          $site = Join-Path $env:RUNNER_TEMP "rws-site"
          New-Item -ItemType Directory -Force $site | Out-Null
          Set-Content (Join-Path $site "index.html") "rws service is running"
          $exe = (Resolve-Path target\release\rws.exe).Path
          New-Service -Name rws -BinaryPathName "`"$exe`" --windows-service=`"$site`"" -StartupType Manual | Out-Null
          Start-Service rws
          $body = $null
          for ($i = 0; $i -lt 30; $i++) {
            try { $body = (Invoke-WebRequest http://localhost:7878/ -UseBasicParsing).Content; break } catch { Start-Sleep 1 }
          }
          if ($body -notmatch "rws service is running") { throw "unexpected response: $body" }
          (Invoke-WebRequest http://localhost:7878/healthz -UseBasicParsing).StatusCode
          Stop-Service rws
          (Get-Service rws).WaitForStatus("Stopped", "00:00:30")
          sc.exe delete rws
      - name: Diagnostics
        if: failure()
        shell: pwsh
        run: |
          Get-Service rws -ErrorAction SilentlyContinue | Format-List *
          Get-WinEvent -LogName System -MaxEvents 20 | Where-Object ProviderName -eq "Service Control Manager" | Format-List TimeCreated, Message

  winget:
    # packaging/winget manifests for the latest release: winget validates and
    # installs them (the PR to microsoft/winget-pkgs is made per release).
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - name: Generate
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          version=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)
          echo "MANIFEST=$(packaging/winget/generate.sh "$version" "$RUNNER_TEMP/winget")" >> "$GITHUB_ENV"
      - name: Validate and install
        shell: pwsh
        run: |
          if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
            Install-PackageProvider -Name NuGet -Force | Out-Null
            Install-Module Microsoft.WinGet.Client -Force -Repository PSGallery | Out-Null
            Repair-WinGetPackageManager -AllUsers
          }
          winget --version
          winget validate --manifest $env:MANIFEST
          winget settings --enable LocalManifestFiles
          winget install --manifest $env:MANIFEST --accept-source-agreements --accept-package-agreements --disable-interactivity
          $rws = Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links\rws.exe"
          & $rws --version

  nix:
    # packaging/nix/package.nix (the nixpkgs expression) built from this checkout.
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cachix/install-nix-action@v31.11.1
        with:
          nix_path: nixpkgs=channel:nixos-unstable
      - run: nix-build packaging/nix/ci.nix
      - run: ./result/bin/rws --version