# Re-pin a Worldtree instance's WORLDTREE_IMAGE to the SHA it is actually # running, closing the stale-`:latest` recreate hazard. # # THE HAZARD (found 2026-08-22, Worldtree #410): both corviduo-dev instances # had `WORLDTREE_IMAGE=…/worldtree:latest` in their .env while running # SHA-tagged images from that day. The local `:latest` tag pointed at # b19afd71d7cc, built 2026-06-14 — 69 days stale. So ANY `docker compose up` # on either instance, by anyone, for any reason, silently DOWNGRADED that # service by 69 days. This is the same footgun that caused the 2026-06-15 # outage; the pin is what disarms it. # # This is a stopgap. The durable fix is worldtree-dev's deploy workflow # stamping the deployed SHA into .env at each deploy (queued repo-side). # Until that lands, re-run this after any deploy that moves the image. # # Usage — one run per instance: # scripts/elway corviduo-dev --playbook playbooks/repin-worldtree-image.yaml \ # --var instance_dir=/opt/worldtree \ # --var api_container=worldtree-worldtree-api-1 \ # --var expect_sha=ae88a057c0ed # # The edit is INERT until the next recreate — it changes what the NEXT # `compose up` resolves to, not the running container. That is the intent: # make the next recreate safe rather than dangerous. # # ⚠ THE `worldtree-pinned` INSTANCE NEEDS AN EXTRA STEP FIRST. It runs image # sha256:446e5807… which has NO repo tags at all — it is dangling, kept alive # only by the running container referencing it. So there is no tag to pin to, # and this playbook's guard will (correctly) refuse. Tag it before re-pinning: # # docker tag sha256:446e5807bf43639be7d285864a7716816880e0f5c0892427e72800f4aa8ffc56 \ # gitea.phasefinal.com/vh/worldtree:446e5807bf43 # # That is also worth doing on its own merits: an untagged image referenced # only by a container is one `docker rm` away from being garbage-collected, # and this one is the frozen reference the whole instance exists to provide. vars: instance_dir: /opt/worldtree api_container: worldtree-worldtree-api-1 expect_sha: "" registry: gitea.phasefinal.com/vh/worldtree steps: - name: Refuse to run without an explicit target SHA shell: test -n "{{ expect_sha }}" changed_when: "false" - name: Confirm the tag we are about to pin resolves to the running image # Guards against a deploy landing between reading the SHA and writing it — # pinning a SHA that is NOT running would arm the exact hazard we are # disarming, just with a different image. # # Compares IMAGE IDs, not the container's .Config.Image string. The string # is only the tag the container was CREATED from, which can differ from # what it actually runs: worldtree-pinned was created from `:latest` back # when that tag pointed at 446e5807, and `:latest` has since moved. A # string compare rejects that instance even though pinning it is correct; # an ID compare asserts the thing we actually care about — that this tag # names the bytes currently running. shell: | running=$(docker inspect {{ api_container }} --format '{{.Image}}') tagged=$(docker image inspect {{ registry }}:{{ expect_sha }} --format '{{.Id}}' 2>/dev/null) || { echo "REFUSING: no local image tagged {{ registry }}:{{ expect_sha }} — tag it first" exit 1; } test "$running" = "$tagged" || { echo "REFUSING: {{ api_container }} runs $running but {{ registry }}:{{ expect_sha }} is $tagged" exit 1; } echo "confirmed: {{ registry }}:{{ expect_sha }} == running image $running" changed_when: "false" - name: Back up .env sudo: true shell: cp -n {{ instance_dir }}/.env {{ instance_dir }}/.env.bak-pre-repin-{{ expect_sha }} creates: "{{ instance_dir }}/.env.bak-pre-repin-{{ expect_sha }}" - name: Re-pin WORLDTREE_IMAGE to the running SHA sudo: true # Skipped when already correct, so a re-run reports OK rather than a # phantom CHANGED — a playbook that always claims to have changed # something trains you to stop reading the summary. when: "! sudo grep -q '^WORLDTREE_IMAGE={{ registry }}:{{ expect_sha }}$' {{ instance_dir }}/.env" # `|` delimiter because the image reference contains slashes. shell: | grep -q '^WORLDTREE_IMAGE=' {{ instance_dir }}/.env || { echo "REFUSING: no WORLDTREE_IMAGE line to replace"; exit 1; } sed -i 's|^WORLDTREE_IMAGE=.*|WORLDTREE_IMAGE={{ registry }}:{{ expect_sha }}|' {{ instance_dir }}/.env chown deploy:deploy {{ instance_dir }}/.env chmod 600 {{ instance_dir }}/.env changed_when: "true" verify: - name: .env now names the SHA, not a floating tag sudo: true shell: grep -q '^WORLDTREE_IMAGE={{ registry }}:{{ expect_sha }}$' {{ instance_dir }}/.env changed_when: "false" - name: compose resolves every service to the pinned SHA (no ':latest' anywhere) # The assertion that matters. Reading the .env proves the line changed; # only rendering the compose file proves what a recreate would actually # pull. sudo: true shell: | cd {{ instance_dir }} if docker compose config 2>/dev/null | grep -E '^\s+image:' | grep -q ':latest'; then echo "STILL RESOLVING TO :latest" docker compose config 2>/dev/null | grep -E '^\s+image:' exit 1 fi docker compose config 2>/dev/null | grep -E '^\s+image:' | sort -u changed_when: "false" - name: Running containers untouched (this edit must not restart anything) shell: docker inspect {{ api_container }} --format '{{.State.Status}} since {{.State.StartedAt}}' changed_when: "false"