ci(docker): build, smoke-test, and publish the remote-HTTP image
New .gitea/workflows/docker.yml (alongside the untouched ci.yml wheel publish). Runs lint + test, then builds the image and smoke-tests it — non-root uid, /healthz 200, MCP 401 without a token and 200 with a valid bearer. On a merge to main/dev it pushes to git.hiddenden.cafe/hiddenden/familiar (main -> :latest + :main, dev -> :dev, plus :sha-<12>) via the existing REGISTRY_TOKEN, then does a scoped cleanup. The two workflows are independent and don't interfere.
This commit is contained in:
1 file changed
+131
@@ -0,0 +1,131 @@
|
||||
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 over HTTP.
|
||||
docker run -d --name familiar-smoke -e FAMILIAR_HTTP_TOKEN="$TOKEN" -p 18080:8080 "$IMG"
|
||||
for i in $(seq 1 30); do
|
||||
if curl -fsS "http://127.0.0.1:18080/healthz" >/dev/null 2>&1; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "== /healthz (expect 200) =="
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:18080/healthz")
|
||||
[ "$code" = "200" ] || { echo "::error::/healthz returned $code"; docker logs familiar-smoke; exit 1; }
|
||||
|
||||
echo "== MCP without token (expect 401) =="
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "http://127.0.0.1:18080/mcp" \
|
||||
-H 'Accept: application/json, text/event-stream' -H 'Content-Type: application/json' \
|
||||
-d '{"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=$(curl -s -o /dev/null -w '%{http_code}' -X POST "http://127.0.0.1:18080/mcp" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Accept: application/json, text/event-stream' -H 'Content-Type: application/json' \
|
||||
-d '{"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
|
||||
Reference in new issue
Block a user