ci: build QA in Portainer from a git stack, drop the build workflow
The QA image is now built by Portainer from this repository rather than by Gitea Actions. Deployed as a Git repository stack, "Pull and redeploy" pulls the repo, builds from the Dockerfile, and recreates the containers in one action. This removes the runner from the loop entirely. Three dispatches failed without ever building: the runner refuses privileged containers, so the dind service was never created. Working around that needed either the host Docker socket mounted into the runner or privileged containers enabled runner-wide, and both hand every workflow on every branch root-equivalent control of the NAS, production included. Portainer already holds the socket — that is how it manages containers — so building there needs no new privilege at all. pull_policy: build is what keeps it honest. Without it the stack reuses whatever is tagged redefined-designs:qa, which is exactly how a redeploy appears to succeed while still serving old code — a failure this project has already hit twice. Deleting qa-build.yml also drops the registry, the REGISTRY_TOKEN and BREVO_API_KEY secrets, and the notification email. The email existed because CI worked asynchronously and had to tell you when it finished; redeploying from Portainer is synchronous, so the browser already does. Losing the per-commit image tags is a real cost — rollback becomes "rebuild from the ref you want" rather than retagging a specific build. README changes for this are deliberately not in this commit: that file also carries uncommitted work of Thom's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,175 +0,0 @@
|
||||
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 "${{ secrets.BREVO_API_KEY }}" ] || missing="$missing BREVO_API_KEY"
|
||||
missingVars=""
|
||||
[ -n "${{ vars.REGISTRY_USER }}" ] || missing="$missing REGISTRY_USER"
|
||||
[ -n "${{ vars.QA_NOTIFY_TO }}" ] || missing="$missing QA_NOTIFY_TO"
|
||||
[ -n "${{ vars.QA_NOTIFY_FROM }}" ] || missing="$missing QA_NOTIFY_FROM"
|
||||
if [ -n "$missing" || -n "$missingVars" ]; then
|
||||
if [ -n "$missing" ]; then
|
||||
echo "::error::Missing configuration secrets:$missing"
|
||||
echo "Secrets go in Settings > Actions > Secrets"
|
||||
fi
|
||||
if [ -n "$missingVars" ]; then
|
||||
echo "::error::Missing configuration variables:$missingVars"
|
||||
echo "Variables go in Settings > Actions > Variables."
|
||||
fi
|
||||
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"
|
||||
+29
-17
@@ -13,27 +13,39 @@
|
||||
# against each other — it would happily remove the production containers
|
||||
# because they are not declared in this file.
|
||||
#
|
||||
# Required stack environment variable:
|
||||
# DEPLOY THIS AS A GIT REPOSITORY STACK, not from the web editor.
|
||||
#
|
||||
# Repository: https://gitea.bermudalamb.synology.me/bermudalamb/redefined-designs
|
||||
# Reference: refs/heads/main
|
||||
# Compose path: docker-compose.qa.yml
|
||||
#
|
||||
# The repository is public, so no credentials are needed. Portainer then does
|
||||
# the whole cycle from one button: it pulls the repo, builds the image from the
|
||||
# Dockerfile below, and recreates the containers. Nothing is built by hand on
|
||||
# the NAS, and no image has to be pushed anywhere first.
|
||||
#
|
||||
# `pull_policy: build` matters. Without it the stack reuses whatever is already
|
||||
# tagged redefined-designs:qa, which is how a redeploy can appear to succeed
|
||||
# while still running old code. Leave any Portainer option that re-pulls images
|
||||
# turned OFF — there is no registry to pull this image from.
|
||||
#
|
||||
# Required stack environment variables:
|
||||
# QA_DB_PASSWORD — deliberately not named DB_PASSWORD, so pasting the
|
||||
# production stack's variables here does nothing silently.
|
||||
#
|
||||
# The image comes from the Gitea container registry, built by the manual
|
||||
# "Build QA Image" workflow. Redeploy this stack with Portainer's
|
||||
# "Pull latest image" toggle ON, or it will keep running the image it already
|
||||
# has and the redeploy will appear to do nothing.
|
||||
#
|
||||
# Portainer needs registry credentials for gitea.bermudalamb.synology.me once
|
||||
# (Registries > Add registry, custom, with a Gitea token that has read:package).
|
||||
#
|
||||
# This replaced a locally-built `redefined-designs:qa` with `pull_policy: never`.
|
||||
# That arrangement meant the image existed only if someone had remembered to
|
||||
# build it, which produced two confusing deploy failures: a "pull access denied"
|
||||
# from Docker Hub when the tag was missing entirely, and a silent stale-image
|
||||
# deploy when the build had not been rerun.
|
||||
# PUBLIC_URL — the QA hostname, e.g.
|
||||
# https://qa-redefined-designs.bermudalamb.synology.me
|
||||
|
||||
services:
|
||||
redefined-designs-qa:
|
||||
image: gitea.bermudalamb.synology.me/bermudalamb/redefined-designs:qa
|
||||
# Built from this repository by Portainer rather than pulled. The context is
|
||||
# the repo root, which is where the Dockerfile lives — the same Dockerfile
|
||||
# production uses, so QA and production images differ only in configuration.
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: redefined-designs:qa
|
||||
# Always build; never reuse the existing tag.
|
||||
pull_policy: build
|
||||
container_name: redefined-designs-qa-syn
|
||||
environment:
|
||||
- TZ=America/Chicago
|
||||
@@ -57,7 +69,7 @@ services:
|
||||
# a fixture ever contains a real address.
|
||||
- SITE_CURRENCY=USD
|
||||
- RESERVATION_MINUTES=15
|
||||
- PUBLIC_URL=https://qa-redefined-designs.bermudalamb.synology.me
|
||||
- PUBLIC_URL=${PUBLIC_URL}
|
||||
volumes:
|
||||
# Separate uploads directory. Sharing production's would let a QA run
|
||||
# write into, and a QA teardown delete, real product images.
|
||||
|
||||
Reference in New Issue
Block a user