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<> "$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 <A QA image has been built and pushed.

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.

" } 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"