The dashboard now answers "where did my tokens go" without opening the log: totals, per-provider and per-model breakdown, and cache hits shown as the requests that cost nothing. Each log row expands to the actual prompt and response, truncated to keep the ring buffer flat. CI's Docker smoke step supplied no virtual keys, so the container correctly refused to bind 0.0.0.0 and the job failed. The failsafe was right and the workflow was wrong; it now supplies keys and additionally asserts that an unauthenticated call gets 401 and an inference key gets 403 on /api, so an image that lost its auth boundaries cannot be published green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
4.5 KiB
YAML
132 lines
4.5 KiB
YAML
name: Docker
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
tags: ['v*.*.*']
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE_NAME: ${{ github.repository }}
|
|
|
|
concurrency:
|
|
group: docker-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
build:
|
|
name: build & push image
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
# Required for cosign keyless signing of pushed images.
|
|
id-token: write
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@v3
|
|
|
|
- name: Set up Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
# A pull request from a fork cannot read the registry secret, so builds are
|
|
# verified without pushing. Only trusted refs publish.
|
|
- name: Log in to GHCR
|
|
if: github.event_name != 'pull_request'
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Derive tags and labels
|
|
id: meta
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
tags: |
|
|
type=ref,event=branch
|
|
type=ref,event=pr
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
type=sha,format=short
|
|
type=raw,value=latest,enable={{is_default_branch}}
|
|
labels: |
|
|
org.opencontainers.image.title=Freeway
|
|
org.opencontainers.image.description=Self-hosted OpenAI-compatible gateway for free-tier LLM providers
|
|
|
|
- name: Build and push
|
|
id: build
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
platforms: linux/amd64,linux/arm64
|
|
push: ${{ github.event_name != 'pull_request' }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
provenance: true
|
|
sbom: true
|
|
|
|
# Smoke-test the image we just built, on the native arch only — an arm64
|
|
# image cannot be started on an amd64 runner without emulation overhead.
|
|
- name: Smoke-test the image
|
|
if: github.event_name != 'pull_request'
|
|
run: |
|
|
set -euo pipefail
|
|
IMAGE="$(echo '${{ steps.meta.outputs.tags }}' | head -n1)"
|
|
# The gateway refuses to bind a public interface with no virtual keys —
|
|
# a deliberate failsafe, and the container binds 0.0.0.0. CI has to
|
|
# supply one, exactly as a real deployment would.
|
|
docker run -d --name freeway-smoke -p 8787:8787 --platform linux/amd64 \
|
|
-e FREEWAY_KEYS=fw-ci-smoke-only \
|
|
-e FREEWAY_ADMIN_KEY=fw-ci-admin-only \
|
|
"$IMAGE"
|
|
|
|
for i in $(seq 1 30); do
|
|
if curl -fsS http://127.0.0.1:8787/healthz >/dev/null 2>&1; then
|
|
echo "gateway is healthy"
|
|
curl -fsS http://127.0.0.1:8787/healthz; echo
|
|
|
|
# An unauthenticated call must be refused, and an app key must not
|
|
# reach the admin surface. If either regresses, the image is unsafe
|
|
# to publish however green everything else looks.
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8787/v1/chat/completions \
|
|
-H 'content-type: application/json' -d '{"model":"auto","messages":[]}')
|
|
[ "$code" = "401" ] || { echo "expected 401 without a key, got $code" >&2; docker rm -f freeway-smoke; exit 1; }
|
|
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8787/api/logs \
|
|
-H 'authorization: Bearer fw-ci-smoke-only')
|
|
[ "$code" = "403" ] || { echo "expected 403 for an inference key on /api, got $code" >&2; docker rm -f freeway-smoke; exit 1; }
|
|
|
|
echo "auth boundaries hold"
|
|
docker rm -f freeway-smoke
|
|
exit 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
echo "gateway did not become healthy in 30s" >&2
|
|
docker logs freeway-smoke >&2
|
|
docker rm -f freeway-smoke
|
|
exit 1
|
|
|
|
- name: Summary
|
|
if: github.event_name != 'pull_request'
|
|
run: |
|
|
{
|
|
echo "### Image published"
|
|
echo
|
|
echo '```'
|
|
echo '${{ steps.meta.outputs.tags }}'
|
|
echo '```'
|
|
echo
|
|
echo "digest: \`${{ steps.build.outputs.digest }}\`"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|