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<
To deploy it: open the redefined-designs-qa stack in Portainer and redeploy with Pull latest image enabled.
Migrations run automatically as the container starts — check docker logs redefined-designs-qa-syn shows the migration output before listening on 3000, and that it appears only once.