diff --git a/.gitea/workflows/qa-build.yml b/.gitea/workflows/qa-build.yml deleted file mode 100644 index 690fa1c..0000000 --- a/.gitea/workflows/qa-build.yml +++ /dev/null @@ -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<> "$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 1aaad97..bdd759d 100644 --- a/docker-compose.qa.yml +++ b/docker-compose.qa.yml @@ -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.