feat(deploy): production always runs a numbered version

Policy set by _null. The stack pins :vX.Y.Z — never :dev, never :latest.

The reason is what a pointer cannot do. A stack file naming :dev says "dev"
today and will still say "dev" after the image underneath it changes, so the
file records a preference rather than a fact and "what is running?" has no
answer that survives the next push. Rollback is worse: there is nothing to roll
back TO, because a pointer only ever names now.

deploy.sh: with no --tag it now resolves the newest published NUMBERED version
from the registry and says which it picked, instead of defaulting to :dev. A
floating tag is refused outright, with --allow-floating as the loud escape hatch
for the one case that is not a mistake — reproducing a fault on whatever a
pointer currently is.

release.sh: no longer moves :dev. It published both tags until now because the
stack followed the pointer; under this policy nothing deploys :dev, so moving it
would only publish something that misrepresents what is running.

scripts/docker-push.sh deleted, with its npm script. Its entire job was building
and pushing an unnumbered :dev with no version bump and no guards, which is now
the exact thing the policy exists to prevent. Keeping a command that quietly
violates a policy is how the policy stops being true.

:dev and :latest stay in the registry, frozen. Not deleted: the running
container was created from :dev, and removing the tag an existing deployment
names is how a recreate fails to pull.

Stack 58 still pins :dev as this lands. Correcting that is the first deploy made
under the policy, and it is why the default resolves a version rather than
reusing whatever the stack already names.

Guards proven: default resolves v0.8.3; --tag dev refused; --tag latest refused;
--allow-floating warns and proceeds; unpublished tag still refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Neo 2026-08-18 02:54:42 -05:00
parent 6a890ad8bc
commit 1a3ba305a7
9 changed files with 152 additions and 96 deletions

View File

@ -249,20 +249,46 @@ There is no non-production environment to rehearse against. `deploy.sh` takes a
verified backup before the irreversible part for that reason, and refuses to verified backup before the irreversible part for that reason, and refuses to
continue if the backup fails. continue if the backup fails.
**Deploy a version, not the pointer, when it matters.** `npm run deploy` follows ### Production always runs a numbered version
`:dev`, which is a moving tag: afterwards nothing in the stack file says which
image that was. `npm run deploy -- --tag v0.9.4` pins an immutable version, which **Policy, set 2026-08-18.** The stack pins `:vX.Y.Z`. Never `:dev`, never
is what you want when somebody asks what is running, and what you need to roll `:latest`, never any other pointer.
back:
The reason is what a pointer cannot do. A stack file naming `:dev` says `dev`
today and will still say `dev` after the image underneath it changes — so the
file records a preference rather than a fact, and "what is running?" has no
answer that survives the next push. Rollback is worse: there is nothing to roll
back *to*, because a pointer only ever names now.
`deploy.sh` enforces it. With no `--tag` it resolves the **newest published
numbered version** from the registry and says which it picked; given a floating
tag it refuses:
```bash ```bash
bash scripts/deploy.sh --tag v0.8.3 --dry-run # see the change first npm run deploy # newest published vX.Y.Z
bash scripts/deploy.sh --tag v0.8.3 # roll back to a known image npm run deploy -- --tag v0.9.4 --dry-run # a specific version, previewed
npm run deploy -- --tag v0.9.4 # deploy it
npm run deploy -- --tag v0.8.3 # roll back to a known image
``` ```
It refuses a tag the registry does not hold — a missing tag would otherwise `--allow-floating` exists for the one case that is not a mistake — reproducing a
become an outage, since Portainer accepts the file, fails to pull, and leaves fault on whatever a pointer currently is — and warns every time.
the container stopped.
It also refuses a tag the registry does not hold. A missing tag would otherwise
become an outage: Portainer accepts the file, fails to pull, and leaves the
container stopped.
**Stack 58 pins `:dev` as this is written**, which is the state the policy
corrects. The first deploy made under it repoints the stack at a version, and
after that the stack file answers the question on its own.
`:dev` and `:latest` remain in the registry, frozen. They are not deleted
because the running container was created from `:dev`, and removing the tag an
existing deployment names is how a recreate fails to pull. Nothing publishes to
them any more — `release.sh` stopped moving `:dev` when this policy was set, and
scripts/docker-push.sh — named without backticks because it is gone — was
deleted, its only job having been to push an unnumbered `:dev` with no version
bump and no guards.
**What `deploy.sh` preserves, and why that is the dangerous part.** Portainer **What `deploy.sh` preserves, and why that is the dangerous part.** Portainer
treats a stack `PUT` as the whole desired state. Sending it without the stack's treats a stack `PUT` as the whole desired state. Sending it without the stack's

View File

@ -127,9 +127,9 @@ Run from the repository root.
| `npm start` / `npm run server` | the Express server alone, serving `dist/` | | `npm start` / `npm run server` | the Express server alone, serving `dist/` |
| `npm run docker:build` / `docker:run` | build and run the image locally | | `npm run docker:build` / `docker:run` | build and run the image locally |
| `npm run docker:compose:up` / `:down` / `:logs` | the compose stack | | `npm run docker:compose:up` / `:down` / `:logs` | the compose stack |
| `npm run release` | **publish an image.** Bump, guards, build, verify its version label, push `:vX.Y.Z` and move `:dev`, commit last, tag. `-- --dry-run` first | | `npm run release` | **publish an image.** Bump, guards, build, verify its version label, push `:vX.Y.Z`, commit last, tag. `-- --dry-run` first |
| `npm run deploy` | **move the running stack** to a published image. Backs up first. `-- --dry-run` first, `-- --tag vX.Y.Z` to pin or roll back | | `npm run deploy` | **move the running stack** to a published **numbered** version — the newest by default, `-- --tag vX.Y.Z` to pin or roll back. Backs up first, and refuses a floating tag. `-- --dry-run` first |
| `npm run docker:push` | build, tag and push `queue-north-website:dev` with no bump and no guards. Superseded by `npm run release` |
| `npm run docker:test` | build the image and smoke-test it on 3001 | | `npm run docker:test` | build the image and smoke-test it on 3001 |
**There is no `npm test`, and that is not an omission in this table.** There is **There is no `npm test`, and that is not an omission in this table.** There is

View File

@ -304,9 +304,9 @@ one, and never report progress the API did not return.
- **Not the work list.** That is the tracker: milestones are batches, issues are - **Not the work list.** That is the tracker: milestones are batches, issues are
deliverables. deliverables.
- **Not the release procedure.** This project has no `release.sh` — publishing - **Not the release procedure.** That is `npm run release` to publish and
is `npm run docker:push`, and how nebula moves to a new image is not yet `npm run deploy` to move the stack — two commands on purpose. `scripts/release.sh`
written down. `docs/OPERATIONS.md` records that gap; and `docs/OPERATIONS.md` carry the detail, and
`docs/security/SECURITY_CHECKLIST.md` is the list to work through first. `docs/security/SECURITY_CHECKLIST.md` is the list to work through first.
- **Not the QA procedure.** That is `docs/qa/ClaudeQAPlan.md`, which ends in its - **Not the QA procedure.** That is `docs/qa/ClaudeQAPlan.md`, which ends in its
own version of step 7. own version of step 7.

View File

@ -150,9 +150,9 @@ each row says what it does *here*.
| `scripts/preflight.sh` | headers and TLS against the live origin. No `--auth` checks: there are no accounts | | `scripts/preflight.sh` | headers and TLS against the live origin. No `--auth` checks: there are no accounts |
| `scripts/backup.sh` | a verified SQLite dump. Its ENGINE block was rewritten for better-sqlite3's online `.backup()` — see below | | `scripts/backup.sh` | a verified SQLite dump. Its ENGINE block was rewritten for better-sqlite3's online `.backup()` — see below |
| `scripts/restore-check.sh` | restores the newest dump into a scratch file, runs `PRAGMA integrity_check`, counts tables, and **times it**. A backup nobody has restored is a guess | | `scripts/restore-check.sh` | restores the newest dump into a scratch file, runs `PRAGMA integrity_check`, counts tables, and **times it**. A backup nobody has restored is a guess |
| `scripts/release.sh` | **publishes.** Bump, guard, build, verify the image's own version label, push `:vX.Y.Z` **and** move `:dev`, commit last, tag. Refuses to overwrite a published tag, to build on a dirty tree, or to build when the three hard-coded copies of the public origin disagree. Adapted from PrivacyLLC-Web's — see below | | `scripts/release.sh` | **publishes.** Bump, guard, build, verify the image's own version label, push `:vX.Y.Z`, commit last, tag. Refuses to overwrite a published tag, to build on a dirty tree, or to build when the three hard-coded copies of the public origin disagree. Adapted from PrivacyLLC-Web's — see below |
| `scripts/deploy.sh` | **deploys, and does not build.** Points Portainer stack 58 at an already-published tag, takes a verified backup first, preserves the stack's twelve environment variables, then waits for health and checks both public origins. Reports the digest before and after | | `scripts/deploy.sh` | **deploys, and does not build.** Points Portainer stack 58 at an already-published **numbered** version — it refuses a floating tag — taking a verified backup first and preserving the stack's twelve environment variables, then waits for health and checks both public origins. Reports the digest before and after |
| `scripts/docker-push.sh` | builds and pushes `queue-north-website:dev` only, with no version bump and no guards. Predates the template and is **superseded by `release.sh`** — kept for now because it is what the muscle memory reaches for |
| `scripts/docker-test.sh` | builds the image and runs it locally on 3001. Predates the template | | `scripts/docker-test.sh` | builds the image and runs it locally on 3001. Predates the template |
| `scripts/prerender.js` | the build step that emits static HTML for every route. Predates the template | | `scripts/prerender.js` | the build step that emits static HTML for every route. Predates the template |
@ -171,9 +171,9 @@ each is a fact about this project rather than a preference:
The original refuses to release on a half-run 1,600-test run; this one says out The original refuses to release on a half-run 1,600-test run; this one says out
loud that a build, a secret scan and a doc-header check are not tests and that loud that a build, a secret scan and a doc-header check are not tests and that
nothing in the gate exercised a route, a form or an API response. nothing in the gate exercised a route, a form or an API response.
- **It moves `:dev` as well as publishing `:vX.Y.Z`,** because stack 58 follows - **It publishes one tag and does not move `:dev`.** Both scripts follow the
`:dev`. That makes `:dev` a pointer and never evidence — `status.sh` reads the policy below: production always runs a numbered version, so nothing deploys a
digest and the version label for that. pointer and publishing one would only misrepresent what is running.
- **It checks the public origin in three files rather than one.** The original - **It checks the public origin in three files rather than one.** The original
passes its origin in as a build arg, so it has one copy to validate. Here passes its origin in as a build arg, so it has one copy to validate. Here
`https://queuenorth.com` is written out in `src/lib/seo.js`, `https://queuenorth.com` is written out in `src/lib/seo.js`,

View File

@ -113,6 +113,7 @@ Observable, in this order:
- **No automated tests at all.** No test runner, no typecheck. `npm run build` is - **No automated tests at all.** No test runner, no typecheck. `npm run build` is
the only gate, and it catches syntax and imports rather than behaviour. Recorded the only gate, and it catches syntax and imports rather than behaviour. Recorded
as a standing gap in `docs/qa/ClaudeQACoverage.md`. as a standing gap in `docs/qa/ClaudeQACoverage.md`.
- **The roll-forward procedure is not written down.** Publishing is - **Deployment is scripted but has never been run end to end.** `npm run
`scripts/docker-push.sh`; how nebula moves to a new image is currently in release` publishes and `npm run deploy` moves Portainer stack 58, both added
somebody's head. Noted in `docs/OPERATIONS.md`. 2026-08-18 and both exercised only in `--dry-run`. The first real run of either
is still ahead.

View File

@ -18,7 +18,6 @@
"docker:compose:up": "docker-compose up -d", "docker:compose:up": "docker-compose up -d",
"docker:compose:down": "docker-compose down", "docker:compose:down": "docker-compose down",
"docker:compose:logs": "docker-compose logs -f", "docker:compose:logs": "docker-compose logs -f",
"docker:push": "bash scripts/docker-push.sh",
"docker:test": "bash scripts/docker-test.sh" "docker:test": "bash scripts/docker-test.sh"
}, },
"dependencies": { "dependencies": {

View File

@ -2,8 +2,9 @@
# #
# Move the running stack to an image that is already published. # Move the running stack to an image that is already published.
# #
# bash scripts/deploy.sh # deploy whatever :dev points at # bash scripts/deploy.sh # newest published vX.Y.Z
# bash scripts/deploy.sh --tag v0.9.4 # pin a specific published version # bash scripts/deploy.sh --tag v0.9.4 # a specific published version
# bash scripts/deploy.sh --allow-floating --tag dev # chase a pointer, loudly
# bash scripts/deploy.sh --dry-run # show the change, touch nothing # bash scripts/deploy.sh --dry-run # show the change, touch nothing
# bash scripts/deploy.sh --no-backup # skip the pre-deploy dump (say why) # bash scripts/deploy.sh --no-backup # skip the pre-deploy dump (say why)
# #
@ -36,15 +37,25 @@
# stack update does not remove one, but "does not normally" is not a reason to # stack update does not remove one, but "does not normally" is not a reason to
# skip the cheap thing before the irreversible one. # skip the cheap thing before the irreversible one.
# #
# ## Why :dev is the default and a version tag is better # ## THE POLICY: production always runs a numbered version
# #
# Stack 58 pins `:dev`, which is a moving pointer. Deploying it means "whatever # Set 2026-08-18. The stack pins `:vX.Y.Z` and never a floating tag.
# was published most recently", and after the fact nothing in the stack file
# says which image that was. Passing --tag pins an immutable version instead,
# which is what you want the day somebody asks what is running — and the day you
# need to roll back to something specific.
# #
# Either way the digest is reported before and after, because that is the only # `:dev` and `:latest` are pointers. A stack that follows one has no answer to
# "what is running" that survives the next push — the tag in the stack file says
# `dev` today and will still say `dev` after the image underneath it changes, so
# the file records a preference rather than a fact. Rollback is worse: there is
# nothing to roll back *to*, because the pointer only ever names now.
#
# So this refuses a floating tag. `--allow-floating` exists for the one case
# that is not a mistake — reproducing a fault on whatever `:dev` happens to be —
# and it says so loudly every time.
#
# **Stack 58 pins `:dev` as this is written.** Correcting that is the first
# deploy made under this policy, and it is why the default below resolves a
# version rather than reusing whatever the stack already names.
#
# The digest is reported before and after either way, because that is the only
# thing that cannot be moved underneath you. # thing that cannot be moved underneath you.
set -uo pipefail set -uo pipefail
@ -64,9 +75,10 @@ say() { printf '\033[1mdeploy:\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1mdeploy:\033[0m %s\n' "$*" >&2; exit 1; } die() { printf '\033[1mdeploy:\033[0m %s\n' "$*" >&2; exit 1; }
stop() { printf '\033[1mdeploy:\033[0m %s\n' "$*" >&2; exit 2; } stop() { printf '\033[1mdeploy:\033[0m %s\n' "$*" >&2; exit 2; }
TAG="${DEPLOY_DEFAULT_TAG:-dev}" TAG=""
DRY_RUN="" DRY_RUN=""
NO_BACKUP="" NO_BACKUP=""
ALLOW_FLOATING=""
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case "$1" in case "$1" in
@ -74,7 +86,8 @@ while [ "$#" -gt 0 ]; do
--tag=*) TAG="${1#--tag=}" ;; --tag=*) TAG="${1#--tag=}" ;;
--dry-run) DRY_RUN="yes" ;; --dry-run) DRY_RUN="yes" ;;
--no-backup) NO_BACKUP="yes" ;; --no-backup) NO_BACKUP="yes" ;;
-h|--help) say "usage: bash scripts/deploy.sh [--tag <tag>] [--dry-run] [--no-backup]"; exit 0 ;; --allow-floating) ALLOW_FLOATING="yes" ;;
-h|--help) say "usage: bash scripts/deploy.sh [--tag vX.Y.Z] [--dry-run] [--no-backup] [--allow-floating]"; exit 0 ;;
*) stop "unknown argument '$1'. Run --help." ;; *) stop "unknown argument '$1'. Run --help." ;;
esac esac
shift shift
@ -102,19 +115,67 @@ api() { curl -sk --max-time 30 -H "X-API-Key: $KEY" "$@"; }
REG_ENV="${RELEASE_REGISTRY_ENV:-$HOME/.openclaw/docker-registry.env}" REG_ENV="${RELEASE_REGISTRY_ENV:-$HOME/.openclaw/docker-registry.env}"
if [ -r "$REG_ENV" ]; then set -a; . "$REG_ENV"; set +a; fi if [ -r "$REG_ENV" ]; then set -a; . "$REG_ENV"; set +a; fi
tags_json=""
if [ -n "${FORGEJO_REGISTRY:-}" ] && [ -n "${FORGEJO_REGISTRY_TOKEN:-}" ]; then if [ -n "${FORGEJO_REGISTRY:-}" ] && [ -n "${FORGEJO_REGISTRY_TOKEN:-}" ]; then
repo="${IMAGE#*/}" repo="${IMAGE#*/}"
tags=$(curl -sS --max-time 20 -u "${FORGEJO_REGISTRY_USER}:${FORGEJO_REGISTRY_TOKEN}" \ tags_json=$(curl -sS --max-time 20 -u "${FORGEJO_REGISTRY_USER}:${FORGEJO_REGISTRY_TOKEN}" \
"https://${FORGEJO_REGISTRY}/v2/${repo}/tags/list" 2>/dev/null) "https://${FORGEJO_REGISTRY}/v2/${repo}/tags/list" 2>/dev/null)
if [ -n "$tags" ]; then fi
printf '%s' "$tags" | grep -q "\"${TAG}\"" \
# ---------------------------------------------------------------------------
# Resolve the tag. With none named, this is the newest published NUMBERED
# version — never `:dev`, and never "whatever the stack already says", because
# both of those are how a deployment stops being able to name itself.
# ---------------------------------------------------------------------------
if [ -z "$TAG" ]; then
[ -n "$tags_json" ] || stop "no tag given and the registry tag list could not be read, so
the newest published version is unknown. Name one explicitly:
bash scripts/deploy.sh --tag vX.Y.Z
Nothing was attempted."
TAG=$(printf '%s' "$tags_json" | python3 -c "
import sys, json, re
tags = json.load(sys.stdin).get('tags') or []
def key(t):
m = re.fullmatch(r'v(\d+)\.(\d+)\.(\d+)', t)
return tuple(int(x) for x in m.groups()) if m else None
vers = sorted([t for t in tags if key(t)], key=key)
print(vers[-1] if vers else '')
")
[ -n "$TAG" ] || stop "the registry holds no numbered vX.Y.Z tag to deploy — only
floating ones. Publish a version first: npm run release. Nothing was attempted."
say "no tag given; newest published version is ${TAG}"
fi
# ---------------------------------------------------------------------------
# Refuse a floating tag. See THE POLICY at the top.
# ---------------------------------------------------------------------------
if ! printf '%s' "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
if [ -z "$ALLOW_FLOATING" ]; then
die "'${TAG}' is not a numbered version, and production always runs one.
A stack pinned to a moving tag cannot say what it is running and cannot be
rolled back to anything, because the pointer only ever names now.
Deploy a version: bash scripts/deploy.sh --tag vX.Y.Z
Publish one first: npm run release
If you genuinely mean to chase a pointer, --allow-floating."
fi
say "WARNING: --allow-floating. Deploying the moving tag '${TAG}'."
say " After this, the stack file will not record what is running."
fi
# ---------------------------------------------------------------------------
# Refuse to deploy a tag that is not published. Portainer would accept the
# stack file, fail to pull, and leave the container stopped — an outage caused
# by a typo, discovered by the site going down.
# ---------------------------------------------------------------------------
if [ -n "$tags_json" ]; then
printf '%s' "$tags_json" | grep -q "\"${TAG}\"" \
|| die "${IMAGE}:${TAG} is not published. Nothing was changed. || die "${IMAGE}:${TAG} is not published. Nothing was changed.
Published tags: $(printf '%s' "$tags" | python3 -c 'import sys,json;print(", ".join(json.load(sys.stdin).get("tags") or []))' 2>/dev/null)" Published tags: $(printf '%s' "$tags_json" | python3 -c 'import sys,json;print(", ".join(json.load(sys.stdin).get("tags") or []))' 2>/dev/null)"
else else
say "WARNING: could not read the registry tag list, so it is not known whether" say "WARNING: could not read the registry tag list, so it is not known whether"
say " ${TAG} is published. A missing tag becomes an outage here." say " ${TAG} is published. A missing tag becomes an outage here."
fi fi
fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# What is running now. Reported before anything changes, so the two halves of # What is running now. Reported before anything changes, so the two halves of

View File

@ -1,22 +0,0 @@
#!/usr/bin/env bash
# docker-push.sh — Tag and push dev image to Forgejo registry
# Usage: ./scripts/docker-push.sh
# Requires: ~/.openclaw/docker-registry.env (chmod 600)
set -euo pipefail
cd "$(dirname "$0")/.."
source ~/.openclaw/docker-registry.env
# Build image via docker compose
DOCKER_API_VERSION=1.44 docker compose build
# Tag and push dev
IMAGE_NAME="queue-north-website-queuenorth"
docker tag "${IMAGE_NAME}:latest" "${FORGEJO_REGISTRY}/null/queue-north-website:dev"
echo "$FORGEJO_REGISTRY_TOKEN" | docker login "$FORGEJO_REGISTRY" -u "$FORGEJO_REGISTRY_USER" --password-stdin
docker push "${FORGEJO_REGISTRY}/null/queue-north-website:dev"
docker logout "$FORGEJO_REGISTRY"
echo "✓ Pushed dev image"

View File

@ -53,16 +53,21 @@
# would leave `dev` carrying a commit announcing a release that was never # would leave `dev` carrying a commit announcing a release that was never
# published, and `.githooks/post-commit` would already have pushed it. # published, and `.githooks/post-commit` would already have pushed it.
# #
# ## Two tags, and only one of them is a record # ## One tag, and it is a record
# #
# `:vX.Y.Z` is immutable. Publishing over one is refused, because a running # `:vX.Y.Z`, immutable. Publishing over one is refused, because a running stack
# stack would silently pull different code on its next recreate while the tag # would silently pull different code on its next recreate while the tag said
# said nothing had changed. # nothing had changed.
# #
# `:dev` is a **moving pointer**, and Portainer stack 58 follows it. Moving it # **It does not move `:dev`, deliberately.** An earlier version of this script
# is what makes a release deployable here at all, so this script moves it on # did, because stack 58 followed that pointer. The policy changed on 2026-08-18:
# purpose — but that also means `:dev` is never evidence of what is running. # production always runs a numbered version, so nothing deploys `:dev` any more
# `scripts/status.sh` reads the digest, and now the version label, for that. # and moving it would publish a pointer that misrepresents what is running to
# anybody who reads it. `scripts/deploy.sh` refuses a floating tag outright.
#
# The `:dev` and `:latest` tags already in this registry are frozen where they
# are rather than deleted — the running container was created from `:dev`, and
# deleting the tag an existing deployment names is how a recreate fails to pull.
# #
# ## What it deliberately does not do # ## What it deliberately does not do
# #
@ -85,7 +90,6 @@ cd "$(git rev-parse --show-toplevel)" || exit 1
IMAGE="${RELEASE_IMAGE:-dream.scheller.ltd/null/queue-north-website}" IMAGE="${RELEASE_IMAGE:-dream.scheller.ltd/null/queue-north-website}"
SITE_URL="${RELEASE_SITE_URL:-https://queuenorth.com}" SITE_URL="${RELEASE_SITE_URL:-https://queuenorth.com}"
DEV_TAG="${RELEASE_DEV_TAG:-dev}"
REGISTRY_ENV="${RELEASE_REGISTRY_ENV:-$HOME/.openclaw/docker-registry.env}" REGISTRY_ENV="${RELEASE_REGISTRY_ENV:-$HOME/.openclaw/docker-registry.env}"
# Files carrying a version string. The Dockerfile is here because its # Files carrying a version string. The Dockerfile is here because its
@ -178,7 +182,7 @@ next=$(cd "$work" && npm version --no-git-tag-version "$BUMP" 2>/dev/null | tr -
[ -n "$next" ] || die "npm rejected '$BUMP' as a version bump." [ -n "$next" ] || die "npm rejected '$BUMP' as a version bump."
TAG="v${next}" TAG="v${next}"
say "$current -> $next (publishing ${IMAGE}:${TAG}, and moving :${DEV_TAG})" say "$current -> $next (publishing ${IMAGE}:${TAG})"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Refuse to move a tag that is already published. # Refuse to move a tag that is already published.
@ -217,13 +221,12 @@ if [ -n "$DRY_RUN" ]; then
say " bash scripts/verify.sh" say " bash scripts/verify.sh"
say " docker build --build-arg APP_VERSION=${next} -t ${IMAGE}:${TAG} ." say " docker build --build-arg APP_VERSION=${next} -t ${IMAGE}:${TAG} ."
say " verify the image's org.opencontainers.image.version label reads ${next}" say " verify the image's org.opencontainers.image.version label reads ${next}"
say " docker tag ${IMAGE}:${TAG} ${IMAGE}:${DEV_TAG}" say " docker push ${IMAGE}:${TAG}"
say " docker push ${IMAGE}:${TAG} and ${IMAGE}:${DEV_TAG}"
say " git commit -m 'chore(release): ${TAG}' (post-commit then pushes)" say " git commit -m 'chore(release): ${TAG}' (post-commit then pushes)"
say " git tag ${TAG} && git push origin ${TAG}" say " git tag ${TAG} && git push origin ${TAG}"
say "" say ""
say "It would NOT deploy. Portainer stack 58 keeps running the image it has" say "It would NOT deploy, and it would NOT move :dev — production runs a"
say "until scripts/deploy.py is run." say "numbered version. Afterwards: npm run deploy -- --tag ${TAG}"
exit 0 exit 0
fi fi
@ -304,11 +307,6 @@ if [ "$baked" != "$next" ]; then
exit 1 exit 1
fi fi
# The pointer stack 58 follows. Tagged after the version tag is verified, so
# :dev can never point at an image that failed its own check.
docker tag "${IMAGE}:${TAG}" "${IMAGE}:${DEV_TAG}" \
|| die "could not tag ${IMAGE}:${DEV_TAG}; nothing was pushed."
say "pushing ${IMAGE}:${TAG}" say "pushing ${IMAGE}:${TAG}"
if ! docker push "${IMAGE}:${TAG}"; then if ! docker push "${IMAGE}:${TAG}"; then
say "push failed. The image exists locally and the bump is in your working" say "push failed. The image exists locally and the bump is in your working"
@ -317,14 +315,6 @@ if ! docker push "${IMAGE}:${TAG}"; then
exit 1 exit 1
fi fi
say "moving ${IMAGE}:${DEV_TAG}"
if ! docker push "${IMAGE}:${DEV_TAG}"; then
say "WARNING: ${TAG} IS published, but :${DEV_TAG} was not moved. Stack 58"
say " follows :${DEV_TAG}, so a deploy right now would redeploy the"
say " PREVIOUS image. Push it by hand before deploying:"
say " docker push ${IMAGE}:${DEV_TAG}"
fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Commit last, by explicit path. # Commit last, by explicit path.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -350,9 +340,10 @@ fi
say "released ${TAG}." say "released ${TAG}."
say "" say ""
say "This published an image. It did NOT deploy it — Portainer stack 58 is still" say "This published an image. It did NOT deploy it — Portainer stack 58 is still"
say "running whatever it was running before. To move it:" say "running whatever it was running before. To move it to this version:"
say "" say ""
say " python3 scripts/deploy.py" say " npm run deploy -- --tag ${TAG} --dry-run"
say " npm run deploy -- --tag ${TAG}"
say "" say ""
say "and see docs/OPERATIONS.md, which covers what a redeploy costs: it recreates" say "and see docs/OPERATIONS.md, which covers what a redeploy costs: it recreates"
say "the container and takes both public front doors down together." say "the container and takes both public front doors down together."