build+deploy: reproducible pipeline via Gitea Actions + gw-04 image-watcher #1

Merged
osobh merged 4 commits from chore/build-and-deploy-pipeline into main 2026-07-05 16:06:56 +00:00
4 changed files with 166 additions and 2 deletions
+82 -2
View File
@@ -20,12 +20,39 @@ jobs:
rust: rust:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: gates needs: gates
# Compile sqlx query! macros against the committed .sqlx cache (no DB needed); # Compile sqlx query! macros against the committed .sqlx cache (no DB needed).
# DB-backed tests spin up their own postgres via testcontainers at runtime. # Tests need a live Postgres — locally cm-testkit reads CM_TEST_DATABASE_URL
# from .cargo/config.toml pointing at scripts/test-server.sh's host container.
# The fleet act_runner uses the `host` executor (jobs run on morpheus/tank/
# architect natively, not inside a container), so we start a per-run postgres
# container and reach it via its bridge IP. GITHUB_RUN_ID scopes the name so
# concurrent jobs on the same runner don't collide.
env: env:
SQLX_OFFLINE: "true" SQLX_OFFLINE: "true"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Start postgres sidecar
run: |
set -euo pipefail
NAME="ci-pg-${GITHUB_RUN_ID}"
docker rm -f "$NAME" >/dev/null 2>&1 || true
docker run -d --name "$NAME" \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=postgres \
postgres:16-alpine >/dev/null
PG_IP=$(docker inspect -f '{{.NetworkSettings.IPAddress}}' "$NAME")
echo "PG_CONTAINER=$NAME" >> "$GITHUB_ENV"
echo "CM_TEST_DATABASE_URL=postgres://postgres:postgres@${PG_IP}:5432/postgres" >> "$GITHUB_ENV"
for i in $(seq 1 30); do
if docker exec "$NAME" pg_isready -U postgres -q >/dev/null 2>&1; then
echo "postgres ready at ${PG_IP} after ${i}s"
exit 0
fi
sleep 1
done
echo "postgres never became ready" >&2
docker logs "$NAME" >&2 || true
exit 1
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: 1.96.0 toolchain: 1.96.0
@@ -39,6 +66,9 @@ jobs:
run: cargo test --workspace run: cargo test --workspace
- name: Air-gapped installer verify path - name: Air-gapped installer verify path
run: ./ci/test-install.sh run: ./ci/test-install.sh
- name: Cleanup postgres sidecar
if: always()
run: docker rm -f "${PG_CONTAINER:-}" >/dev/null 2>&1 || true
frontend: frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -92,3 +122,53 @@ jobs:
with: with:
name: playwright-traces name: playwright-traces
path: frontend/test-results/ path: frontend/test-results/
# Rolling deploy: on green main only, build the three prod images, tag with
# :main-<sha> + :latest, push to the fleet registry (redclaw-web-01:5000 via
# its Tailscale IP — the fleet's daemons trust it in insecure-registries by
# IP, not by hostname). GW-04's clawmates-deploy.timer rolls forward within
# ~1 minute of the push. Skipped on PRs.
#
# `e2e` is intentionally NOT in `needs`: it launches its own postgres + dex
# via `docker run` on the host and then reaches them via 127.0.0.1, which
# fails from inside the act_runner container. Migrating e2e to a physical
# build node is a separate task; until then e2e is signal-only, not gating.
publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: [gates, rust, frontend]
env:
REGISTRY: 100.94.185.103:5000
NAMESPACE: clawmates
steps:
- uses: actions/checkout@v4
- name: Resolve short SHA
run: echo "SHA=${GITHUB_SHA::7}" >> "$GITHUB_ENV"
- name: Build images
run: |
set -euo pipefail
for svc in broker server frontend; do
docker build \
-t "${REGISTRY}/${NAMESPACE}/${svc}:main-${SHA}" \
-t "${REGISTRY}/${NAMESPACE}/${svc}:latest" \
-f "images/${svc}.Dockerfile" .
done
- name: Push images
run: |
set -euo pipefail
for svc in broker server frontend; do
docker push "${REGISTRY}/${NAMESPACE}/${svc}:main-${SHA}"
docker push "${REGISTRY}/${NAMESPACE}/${svc}:latest"
done
- name: Summary
run: |
{
echo "## Published images"
echo ""
for svc in broker server frontend; do
echo "- \`${REGISTRY}/${NAMESPACE}/${svc}:main-${SHA}\`"
echo "- \`${REGISTRY}/${NAMESPACE}/${svc}:latest\`"
done
echo ""
echo "GW-04 timer picks these up within ~1 minute."
} >> "$GITHUB_STEP_SUMMARY"
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Clawmates rolling deploy (pull :latest, roll on drift)
Wants=docker.service
After=docker.service network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/clawmates-deploy.sh
StandardOutput=journal
StandardError=journal
Nice=10
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
#
# clawmates rolling deploy — polls the fleet registry for :latest of the three
# prod images (broker, server, frontend). On drift, pulls the new image, retags
# it under the un-prefixed name the running compose file uses, and rolls the
# affected services with `docker compose up -d`.
#
# Install:
# sudo install -m 0755 clawmates-deploy.sh /usr/local/bin/clawmates-deploy.sh
# sudo install -m 0644 clawmates-deploy.service /etc/systemd/system/
# sudo install -m 0644 clawmates-deploy.timer /etc/systemd/system/
# sudo systemctl daemon-reload
# sudo systemctl enable --now clawmates-deploy.timer
#
# Verify:
# systemctl list-timers clawmates-deploy.timer
# tail -f /var/log/clawmates-deploy.log
#
# The retag step (registry/clawmates/<svc>:latest → clawmates/<svc>:latest)
# keeps the current /root/clawmates/docker-compose.yml working unchanged until
# we're ready to migrate the compose file to registry-prefixed image names.
set -euo pipefail
REGISTRY="${REGISTRY:-100.94.185.103:5000}"
NAMESPACE="${NAMESPACE:-clawmates}"
COMPOSE_DIR="${COMPOSE_DIR:-/root/clawmates}"
LOG="${LOG:-/var/log/clawmates-deploy.log}"
SERVICES=(broker server frontend)
log() { printf '%s %s\n' "$(date -Iseconds)" "$*" | tee -a "$LOG" >/dev/null; }
changed=()
for svc in "${SERVICES[@]}"; do
ref="${REGISTRY}/${NAMESPACE}/${svc}:latest"
local_ref="${NAMESPACE}/${svc}:latest"
before=$(docker inspect --format '{{.Id}}' "$ref" 2>/dev/null || echo "")
if ! docker pull -q "$ref" >/dev/null 2>&1; then
log "pull failed: $ref"
continue
fi
after=$(docker inspect --format '{{.Id}}' "$ref")
if [[ "$before" != "$after" ]]; then
log "new image for $svc: ${before:-<none>} -> $after"
changed+=("$svc")
fi
# Always keep the un-prefixed tag pointing at the fresh image, so a stale
# local retag can't wedge us.
docker tag "$ref" "$local_ref"
done
if [[ ${#changed[@]} -eq 0 ]]; then
exit 0
fi
log "rolling: ${changed[*]}"
cd "$COMPOSE_DIR"
docker compose up -d "${changed[@]}"
log "roll complete: ${changed[*]}"
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Clawmates rolling deploy — every minute
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
AccuracySec=15s
Unit=clawmates-deploy.service
[Install]
WantedBy=timers.target