Files
myAi/.gitea/workflows/build.yml
T
claude 4e09f27e4d
Build and Push Docker Images / build (push) Successful in 14s
Build and Push Docker Images / smoke (push) Successful in 0s
ci: trigger Watchtower instead of waiting for its poll
The poll is the fallback, not the mechanism. It is 30s on staging but 300s
on production, so a green run could sit five minutes ahead of the deploy it
claimed to have made -- the smoke job was absorbing that wait.

Copied from easyDent verbatim, including the soft failure: an unset secret
or an unreachable API logs and falls back to the poll rather than failing
the build. That matters right now because production has no Watchtower HTTP
API yet, so the production URL will do nothing until the infra stack there
is updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpTYCBLH58XzrM7n3xPJ5N
2026-08-24 11:01:35 +00:00

205 lines
8.6 KiB
YAML

name: Build and Push Docker Images
# Branch-driven deploys — no yaml edits to switch environment:
# merge into `staging` -> tag :staging (staging Watchtower deploys)
# merge into `production` -> tag :production (production Watchtower deploys)
# `main` is the day-to-day work branch and deploys nothing.
on:
push:
branches:
- staging
- production
env:
GIT_HOST: docker-git.easysoft.ro
REGISTRY_HOST: registry.easysoft.ro
DOCKER_BUILDKIT: "1"
API_IMAGE: apps/myai-api
CV_MATCHER_API_IMAGE: apps/myai-cv-matcher-api
RAG_API_IMAGE: apps/myai-rag-api
EMAIL_API_IMAGE: apps/myai-email-api
WEB_IMAGE: apps/myai-web
CV_CLEANUP_JOB_IMAGE: apps/myai-cv-cleanup-job
CV_SEARCH_JOB_IMAGE: apps/myai-cv-search-job
PAGE_FETCHER_API_IMAGE: apps/myai-page-fetcher-api
IMAGE_TAG: ${{ github.ref_name }} # branch name == image tag (staging | production)
WEB_PORT: "5140" # host port the web container is published on
jobs:
build:
runs-on: host
steps:
- name: Checkout the pushed commit
env:
TOKEN: ${{ secrets.REPO_TOKEN }}
run: |
git clone "http://gelu:${TOKEN}@${GIT_HOST}:3000/${GITHUB_REPOSITORY}.git" .
git checkout "${{ github.sha }}"
- name: Login to registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY_HOST}" \
-u "${{ secrets.REGISTRY_USER }}" \
--password-stdin
- name: Build API image
run: |
docker build -f Apis/api/Dockerfile -t "${REGISTRY_HOST}/${API_IMAGE}:${IMAGE_TAG}" .
- name: Build CV Matcher API image
run: |
docker build -f Apis/cv-matcher-api/Dockerfile -t "${REGISTRY_HOST}/${CV_MATCHER_API_IMAGE}:${IMAGE_TAG}" .
- name: Build RAG API image
run: |
docker build -f Apis/rag-api/Dockerfile -t "${REGISTRY_HOST}/${RAG_API_IMAGE}:${IMAGE_TAG}" .
- name: Build Email API image
run: |
docker build -f Apis/email-api/Dockerfile -t "${REGISTRY_HOST}/${EMAIL_API_IMAGE}:${IMAGE_TAG}" .
- name: Build Web image
run: |
docker build --build-arg GIT_SHA="${{ github.sha }}" \
-f web/Dockerfile -t "${REGISTRY_HOST}/${WEB_IMAGE}:${IMAGE_TAG}" .
- name: Build CV cleanup job image
run: |
docker build -f Jobs/cv-cleanup-job/Dockerfile -t "${REGISTRY_HOST}/${CV_CLEANUP_JOB_IMAGE}:${IMAGE_TAG}" .
- name: Build CV search job image
run: |
docker build -f Jobs/cv-search-job/Dockerfile -t "${REGISTRY_HOST}/${CV_SEARCH_JOB_IMAGE}:${IMAGE_TAG}" .
- name: Build Page Fetcher API image
run: |
docker build -f Apis/page-fetcher-api/Dockerfile -t "${REGISTRY_HOST}/${PAGE_FETCHER_API_IMAGE}:${IMAGE_TAG}" .
- name: Push API image
run: |
docker push "${REGISTRY_HOST}/${API_IMAGE}:${IMAGE_TAG}"
- name: Push CV Matcher API image
run: |
docker push "${REGISTRY_HOST}/${CV_MATCHER_API_IMAGE}:${IMAGE_TAG}"
- name: Push RAG API image
run: |
docker push "${REGISTRY_HOST}/${RAG_API_IMAGE}:${IMAGE_TAG}"
- name: Push Email API image
run: |
docker push "${REGISTRY_HOST}/${EMAIL_API_IMAGE}:${IMAGE_TAG}"
- name: Push Web image
run: |
docker push "${REGISTRY_HOST}/${WEB_IMAGE}:${IMAGE_TAG}"
- name: Push CV cleanup job image
run: |
docker push "${REGISTRY_HOST}/${CV_CLEANUP_JOB_IMAGE}:${IMAGE_TAG}"
- name: Push CV search job image
run: |
docker push "${REGISTRY_HOST}/${CV_SEARCH_JOB_IMAGE}:${IMAGE_TAG}"
- name: Push Page Fetcher API image
run: |
docker push "${REGISTRY_HOST}/${PAGE_FETCHER_API_IMAGE}:${IMAGE_TAG}"
# Watchtower's poll is the fallback, not the mechanism: 30s on staging but 300s on
# production, so without this a green run can sit five minutes ahead of the deploy it
# claims to have made. Copied from easyDent, including the soft failure -- an unset
# secret or an unreachable API must degrade to the poll, never fail the build.
- name: Trigger Watchtower redeploy
env:
URL_STAGING: ${{ secrets.WATCHTOWER_URL_STAGING }}
URL_PRODUCTION: ${{ secrets.WATCHTOWER_URL_PRODUCTION }}
TOKEN: ${{ secrets.WATCHTOWER_TOKEN }}
run: |
if [ "${IMAGE_TAG}" = "production" ]; then URL="${URL_PRODUCTION}"; else URL="${URL_STAGING}"; fi
if [ -n "${URL}" ] && [ -n "${TOKEN}" ]; then
echo "Triggering Watchtower at ${URL}"
curl -sf -m 30 -H "Authorization: Bearer ${TOKEN}" "${URL}" && echo " -> redeploy triggered" \
|| echo " -> trigger failed; Watchtower will still pick it up on the next poll"
else
echo "Watchtower push-trigger not configured (WATCHTOWER_* secrets unset); relying on the poll interval."
fi
- name: Reclaim disk space (keep recent build cache)
if: always()
run: |
docker image prune -f # dangling only (keep base images)
# Building and pushing an image proves nothing about what the host is running.
# Watchtower pulls asynchronously, and for a month it was pulling a tag nobody
# intended -- with every run green, because no step ever asked the deployed site
# what it was serving. This job asks.
#
# It polls the deploy host directly on the LAN rather than the public hostname:
# the runner sits inside the network, only easysoft.ro has a staging equivalent in
# public DNS, and going direct also takes Caddy and any CDN out of the answer.
smoke:
runs-on: host
needs: build
steps:
- name: Wait for the deploy host to serve this commit
run: |
case "${{ github.ref_name }}" in
staging) HOST=192.168.1.111 ;;
production) HOST=192.168.1.101 ;;
*) echo "::error::No deploy host mapped for '${{ github.ref_name }}'."; exit 1 ;;
esac
URL="http://${HOST}:${WEB_PORT}/version.json"
echo "Polling ${URL} for ${{ github.sha }}"
# 10 minutes: Watchtower's poke is fire-and-forget with a 30s fallback poll,
# and the container still has to start.
# ⚠️ Steps run under `bash -e -o pipefail`, so a polling loop has to be written
# defensively: the FIRST miss is the normal case, not an error.
# - `curl -sf | sed` fails the whole pipeline under pipefail while the old
# container is still up (404/connection refused), so `|| GOT=""` is required
# - `[ test ] && { ... }` returns non-zero when the test fails, which under -e
# aborts the step. Use `if`.
# Getting both wrong made the first run of this job fail in 20 seconds.
DEADLINE=$(( $(date +%s) + 600 ))
while :; do
GOT=$(curl -sf -m 15 "${URL}" 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') || GOT=""
if [ "${GOT}" = "${{ github.sha }}" ]; then
echo "Serving ${GOT}."
break
fi
if [ "$(date +%s)" -ge "${DEADLINE}" ]; then
echo "::error::Timed out after 10m. ${HOST} is serving '${GOT:-nothing}', wanted ${{ github.sha }}."
echo "Either Watchtower never pulled the new image, the container failed to"
echo "start, or the stack's IMAGE_TAG does not match this branch."
exit 1
fi
echo " still serving '${GOT:-nothing}' ..."
sleep 15
done
- name: Check the site actually answers
run: |
case "${{ github.ref_name }}" in
staging) HOST=192.168.1.111 ;;
production) HOST=192.168.1.101 ;;
esac
# `-L` follows redirects and we assert on the FINAL code, because a 302 from `/`
# is a healthy answer for a site running in UnderConstruction mode -- it means the
# app is up and routing. Asserting a bare 200 failed jecreativ.ro's first
# production deploy for doing exactly what it was configured to do.
#
# `|| CODE=000` for the same reason as above: curl exiting non-zero on a
# connection failure must produce a reportable code, not kill the step before
# it can say what went wrong. (`-s` without `-f` already tolerates 4xx/5xx.)
CODE=$(curl -sL -o /dev/null -w '%{http_code}' -m 20 "http://${HOST}:${WEB_PORT}/") || CODE=000
if [ "${CODE}" != "200" ]; then
echo "::error::Home page returned ${CODE}."
exit 1
fi
echo "Home page 200."