diff --git a/.gitea/workflows/qa-build.yml b/.gitea/workflows/qa-build.yml new file mode 100644 index 0000000..db2659b --- /dev/null +++ b/.gitea/workflows/qa-build.yml @@ -0,0 +1,152 @@ +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" diff --git a/docker-compose.qa.yml b/docker-compose.qa.yml index 0ae7831..1aaad97 100644 --- a/docker-compose.qa.yml +++ b/docker-compose.qa.yml @@ -17,23 +17,23 @@ # QA_DB_PASSWORD — deliberately not named DB_PASSWORD, so pasting the # production stack's variables here does nothing silently. # -# BUILD THE IMAGE BEFORE DEPLOYING THIS STACK. redefined-designs:qa exists only -# on the NAS and is never pushed to a registry, so deploying first makes Compose -# fall back to pulling from Docker Hub and fail with a misleading -# "pull access denied ... repository does not exist or may require docker login". +# 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. # -# sudo docker build --no-cache -t redefined-designs:qa /volume1/docker/redefined-designs +# Portainer needs registry credentials for gitea.bermudalamb.synology.me once +# (Registries > Add registry, custom, with a Gitea token that has read:package). # -# For the same reason, leave Portainer's "Pull latest image" toggle off. -# `pull_policy: never` below makes a missing image report itself as missing -# rather than as a registry authentication problem. If the Docker Compose -# version on the NAS ever rejects that key, it is safe to delete the line — it -# only improves the error message. +# 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. services: redefined-designs-qa: - image: redefined-designs:qa - pull_policy: never + image: gitea.bermudalamb.synology.me/bermudalamb/redefined-designs:qa container_name: redefined-designs-qa-syn environment: - TZ=America/Chicago