name: Release
on:
push:
tags: ["[0-9]+.[0-9]+.[0-9]+"]
workflow_dispatch:
inputs:
tag:
description: Existing version tag to (re)build, e.g. 17.111.0
required: true
permissions:
contents: write
packages: write
env:
TAG: ${{ inputs.tag || github.ref_name }}
CARGO_TERM_COLOR: always
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.TAG }}
fetch-depth: 0
- name: Check tag matches Cargo.toml
run: |
crate_version=$(grep -m1 '^version' Cargo.toml | cut -d '"' -f2)
if [ "$crate_version" != "$TAG" ]; then
echo "Tag $TAG does not match Cargo.toml version $crate_version" >&2
exit 1
fi
- name: Build release notes
run: |
prev=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true)
range=${prev:+$prev..}$TAG
{
echo "## Install"
echo
echo '```bash'
echo "brew install bohdaq/tap/rws # macOS / Linux"
echo "cargo binstall rust-web-server # prebuilt binary, no compile"
echo "docker run -p 7878:7878 bohdaq/rws:$TAG"
echo '```'
echo
echo "Or download a binary below (Linux binaries are static musl builds)."
echo
breaking=$(git log --format='- %s (%h)' "$range" | grep -E '^- [a-z]+(\([^)]*\))?!:' || true)
if [ -n "$breaking" ]; then
printf '## ⚠️ Breaking changes\n\n%s\n\n' "$breaking"
fi
for kind in feat fix perf; do
case $kind in
feat) title="Features" ;;
fix) title="Fixes" ;;
perf) title="Performance" ;;
esac
lines=$(git log --format='- %s (%h)' "$range" | grep -E "^- $kind(\(|:)" || true)
if [ -n "$lines" ]; then
printf '## %s\n\n%s\n\n' "$title" "$lines"
fi
done
if [ -n "$prev" ]; then
echo "**Full diff:** https://github.com/$GITHUB_REPOSITORY/compare/$prev...$TAG"
fi
echo
echo "Crate: https://crates.io/crates/rust-web-server · Docs: https://rws8.tech"
} > notes.md
cat notes.md
- name: Publish GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
gh release edit "$TAG" --notes-file notes.md
else
gh release create "$TAG" --title "$TAG" --notes-file notes.md --verify-tag
fi
binaries:
needs: release
name: binary (${{ matrix.target }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { target: x86_64-unknown-linux-musl, os: ubuntu-latest }
- { target: aarch64-unknown-linux-musl, os: ubuntu-24.04-arm }
- { target: x86_64-apple-darwin, os: macos-latest }
- { target: aarch64-apple-darwin, os: macos-latest }
- { target: x86_64-pc-windows-msvc, os: windows-latest }
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.TAG }}
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install musl tools
if: contains(matrix.target, 'musl')
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Install NASM (aws-lc-sys on Windows)
if: runner.os == 'Windows'
uses: ilammy/setup-nasm@v1
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Build
run: cargo build --release --bin rws --features "acme auth" --target ${{ matrix.target }}
- name: Package
shell: bash
run: |
name="rws-$TAG-${{ matrix.target }}"
mkdir "$name"
cp -R README.md LICENSE "$name/"
if [ "${{ runner.os }}" = "Windows" ]; then
cp "target/${{ matrix.target }}/release/rws.exe" "$name/"
7z a "$name.zip" "$name" >/dev/null
archive="$name.zip"
else
cp "target/${{ matrix.target }}/release/rws" "$name/"
tar czf "$name.tar.gz" "$name"
archive="$name.tar.gz"
fi
if command -v sha256sum >/dev/null; then
sha256sum "$archive" > "$archive.sha256"
else
shasum -a 256 "$archive" > "$archive.sha256"
fi
echo "ARCHIVE=$archive" >> "$GITHUB_ENV"
- name: Smoke test
if: runner.os != 'Windows' && matrix.target != 'x86_64-apple-darwin'
shell: bash
run: |
# Run from an empty directory, like a fresh install would.
bin="$PWD/rws-$TAG-${{ matrix.target }}/rws"
mkdir -p "$RUNNER_TEMP/smoke" && cd "$RUNNER_TEMP/smoke"
"$bin" --port=7979 &
pid=$!
for i in $(seq 1 30); do
curl -sf http://127.0.0.1:7979/healthz && break
sleep 1
done
curl -sf http://127.0.0.1:7979/healthz
kill $pid
- name: Upload to release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$TAG" "$ARCHIVE" "$ARCHIVE.sha256" --clobber
- name: Keep Linux binary for the image build
if: contains(matrix.target, 'linux')
uses: actions/upload-artifact@v4
with:
name: docker-${{ matrix.target }}
path: target/${{ matrix.target }}/release/rws
docker:
needs: binaries
runs-on: ubuntu-latest
env:
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.TAG }}
- uses: actions/download-artifact@v4
with:
name: docker-x86_64-unknown-linux-musl
path: dist/amd64
- uses: actions/download-artifact@v4
with:
name: docker-aarch64-unknown-linux-musl
path: dist/arm64
- run: chmod +x dist/*/rws
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Build amd64 image for testing
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.release
platforms: linux/amd64
load: true
tags: rws-smoke:test
- name: Smoke-test the image
run: |
mkdir -p "$RUNNER_TEMP/site" && echo "smoke" > "$RUNNER_TEMP/site/index.html"
docker run -d --name rws-smoke -p 7878:7878 -v "$RUNNER_TEMP/site:/srv:ro" rws-smoke:test
for i in $(seq 1 30); do curl -sf http://127.0.0.1:7878/healthz && break; sleep 1; done
curl -sf http://127.0.0.1:7878/healthz
[ "$(curl -sf http://127.0.0.1:7878/)" = "smoke" ]
[ "$(docker run --rm rws-smoke:test --version)" = "rws $TAG" ]
# /data must belong to the image's nonroot user (65532) — ACME writes
# certificates there.
docker create --name rws-data-check rws-smoke:test >/dev/null
docker cp rws-data-check:/data - | tar --numeric-owner -tvf - | head -1 | grep -q " 65532/65532 "
docker rm rws-data-check >/dev/null
docker logs rws-smoke 2>&1 | tail -5
docker rm -f rws-smoke
- name: Tags
id: tags
run: |
major="${TAG%%.*}"
tags="ghcr.io/${{ github.repository_owner }}/rws:$TAG,ghcr.io/${{ github.repository_owner }}/rws:$major,ghcr.io/${{ github.repository_owner }}/rws:latest"
if [ -n "$DOCKERHUB_USERNAME" ] && [ -n "$DOCKERHUB_TOKEN" ]; then
tags="$tags,docker.io/$DOCKERHUB_USERNAME/rws:$TAG,docker.io/$DOCKERHUB_USERNAME/rws:$major,docker.io/$DOCKERHUB_USERNAME/rws:latest"
else
echo "::warning::DOCKERHUB_USERNAME / DOCKERHUB_TOKEN not set — publishing to ghcr.io only"
fi
echo "tags=$tags" >> "$GITHUB_OUTPUT"
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- uses: docker/login-action@v3
if: env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != ''
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.release
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.tags.outputs.tags }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.version=${{ env.TAG }}
org.opencontainers.image.licenses=MIT
org.opencontainers.image.title=rws
org.opencontainers.image.description=Single-binary web server and reverse proxy: automatic HTTPS, HTTP/3, load balancing, configured in one TOML file
org.opencontainers.image.url=https://rws8.tech
homebrew:
needs: binaries
runs-on: ubuntu-latest
steps:
- name: Fetch checksums
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.tar.gz.sha256'
for t in aarch64-apple-darwin x86_64-apple-darwin aarch64-unknown-linux-musl x86_64-unknown-linux-musl; do
echo "SHA_$(echo $t | tr 'a-z-' 'A-Z_')=$(cut -d ' ' -f1 rws-$TAG-$t.tar.gz.sha256)" >> "$GITHUB_ENV"
done
- uses: actions/checkout@v4
with:
repository: ${{ github.repository_owner }}/homebrew-tap
ssh-key: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
path: tap
- name: Write formula
run: |
base="https://github.com/$GITHUB_REPOSITORY/releases/download/$TAG"
mkdir -p tap/Formula
cat > tap/Formula/rws.rb <<EOF
class Rws < Formula
desc "HTTP/1.1, HTTP/2 and HTTP/3 web server, reverse proxy and app framework"
homepage "https://rws8.tech"
version "$TAG"
license "MIT"
on_macos do
on_arm do
url "$base/rws-$TAG-aarch64-apple-darwin.tar.gz"
sha256 "$SHA_AARCH64_APPLE_DARWIN"
end
on_intel do
url "$base/rws-$TAG-x86_64-apple-darwin.tar.gz"
sha256 "$SHA_X86_64_APPLE_DARWIN"
end
end
on_linux do
on_arm do
url "$base/rws-$TAG-aarch64-unknown-linux-musl.tar.gz"
sha256 "$SHA_AARCH64_UNKNOWN_LINUX_MUSL"
end
on_intel do
url "$base/rws-$TAG-x86_64-unknown-linux-musl.tar.gz"
sha256 "$SHA_X86_64_UNKNOWN_LINUX_MUSL"
end
end
def install
bin.install "rws"
end
test do
port = free_port
pid = spawn bin/"rws", "--port=#{port}"
sleep 2
assert_match "OK", shell_output("curl -s http://127.0.0.1:#{port}/healthz")
ensure
Process.kill("TERM", pid)
end
end
EOF
cat tap/Formula/rws.rb
- name: Push formula
working-directory: tap
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/rws.rb
git diff --cached --quiet && exit 0
git commit -m "rws $TAG"
git push