Files
redefined-designs/.gitea/workflows/qa-build.yml
T
bermudalambandClaude Opus 5 711f59c0a1
SonarQube Analysis / sonarqube (pull_request) Successful in 2m40s
Tests / backend-unit (pull_request) Successful in 48s
Tests / frontend-e2e (pull_request) Canceled after 0s
Tests / backend-integration (pull_request) Canceled after 34m15s
ci: distinguish why the QA build daemon is unreachable (#25)
The first dispatch failed at the wait step with nothing to act on. The
run logs showed the dind service container was never created — no service
startup output at all, and teardown reporting "No such container" for the
ID it had recorded — which is what act_runner does when it refuses a
privileged container.

From the failing step, that is indistinguishable from dockerd simply
being slow, so the step now says which one it is: if the service host
resolves, the container exists and dockerd is not serving plain TCP on
2375; if it does not resolve, the service never started and the runner
needs container.privileged.

Also raises the wait from 30s to 90s. The NAS took the full 30s before
failing, so the old ceiling was too close to the observed time to
distinguish slow from broken.

The docker CLI fallback is retained: the runner image has no docker
binary, and the static install worked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 08:12:11 -05:00

169 lines
7.3 KiB
YAML

name: Build QA Image
# Manual only. Builds the QA image from a chosen ref, pushes it to the Gitea
# container registry, and emails when it is ready to redeploy.
#
# It deliberately does NOT restart the QA stack. Redeploying stays a human
# action in Portainer, so nothing changes what is running without someone
# deciding it should.
on:
workflow_dispatch:
inputs:
ref:
description: Branch, tag, or commit to build
required: true
default: main
env:
IMAGE: gitea.bermudalamb.synology.me/bermudalamb/redefined-designs
# The build runs against a Docker-in-Docker service rather than the host
# daemon. Mounting the host socket into the runner would give every workflow
# on every branch root-equivalent control of the NAS, production included.
# Because the image is pushed to a registry, it does not need to survive in
# the build daemon.
DOCKER_HOST: tcp://docker:2375
jobs:
build:
runs-on: ubuntu-latest
services:
docker:
image: docker:27-dind
options: --privileged
env:
DOCKER_TLS_CERTDIR: ""
steps:
- name: Check required configuration
run: |
missing=""
[ -n "${{ secrets.REGISTRY_TOKEN }}" ] || missing="$missing REGISTRY_TOKEN"
[ -n "${{ vars.REGISTRY_USER }}" ] || missing="$missing REGISTRY_USER"
[ -n "${{ secrets.BREVO_API_KEY }}" ] || missing="$missing BREVO_API_KEY"
[ -n "${{ vars.QA_NOTIFY_TO }}" ] || missing="$missing QA_NOTIFY_TO"
[ -n "${{ vars.QA_NOTIFY_FROM }}" ] || missing="$missing QA_NOTIFY_FROM"
if [ -n "$missing" ]; then
echo "::error::Missing configuration:$missing"
echo "Secrets go in Settings > Actions > Secrets; variables in Settings > Actions > Variables."
exit 1
fi
echo "All required secrets and variables are present."
- name: Checkout ${{ inputs.ref }}
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Ensure a docker CLI is available
run: |
if command -v docker >/dev/null 2>&1; then
echo "docker CLI already present: $(docker --version)"
exit 0
fi
echo "docker CLI missing from the runner image; installing the static binary."
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz -o /tmp/docker.tgz
tar -xzf /tmp/docker.tgz -C /tmp
install -m 0755 /tmp/docker/docker /usr/local/bin/docker
docker --version
- name: Wait for the build daemon
run: |
# dind needs a privileged service container, which is a runner-wide
# setting this workflow cannot check in advance. The NAS is also slow
# to start one, so allow well over the observed time before giving up.
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
echo "Build daemon reachable after ${i}s."
exit 0
fi
sleep 1
done
echo "::error::No Docker daemon at $DOCKER_HOST after 90s."
echo ""
# These two cases look identical from the failing step but have
# completely different fixes, so name which one it is.
if getent hosts docker >/dev/null 2>&1; then
echo "The 'docker' service host resolves, so the container exists but"
echo "dockerd is not accepting connections on 2375. Check that"
echo "DOCKER_TLS_CERTDIR is empty, so dind serves plain TCP rather"
echo "than TLS on 2376."
else
echo "The 'docker' service host does not resolve, so the service"
echo "container never started. This is what act_runner does when it"
echo "refuses a privileged container: it allocates an ID, creation"
echo "fails, and the job continues with nothing listening."
echo ""
echo "Set 'container.privileged: true' in the act_runner config.yaml"
echo "and restart the runner. Check the runner's own logs to confirm."
fi
exit 1
- name: Record what is being built
id: meta
run: |
echo "sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
echo "full_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
{
echo "subject<<EOF"
git log -1 --pretty=%s
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Log in to the Gitea registry
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" \
| docker login gitea.bermudalamb.synology.me \
-u "${{ vars.REGISTRY_USER }}" --password-stdin
- name: Build and push
run: |
# Tagged twice: :qa is what the stack pulls, and the commit tag makes
# it possible to tell what is actually deployed and to roll back to a
# specific build rather than "the previous one".
docker build --no-cache \
-t "$IMAGE:qa" \
-t "$IMAGE:${{ steps.meta.outputs.sha }}" \
.
docker push "$IMAGE:qa"
docker push "$IMAGE:${{ steps.meta.outputs.sha }}"
- name: Email that QA is ready to redeploy
run: |
cat > /tmp/mail.json <<JSON
{
"sender": { "email": "${{ vars.QA_NOTIFY_FROM }}", "name": "Redefined Designs CI" },
"to": [ { "email": "${{ vars.QA_NOTIFY_TO }}" } ],
"subject": "QA image ready — ${{ inputs.ref }} @ ${{ steps.meta.outputs.sha }}",
"htmlContent": "<p>A QA image has been built and pushed.</p><ul><li><b>Ref:</b> ${{ inputs.ref }}</li><li><b>Commit:</b> ${{ steps.meta.outputs.sha }}</li><li><b>Subject:</b> ${{ steps.meta.outputs.subject }}</li></ul><p><b>To deploy it:</b> open the <code>redefined-designs-qa</code> stack in Portainer and redeploy with <i>Pull latest image</i> enabled.</p><p>Migrations run automatically as the container starts — check <code>docker logs redefined-designs-qa-syn</code> shows the migration output before <code>listening on 3000</code>, and that it appears only once.</p>"
}
JSON
code=$(curl -sS -o /tmp/mail-response.json -w '%{http_code}' \
-X POST https://api.brevo.com/v3/smtp/email \
-H "api-key: ${{ secrets.BREVO_API_KEY }}" \
-H "Content-Type: application/json" \
--data @/tmp/mail.json)
echo "Brevo responded $code"
if [ "$code" -ge 300 ]; then
cat /tmp/mail-response.json
# The image is already pushed and usable at this point, so a failed
# notification must not report the build as failed.
echo "::warning::Image pushed successfully, but the notification email failed."
fi
- name: Summary
run: |
{
echo "### QA image pushed"
echo ""
echo "| | |"
echo "|---|---|"
echo "| Ref | \`${{ inputs.ref }}\` |"
echo "| Commit | \`${{ steps.meta.outputs.full_sha }}\` |"
echo "| Tags | \`$IMAGE:qa\`, \`$IMAGE:${{ steps.meta.outputs.sha }}\` |"
echo ""
echo "Redeploy the \`redefined-designs-qa\` stack in Portainer with **Pull latest image** enabled."
} >> "$GITHUB_STEP_SUMMARY"