ci(deploy): deploy releases to production from GHCR - #211
Merged
Merged
Conversation
Only the runtime image was pushed. The migrator was built on every pull request and then thrown away, so a deployment had no published image to apply the migration history with — it had to build one from a source checkout on the host. It now ships as its own GHCR package under the same tags as the bot, so a deployment pulls the pair for one version and the migration history can never drift from the code that expects it. The release cache gets its own scope, and reads the CI migrator scope as a second source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`compose.prod.yaml` carried `build:` sections, so a production host had to hold a source checkout and rebuild both images itself — running something subtly different from what CI tested, and rebuilding it on every machine. It now names published images and builds nothing. A host needs Docker, this file and an env file, and deploying or updating is the same two commands: `pull`, then `up -d`. Both images track `latest` rather than a version variable. A pinned version would have to be edited on the host for every release, and an automated `pull && up -d` would otherwise redeploy the very same image and report success while changing nothing. `latest` only ever moves to a non-prerelease tag, so cutting an `-rc` publishes it without deploying it. Building locally is still `docker build --target runtime .`, and CI builds both targets on every pull request, so the compose file no longer has to serve both purposes. Note that this file only becomes usable once the migrator package exists and a non-prerelease tag has moved `latest` — as of writing, only `omnibot:0.1.0-rc1` is published. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The chain stopped at the registry: publishing an image changed nothing on the host until someone connected to it and pulled by hand. A `deploy` job now calls the Portainer stack webhook, which re-pulls `latest` and recreates what changed. The webhook is the whole reason this is a single `curl` rather than an SSH session: the secret it needs grants no shell on the host and can do nothing but redeploy this one stack. It skips prerelease tags. `latest` does not move for those, so redeploying would restart the stack onto the version already running and report success having changed nothing. `curl -f` matters more than it looks: without it a revoked or renamed webhook answers 404, curl exits 0, and a dead deployment reads as a green release for as long as nobody checks the bot. Requires the `PORTAINER_WEBHOOK_URL` secret, and a stack configured with Re-pull image — the README says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Portainer refuses to clone a repository that contains one:
Failed to download git repository: unable to clone git repository:
repository contains a symlink, which is not allowed for security
reasons
`CLAUDE.md` pointed at `AGENTS.md`, and it was the only symlink tracked
here, so a git-backed stack could not deploy at all.
It becomes a regular file that imports `AGENTS.md` rather than copying it,
so there is still one source to keep up to date.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🇬🇧 English
🎯 Summary
A release now reaches production on its own: both images are published, and the Portainer stack redeploys itself.
🤔 Motivation
The chain stopped at the registry. Publishing an image changed nothing on the host until someone connected to it and pulled by hand — and the migrator was never published at all, so there was no image to apply the migration history with.
📦 What's included
omnibot-migrator, under the same tags as the bot — a deployment pulls the pair for one version, so the migration history cannot drift from the code that expects it.compose.prod.yamlnames published images and builds nothing. A host needs Docker, that file and the environment variables; what runs is exactly what CI tested, not a rebuild from a checkout that may have drifted.deployjob calls the Portainer stack webhook once the images are published. That is the whole reason it is a singlecurland not an SSH session: the secret grants no shell on the host and can do nothing but redeploy this one stack.CLAUDE.mdstops being a symlink toAGENTS.md— Portainer refuses to clone a repository containing one, which blocked the git-backed stack outright. It now importsAGENTS.mdinstead of copying it, so there is still a single source. Verified by running a session with file tools disabled: the import resolves.🧭 Notes
PORTAINER_WEBHOOK_URLsecret, and a stack with Re-pull image enabled — without it a redeploy reuses the image already cached underlatest.latesttag, since onlyomnibot:0.1.0-rc1is published. actionlint anddocker compose configcover syntax, nothing more. The first non-prerelease tag is the real test.latestdoes not move for them and redeploying would restart the stack onto the version already running — succeeding while changing nothing.🇫🇷 Français
🎯 Résumé
Une release atteint désormais la production toute seule : les deux images sont publiées, et la stack Portainer se redéploie.
🤔 Motivation
La chaîne s'arrêtait au registre. Publier une image ne changeait rien sur l'hôte tant que personne ne s'y connectait pour tirer à la main — et le migrator n'était pas publié du tout, donc aucune image ne permettait d'appliquer l'historique des migrations.
📦 Contenu
omnibot-migrator, sous les mêmes tags que le bot — un déploiement tire la paire d'une version, donc l'historique des migrations ne peut pas diverger du code qui l'attend.compose.prod.yamlnomme des images publiées et ne construit plus rien. Un hôte n'a besoin que de Docker, de ce fichier et des variables ; ce qui tourne est exactement ce que la CI a testé, pas une reconstruction depuis un checkout qui a pu dériver.deployappelle le webhook de la stack Portainer une fois les images publiées. C'est toute la raison pour laquelle c'est un simplecurlet non une session SSH : le secret ne donne aucun shell sur l'hôte et ne sait rien faire d'autre que redéployer cette stack.CLAUDE.mdcesse d'être un symlink versAGENTS.md— Portainer refuse de cloner un dépôt qui en contient un, ce qui bloquait purement et simplement la stack adossée à git. Il importe désormaisAGENTS.mdau lieu de le copier, donc il n'y a toujours qu'une source. Vérifié en lançant une session sans outils de lecture : l'import se résout bien.🧭 À noter
PORTAINER_WEBHOOK_URLet une stack avec Re-pull image activé — sans lui, un redéploiement réutilise l'image déjà en cache souslatest.latestpuisque seulomnibot:0.1.0-rc1est publié. actionlint etdocker compose configcouvrent la syntaxe, rien de plus. Le premier tag non-préversion sera le vrai test.latestne bouge pas pour elles, et redéployer relancerait la stack sur la version déjà en place — un succès qui ne change rien.📋 Checklist
PORTAINER_WEBHOOK_URLsecret and the Portainer stack (Repository mode, Re-pull image, the four env vars) / 🇫🇷 Créer le secretPORTAINER_WEBHOOK_URLet la stack Portainer (mode Repository, Re-pull image, les quatre variables)latest, that the migration runs, and that the bot comes up / 🇫🇷 Poser un tag non-préversion et vérifier que les deux packages reçoiventlatest, que la migration s'applique et que le bot démarre