Files
Latte 288c696f28
CI / lint (pull_request) Successful in 55s
docker / lint (pull_request) Successful in 56s
CI / test (pull_request) Successful in 1m6s
docker / test (pull_request) Successful in 1m4s
CI / publish (pull_request) Has been skipped
docker / docker (pull_request) Successful in 41s
fix(ci): give the smoke-test container a per-commit name
The Docker smoke-test ran the server container under the fixed name
familiar-smoke. On the self-hosted runner the job shares the host
Docker daemon, so a container left behind by a killed or re-run job
(or a concurrent dev push + PR) collides on that name and 'docker run'
fails with exit 125.

Derive the name from the commit SHA, remove any stale container of that
name before starting, and thread the name through the smoke-test and
cleanup steps.
2026-07-05 13:55:29 +02:00

150 lines
6.4 KiB
YAML

name: docker
# Build + smoke-test the remote-HTTP (`familiar serve`) image on every push/PR;
# push to the Gitea container registry only on a real merge to main/dev.
# Runs alongside ci.yml (which publishes the Python wheel) — they don't overlap.
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Install dependencies
run: uv sync --group dev
- name: Ruff (lint + format check)
run: |
uv run ruff check .
uv run ruff format --check .
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Install dependencies
run: uv sync --group dev
- name: Run tests with coverage (fails under 80%)
run: uv run pytest
docker:
needs: [lint, test]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Compute image name & tags
id: meta
shell: bash
run: |
IMAGE="git.hiddenden.cafe/${GITHUB_REPOSITORY,,}"
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
echo "sha_tag=${IMAGE}:sha-${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT"
# Per-commit container name: the self-hosted runner shares the host
# Docker daemon, so a fixed name collides across concurrent/re-run jobs.
echo "container=familiar-smoke-${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT"
if [ "${GITHUB_REF_NAME}" = "main" ]; then
echo "branch_tags=${IMAGE}:latest ${IMAGE}:main" >> "$GITHUB_OUTPUT"
else
echo "branch_tags=${IMAGE}:${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
fi
- name: Build image
shell: bash
run: docker build -f docker/Dockerfile -t "${{ steps.meta.outputs.sha_tag }}" .
- name: Smoke-test image
shell: bash
run: |
set -euo pipefail
IMG="${{ steps.meta.outputs.sha_tag }}"
CONTAINER="${{ steps.meta.outputs.container }}"
TOKEN="smoke-$(date +%s)"
# Remove any container left behind by a killed/re-run job before we
# reuse the name (shared host daemon → stale containers can survive).
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
# 1. Runs as a non-root user (uid 1000).
uid=$(docker run --rm --entrypoint id "$IMG" -u)
echo "container uid: $uid"
[ "$uid" = "1000" ] || { echo "::error::image runs as uid $uid, expected 1000"; exit 1; }
# 2. Boot the server and probe it from *inside* its own container via
# `docker exec`. On self-hosted runners the job step runs in a container
# that shares the host's Docker socket but sits on a different network,
# so it can reach neither a published host port (-p lands on the runner
# host, not this step's netns) nor the server container's bridge IP
# (different network → connect hangs). `docker exec` goes through the
# socket and runs the probe in the server's own netns, where
# 127.0.0.1:8080 always works regardless of runner topology. The image
# ships python + httpx, so no curl or extra pull is needed.
docker run -d --name "$CONTAINER" -e FAMILIAR_HTTP_TOKEN="$TOKEN" "$IMG"
# probe "<httpx call returning a Response>" -> prints the status code
probe() { docker exec "$CONTAINER" python -c "import httpx; print(($1).status_code)"; }
for i in $(seq 1 30); do
if docker exec "$CONTAINER" python -c \
"import httpx; httpx.get('http://127.0.0.1:8080/healthz', timeout=5)" >/dev/null 2>&1; then
break
fi
sleep 1
done
echo "== /healthz (expect 200) =="
code=$(probe "httpx.get('http://127.0.0.1:8080/healthz', timeout=10)")
[ "$code" = "200" ] || { echo "::error::/healthz returned $code"; docker logs "$CONTAINER"; exit 1; }
echo "== MCP without token (expect 401) =="
code=$(probe "httpx.post('http://127.0.0.1:8080/mcp', timeout=10, headers={'Accept':'application/json, text/event-stream','Content-Type':'application/json'}, json={'jsonrpc':'2.0','id':1,'method':'initialize','params':{}})")
[ "$code" = "401" ] || { echo "::error::unauthenticated MCP returned $code"; exit 1; }
echo "== MCP with valid token (expect 200) =="
code=$(probe "httpx.post('http://127.0.0.1:8080/mcp', timeout=10, headers={'Authorization':'Bearer $TOKEN','Accept':'application/json, text/event-stream','Content-Type':'application/json'}, json={'jsonrpc':'2.0','id':1,'method':'initialize','params':{'protocolVersion':'2025-06-18','capabilities':{},'clientInfo':{'name':'ci','version':'1'}}})")
[ "$code" = "200" ] || { echo "::error::authenticated MCP returned $code"; docker logs "$CONTAINER"; exit 1; }
echo "Image smoke-test passed."
- name: Log in to Gitea Container Registry
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'dev')
uses: docker/login-action@v3
with:
registry: git.hiddenden.cafe
username: ${{ github.actor }}
# PAT with write:package scope (the auto GITEA_TOKEN lacks it here).
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Tag & push
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'dev')
shell: bash
run: |
for tag in ${{ steps.meta.outputs.branch_tags }} ${{ steps.meta.outputs.sha_tag }}; do
docker tag "${{ steps.meta.outputs.sha_tag }}" "$tag"
docker push "$tag"
echo "Pushed $tag"
done
# Always runs — removes only what this run created (the self-hosted runner
# may share the host Docker daemon, so no global prune of other services).
- name: Cleanup
if: always()
shell: bash
run: |
docker rm -f ${{ steps.meta.outputs.container }} || true
docker rmi -f ${{ steps.meta.outputs.sha_tag }} ${{ steps.meta.outputs.branch_tags }} || true
docker image prune -f || true
docker builder prune -f || true