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: |
# A privileged service container is the one runner capability this
# workflow cannot verify in advance. Fail here with an explanation
# rather than at `docker build` with a connection refused.
for i in $(seq 1 30); 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 30s."
echo "The dind service needs privileged containers. If the runner"
echo "forbids them, this workflow cannot build without host socket access."
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.