CI / lint (pull_request) Successful in 56s
docker / lint (pull_request) Successful in 54s
CI / test (pull_request) Successful in 1m2s
CI / publish (pull_request) Has been skipped
docker / test (pull_request) Successful in 57s
docker / docker (pull_request) Successful in 41s
The smoke test probed the server over the network, which fails on the self-hosted runner: the job step runs in a container that shares the host Docker socket but sits on a different network, so it can reach neither a published host port (-p lands on the runner host) nor the server container's bridge IP (different network → curl connect hangs, so the job looked stuck rather than failing). Probe from inside the server container with `docker exec` instead: it goes through the Docker socket and runs 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 image pull is needed. Drop the now unnecessary -p publish.
142 lines
5.8 KiB
YAML
142 lines
5.8 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"
|
|
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 }}"
|
|
TOKEN="smoke-$(date +%s)"
|
|
|
|
# 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 familiar-smoke -e FAMILIAR_HTTP_TOKEN="$TOKEN" "$IMG"
|
|
|
|
# probe "<httpx call returning a Response>" -> prints the status code
|
|
probe() { docker exec familiar-smoke python -c "import httpx; print(($1).status_code)"; }
|
|
|
|
for i in $(seq 1 30); do
|
|
if docker exec familiar-smoke 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 familiar-smoke; 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 familiar-smoke; 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 familiar-smoke || 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
|