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>
169 lines
7.3 KiB
YAML
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"
|