Release Ririko
This guide is for the maintainer. It explains how to publish a release, how to deploy it to staging and production, and how to recover when a deploy fails.
Agents never push, retag or log in to Docker Hub. They never create release tags, never move latest and never approve a deploy. They prepare the code and the documents. The maintainer does every step below.
For launch day, follow the checklist in section 10. For the npm package (npx ririko), follow section 11.
Branches
developis the base of every pull request. Work lands here first.masterholds released code. You mergedevelopintomasterfor a release and tag the release onmaster. The published documentation (ririko.ai) is built frommaster.1.xholds the 1.4.0 line. It takes no new work.
CircleCI has no branch filters for releases: the release workflow runs for v* tags, so the branch names do not change what it runs.
After the rename. The branch develop/2.0.0 was renamed to develop on GitHub. Git cannot keep develop and develop/2.0.0 in one clone, so every existing clone needs these steps once:
-
Drop the old remote-tracking branch:
git fetch --prune -
Rename your local branch (or delete it with
git branch -d develop/2.0.0):git branch -m develop/2.0.0 develop -
Point it at the new remote branch:
git branch -u origin/develop develop
Open pull requests move to develop by themselves when the maintainer renames the branch on GitHub.
How Releases Work
- The CircleCI
releaseworkflow (.circleci/config.yml) runs only for git tags such asv2.1.3orv2.1.3-beta.1. A branch push never starts it. - The workflow builds
bot-runnerandweb-runner. It runsscripts/docker-smoke.tson both images. Then it pushes them to Docker Hub:ririkoai/ririkobotandririkoai/ririkobot-dashboard;- with the tags
2.1.3,2.1and2for a release, or only2.1.3-beta.1for a prerelease (scripts/release-tags.ts).
- The workflow never pushes
latest. You movelatestby hand (section 6). - The Docker Hub login comes from the CircleCI context
dockerhub. - After the push, the workflow deploys.
deploy-stagingruns for every release tag. For a final tagvX.Y.Z, the workflow then waits for your approval and runsdeploy-production(section 8).
1. One-Time Setup
- In Docker Hub, open Account settings, then Personal access tokens. Create a token with Read & Write scope.
- In CircleCI, open Organization settings, then Contexts. Create a context named
dockerhub. - Add two variables to the context:
DOCKERHUB_USERNAME: the Docker Hub account that ownsririkoai.DOCKERHUB_TOKEN: the token from step 1.
- Optional: limit the context to a security group, so only maintainers can use it.
- In Docker Hub, check that the repository
ririkoai/ririkobot-dashboardexists. If it does not, create it as a public repository.
2. Keep 1.4.0 Available as 1.4.0
latest points to Ririko 1.4.0 until you move it. Give that exact image a permanent tag first.
-
Log in to Docker Hub:
docker login -
Show the image:
docker buildx imagetools inspect ririkoai/ririkobot:latestCopy the top-level
Digest:line. For 1.4.0 it issha256:c11e8defda3beb390a8ef673b92f82bb958952abfe214d49436388ab6bc9c012. If the digest is different, find out why before you go on. -
Add the tag. Docker Hub copies the manifest for every platform. Nothing is pulled or built:
docker buildx imagetools create --tag ririkoai/ririkobot:1.4.0 ririkoai/ririkobot@sha256:<digest from step 2> -
Check the new tag:
docker buildx imagetools inspect ririkoai/ririkobot:1.4.0The digest is the same as in step 2.
The package version inside that image is 1.4.1. If you want that tag too, repeat step 3 with --tag ririkoai/ririkobot:1.4.1.
3. Update the Docker Hub Descriptions
- Open the file
docs/dockerhub/ririkobot.md. Copy all of it into the Overview ofririkoai/ririkoboton Docker Hub. - Open the file
docs/dockerhub/ririkobot-dashboard.md. Copy all of it into the Overview ofririkoai/ririkobot-dashboard. - Check that the links in both files point to the branch
master.
4. Publish a Release
-
Merge
developintomasterwith a pull request, somasterholds the release. -
Check that
package.jsonhas the version you are about to tag. -
Optional: publish a prerelease first. A prerelease checks the pipeline and does not move the tag
2:git tag -a v2.0.0-beta.1 -m "Ririko 2.0.0 beta 1"git push origin v2.0.0-beta.1 -
Go to
masterand update it:git checkout mastergit pull -
Create the release tag:
git tag -a v2.0.0 -m "Ririko 2.0.0" -
Push the tag. This starts the
releaseworkflow:git push origin v2.0.0 -
Watch the
releaseworkflow in CircleCI. Staging deploys by itself. Check staging, then approve production (section 8). -
When the workflow is green, check the Docker Hub tags. Each command shows a manifest:
docker buildx imagetools inspect ririkoai/ririkobot:2.0.0docker buildx imagetools inspect ririkoai/ririkobot-dashboard:2docker buildx imagetools inspect ririkoai/ririkobot:latestThe last command still shows the 1.4.0 digest.
-
Publish a GitHub release for the tag. Add a link to
docs/upgrading-from-1.4.mdin the notes.
If the workflow fails, fix the cause and run it again in CircleCI. A new run pushes the same tags again from the same commit. Never use a version number for a different commit. Tag a new patch version instead.
5. Announce When latest Will Move
Choose a date at least 30 days after the 2.0.0 release. On that date latest moves to 2.x.
- Write the date in the GitHub release notes and in the README.
- Write the date in the Docker Hub descriptions. The tag table there already says that
latestmoves after the sunset. - Post the date on the support Discord server.
The announcement says:
latestmoves to 2.x on that date;- 1.4.0 stays available as
ririkoai/ririkobot:1.4.0; - users should pin a version tag (
2or1.4.0) and followdocs/upgrading-from-1.4.md; - a 2.x bot that starts with an unchanged 1.4.0 compose file stops, prints the upgrade steps and leaves the 1.4.0 data untouched.
6. Move latest
Do this on the announced date. It points latest to the current 2.x release, by digest, for both repositories.
-
Show the bot image and copy its digest:
docker buildx imagetools inspect ririkoai/ririkobot:2 -
Move
latestof the bot image:docker buildx imagetools create --tag ririkoai/ririkobot:latest ririkoai/ririkobot@sha256:<digest from step 1> -
Show the dashboard image and copy its digest:
docker buildx imagetools inspect ririkoai/ririkobot-dashboard:2 -
Move
latestof the dashboard image:docker buildx imagetools create --tag ririkoai/ririkobot-dashboard:latest ririkoai/ririkobot-dashboard@sha256:<digest from step 3> -
In
docs/dockerhub/and on Docker Hub, change the tag tables:latestis now 2.x.
Later releases do not move latest. Repeat this section after a release if latest must follow it.
7. Move latest Back to 1.4.0
Do this only if 2.x must be pulled back after latest moved.
-
Point
latestof the bot image to 1.4.0:docker buildx imagetools create --tag ririkoai/ririkobot:latest ririkoai/ririkobot:1.4.0 -
Check the result. The digest is the 1.4.0 digest again:
docker buildx imagetools inspect ririkoai/ririkobot:latest
The dashboard had no 1.4.0 image. Point its latest to the last good 2.x version, or leave it. The 1.4.0 bot does not use the dashboard.
This does not touch anyone's data. A 1.4.0 user never let 2.x write to the ./data folder. A user who upgraded keeps the 2.x data in its own volume.
8. Deploy to Staging and Production
After the images are pushed, the same release workflow deploys them. CircleCI holds no app secrets and has no Docker access on the hosts. For each host it holds one SSH key and one Cloudflare Access service token. It can only ask a host for deploy <version> or status. The key's forced command, deploy/host/bin/ririko-deploy-ssh.sh, enforces this.
There are three kinds of hosts: the staging app host, the production app host, and the Lavalink host that serves them. The maintainer's private runbook (not in the repository) explains how to set up the hosts, the tunnels and the contexts, and how to recover a host.
8.1. Pipeline Order
release -> deploy-lavalink-staging -> deploy-staging -> approve-production -> deploy-lavalink-production -> deploy-production
| Git tag | Jobs that run |
|---|---|
v2.1.3-beta.1 | release, deploy-lavalink-staging, deploy-staging. The pipeline stops there. It does not ask for approval and does not deploy to production. |
v2.1.3 | release, deploy-lavalink-staging, deploy-staging. Then approve-production waits for a maintainer. After approval, deploy-lavalink-production and deploy-production run one after the other. |
deploy-lavalink-stagingstarts whenreleaseis green.deploy-stagingfollows it. If one of them fails, production is never offered.- Each environment updates its Lavalink instance first. A release ships
docker/lavalink/application.yml, which pins the Lavalink plugins. The instance must run the new file before the bot that needs it starts. A Lavalink restart stops every player on that instance. So the host restarts it only whendeploy/lavalink/docker-compose.ymlordocker/lavalink/application.ymlchanged since the last deploy (section 8.5). Most releases leave it alone. approve-productionis a CircleCI approval job (type: approval). Open the workflow in CircleCI. After you check staging, click Approve.- Every job has its own tag filter and ignores all branches. A branch push never deploys. The separate
ciworkflow never deploys. - The
releasejob pushes the tags2.1and2before you approve production. The hosts always deploy the exact version of the tag, for example2.1.3. They never deploy a moving tag. - You can run a failed deploy job again. The host starts from its recorded state. You can also deploy the version that already runs.
8.2. CircleCI Contexts
Create four organization contexts:
deploy-staginganddeploy-productionfor the app hosts;deploy-lavalink-staginganddeploy-lavalink-productionfor the Lavalink host (section 8.5).
The deploy job in .circleci/config.yml is the same for all four. Its host parameter is app or lavalink. Each context holds the values of its own host, with the same five variable names:
| Variable | Value |
|---|---|
DEPLOY_SSH_HOST | The SSH tunnel hostname of the host, for example ssh-staging.example.com. Not an IP address. |
DEPLOY_SSH_KEY_B64 | The private key of the deploy user in base64 on one line. CircleCI values must be one line. |
DEPLOY_KNOWN_HOSTS | The host key on one line, under the name in DEPLOY_SSH_HOST. |
TUNNEL_SERVICE_TOKEN_ID | The Client ID of the Cloudflare Access service token. |
TUNNEL_SERVICE_TOKEN_SECRET | The Client Secret of that token. |
Use a different key and token in each context. A leaked staging value then cannot reach production.
To make the values for one context:
-
Make a key pair for the deploy user:
ssh-keygen -t ed25519 -N '' -f deploy_key -
Print the private key as base64 on one line, and store the output as
DEPLOY_SSH_KEY_B64:base64 -w0 < deploy_key -
Give the public key
deploy_key.pubto the host withbootstrap.sh --deploy-key. The host makes it a forced-command key. -
Delete the private file
deploy_key. -
On the host, print the host key line. Use the hostname of that host in place of
ssh-staging.example.com. Store the output asDEPLOY_KNOWN_HOSTS:echo "ssh-staging.example.com $(cut -d' ' -f1,2 /etc/ssh/ssh_host_ed25519_key.pub)"The job checks this key with
StrictHostKeyChecking=yes. It never trusts a host key on first use. -
In Cloudflare Zero Trust, create a service token. Give the Access application of the SSH hostname a Service Auth policy that includes the token.
cloudflared access sshreads the token from the environment variables, so the secret never appears in a command line.
Then limit who can deploy to production:
- In CircleCI, open Organization settings, Contexts,
deploy-production, Security. - Add a security group that holds only the maintainers.
- Do the same for
deploy-lavalink-production.
A job that uses a restricted context runs only for a member of the group. A collaborator who is not a maintainer cannot make production deploy, even if they click Approve. The approval job has no context of its own, so the restriction on deploy-production is what protects it. Restricting the staging contexts is optional.
Agents never create these contexts, push tags or approve a deploy.
8.3. What the Job Does
-
The job runs on
cimg/base:currentwithout a checkout. It installs a pinnedcloudflaredfrom its GitHub release, checks the.debfile against a sha256 in the repository, and installs it withdpkg. -
It checks that all five variables exist. It writes the key (mode 0600) and
known_hosts. -
It runs
sshwithStrictHostKeyChecking=yes,IdentitiesOnly=yesandProxyCommand="cloudflared access ssh --hostname %h". It sendsdeploy ${CIRCLE_TAG#v}todeploy@$DEPLOY_SSH_HOST. The host prints its progress, and the job shows it. -
On an app host,
ririko-deploydoes these steps in order:- It downloads the compose files of the release.
- It dumps the database. It skips the dump when no release runs yet.
- It pulls the images.
- It runs
ririko db:migratefrom the new bot image while the old release still runs (section 8.6). - It starts the stack.
- It waits until the bot and the dashboard are ready.
On a first deploy, it starts the
postgresservice before the migration. -
The job fails with the exit status of the host (section 8.4) and prints what it means.
On a host, ririko-deploy runs every compose call with DB_AUTO_MIGRATE=false in its environment. This wins over the env file. So ririko db:migrate is the only step that changes the schema. A self-hosted stack uses the default true and migrates when the bot starts.
The command ririko-deploy status prints the container states and the latest and pending migration ids.
To update cloudflared:
-
Change
CLOUDFLARED_VERSIONandCLOUDFLARED_SHA256together in thessh-hostcommand of.circleci/config.yml. -
Download the file yourself:
gh release download <version> --repo cloudflare/cloudflared --pattern cloudflared-linux-amd64.deb -
Print its checksum:
sha256sum cloudflared-linux-amd64.deb -
Compare the result with the digest that GitHub shows for the file. Use the value only if they are equal.
8.4. Host Exit Codes
ririko-deploy (deploy/host/bin/ririko-deploy.sh) keeps these exit codes. The job passes them through.
| Exit | Meaning |
|---|---|
| 0 | Deployed. The bot and the dashboard are ready, and the host recorded the version. |
| 1 | Failed before the running release was touched (bad tag or file, the dump failed, invalid ririko.conf). Nothing changed. Read the output and fix the cause. |
| 2 | The host rejected the command or the version. The tag is not X.Y.Z or X.Y.Z-prerelease, or the job sent something other than deploy <version> or status. A Lavalink host also rejects an instance it does not run (section 8.5). |
| 3 | The lock stayed busy for DEPLOY_LOCK_WAIT seconds. Another deploy, backup or watchdog run holds it. Nothing changed. Run the job again when that run is done. |
| 4 | The new release did not become ready, so the host started the previous release again. The database was not restored. It keeps the migrations of the new release. They only add, so the previous release runs on them. The output names the dump (section 8.7). On a Lavalink host: /version did not answer, and the previous release of that instance runs again. |
| 5 | The new release did not become ready, and there is no previous release (first deploy, or the same version again). Nothing runs. Fix the problem on the host and deploy again. |
| 6 | The database migration failed (ririko db:migrate exit 1) or was refused (exit 2: the database holds a migration this release does not know). The host started and stopped nothing. The previous release keeps running. The output names the dump (section 8.6). |
| 255 | ssh itself failed: the tunnel, the service token, the key or the pinned host key. |
What to do next:
- After exit 4, the host runs the old version. Fix the cause and tag a new patch version. Restore the database only when the failed release damaged data (section 8.7).
- After exit 6, the host runs the old version and nothing changed. The cause is in the output of the migration. Run
ririko db:migrate --statuson the host to see the same state. If the guard refused, the tag is older than what the database holds (section 8.6). - After exit 5, log in to the host before you deploy again. Run
ririko-deploy statusanddocker compose -p ririko ps, and read/opt/ririko/deploy.log.
To see what a host runs without a deploy, run this command through the same cloudflared ProxyCommand:
ssh deploy@<DEPLOY_SSH_HOST> status
8.5. The Lavalink Host
The Lavalink host runs one container for each environment: lavalink-production on port 2333 and lavalink-staging on port 2334. The compose project is ririko-lavalink. CI reaches the host through its own tunnel hostname, for example lavalink-staging-ssh.example.com.
bootstrap.sh --role lavalink sets up the host and writes RIRIKO_ROLE=lavalink into /etc/ririko/ririko.conf. The file deploy/host/ririko.conf.example describes the keys that follow.
- One key for each instance. Run
bootstrap.sh --role lavalink --deploy-key-staging "<key>"(or--deploy-key-production). It writesrestrict,command="/usr/local/bin/ririko-deploy-ssh staging"(orproduction) for that key. The instance comes from this argument in theauthorized_keysfile that root owns. It never comes from the command that CI sends. So the staging key can update only the staging instance. Put each key in the matching context. The staging key andDEPLOY_SSH_HOST=lavalink-staging-ssh.<domain>go indeploy-lavalink-staging. The production values go indeploy-lavalink-production. - Which instances a host runs.
LAVALINK_INSTANCES(production,stagingor both) names them. If it is not set, the Lavalink ports in theWG_PEERSentries decide: 2333 is production, 2334 is staging. A deploy for an instance the host does not run ends with exit 2. The watchdog checks only the instances the host runs. - Env files.
/opt/ririko/lavalink-<instance>.env(owner root, mode 0600) holdsLAVALINK_PASSWORDand theSPOTIFY_*values. Start fromdeploy/lavalink/lavalink.env.example. Use one file for each instance, with different passwords. A deploy with a missing file or an empty password ends with exit 1. - What a deploy does.
ririko-deploy deploy <version> <instance>downloadsdeploy/lavalink/docker-compose.ymlanddocker/lavalink/application.ymlof the tag intoreleases/<version>/. It compares their sha256 withstate/lavalink-<instance>.sha.- If both are equal, it prints
unchanged, records the version and ends with exit 0. The container is not touched, so no player stops. - Otherwise it pulls the image and recreates only
lavalink-<instance>. This also runs the one-shot plugin-volume service of that instance. It never touches the other instance. Then it waits up toLAVALINK_READY_TIMEOUT(default 180) seconds untilGET http://<WG_ADDRESS>:<port>/versionanswers200with the password of the instance. If it never answers, the previous release of that instance starts again (exit 4), or the deploy ends with exit 5 when there is none. - The password goes to
curlon standard input, never in a command line. - A watchdog or backup run delays a deploy. It does not fail it.
- If both are equal, it prints
- A change in the env file is not a deploy. The env file is not part of the release. If you change it, the deploy prints
unchanged. Recreate the container yourself (deployment.md section 8.2). - Watchdog. With
RIRIKO_ROLE=lavalink,ririko-watchdogpingsHEALTHCHECK_PING_URLonly when every configured instance runs and its/versionanswers. Each failed check posts/failwith the reason. After 3 failed checks in a row, it restarts that container and starts counting again. The counter is the file/opt/ririko/state/watchdog-lavalink-<instance>. - Memory.
LAVALINK_PRODUCTION_HEAPandLAVALINK_STAGING_HEAPinririko.confset the Java memory limit, for example768m.
8.6. Migrations on a Host
The host applies the migrations of a release as a separate step. It runs between the image pull and up -d (section 8.3). The bot on a host only checks the schema. ririko db:migrate, run once from the new bot image, is the only step that changes it.
The host scripts (/usr/local/bin/ririko-deploy and the other files in deploy/host/bin) change only when bootstrap.sh --ref <ref> runs on the host. A deploy never updates them. If a release changes them, run bootstrap.sh on every host before you deploy that release (section 8.8, step 1).
- The old release keeps running during the migration. Every migration leaves the schema usable by the previous release. It adds tables, nullable or defaulted columns, indexes or data. A change that removes something ships in a later release, as a migration whose first line is
-- ririko:contract. The host starts the new containers after the migration is done. - Each migration is one transaction. PostgreSQL rolls back a failed migration by itself. The database stays at the last migration that worked. Migrations of the same run that already finished stay applied. They only add.
- Exit 6 with
ririko db:migrateexit 1: the migration failed. The old release still runs on the database. The job output and/opt/ririko/deploy.loghold the output of the migration. Each line starts withdb:migrate:. Nothing needs a restore. Fix the cause, which is usually the migration, and tag a new patch version. If the cause is a one-time problem in the data of that host, fix the data there and run the failed job again. - Exit 6 with
ririko db:migrateexit 2: the downgrade guard refused. The database holds a contract migration that the tag does not contain. So the tag is older than the database. Nothing changed. Deploy a newer version that contains the migration. Do not restore a dump to get past the guard. - First deploy. When nothing runs yet, the host starts the
postgresservice before the migration and takes no dump. The exit message then saysPre-deploy dump: none taken. ririko-deploy statusprints the container states and these lines ofririko db:migrate --status:Latest,Pending,Unknown(recorded by a newer release) andAdopted. After a good deploy,Pendingis(none).
What a rollback (exit 4) does and does not undo:
- It undoes the code. The host starts the previous release again from
releases/<previous>. It tries to pull the images and uses the ones on the host if the pull fails. It runs no migration, and leavesstate/currenton that version. - It does not undo the database. There is no down migration. The database keeps the migrations of the failed release, and the previous release runs on them. It also keeps every row that the failed release wrote.
- A contract migration breaks the rollback. The downgrade guard also stops a bot that only checks (
DB_AUTO_MIGRATE=false). So the previous release cannot start on a database with a contract migration it does not know. The log then saysROLLBACK FAILEDand nothing runs. Deploy the newer release. Restoring the dump (section 8.7) is the last choice. The first release with a contract migration has no rollback. Check its dump before you approve production. - The dump is the only way back in time. It is a snapshot from just before the deploy. A restore also removes everything written after it.
8.7. Restore the Pre-Deploy Dump
Restore only when the failed release damaged data, or when a contract migration is on the database and the previous release must run. After a failed deploy that left the old release running (exit 1, 3, 4 or 6), there is nothing to restore.
Before it pulls anything, ririko-deploy runs pg_dump -Fc in the postgres container of the running release. It writes the dump to /opt/ririko/backups/predeploy/<UTC timestamp>-from-<version>.dump (owner root, mode 0600). <version> is the release that ran when the dump was made. The host keeps the newest five dumps. It makes no dump when no release runs. The dump uses the custom format, so restore it with pg_restore, not psql. The nightly ririko-backup keeps a similar dump off the host. How to restore that one is in the maintainer's private runbook.
Do these steps as root on the host, in one shell. They follow what ririko-deploy does (project ririko, the compose files of the release, the env file of the host).
-
Go to the folder of the host:
cd /opt/ririko -
List the dumps. Choose the newest one from before the problem:
ls -l backups/predeploy/ -
Show the version that runs now:
cat state/current -
Set
Vto the version in the file name of the dump. It is normally the same as the line from step 3:V=<the version in the dump file name> -
Set the path of the dump:
DUMP=/opt/ririko/backups/predeploy/<the file> -
Read the list of compose files of that release:
files=(); while read -r f; do files+=(-f "/opt/ririko/releases/$V/$f"); done < "releases/$V/.compose-files" -
Define a helper for compose. The env file is
/opt/ririko/.env.productionunlessRIRIKO_ENV_FILEin/etc/ririko/ririko.confnames another file. Use the same path here:rc() { RIRIKO_VERSION=$V RIRIKO_ENV_FILE=/opt/ririko/.env.production DB_AUTO_MIGRATE=false docker compose -p ririko "${files[@]}" --env-file /opt/ririko/.env.production "$@"; }The restore returns the database to the schema of that release. So you start the images of that release, not the failed one.
-
Check the dump. If the list is empty or the command fails, the file is bad. Take an older dump:
rc exec -T postgres pg_restore --list < "$DUMP" | head -
Take the lock that
ririko-deploy,ririko-backupandririko-watchdogshare. It is released when the shell ends:exec 9>/run/lock/ririko-deploy.lock; flock -w 300 9 -
Optional: dump the current state, if anything in it might still matter. The file is outside
predeploy/, so nothing deletes it:umask 077; rc exec -T postgres sh -c 'exec pg_dump -Fc -U "$POSTGRES_USER" -d "$POSTGRES_DB"' > "/opt/ririko/backups/before-restore-$(date -u +%Y%m%dT%H%M%SZ).dump" -
Stop the writers.
postgreskeeps running. Nothing else may use the database, because--cleandrops objects that a connection still uses:rc stop bot web -
Restore the dump.
--clean --if-existsdrops each object of the dump before it makes it again.--single-transactionmakes the restore all or nothing, so a failure leaves the database as it was. The single quotes are needed, because the variables belong to the container:rc exec -T postgres sh -c 'exec pg_restore --clean --if-exists --single-transaction -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < "$DUMP"The dump holds the migration records (
ririko_schema_migrations), so the restored database knows which migrations it has. -
Download the images of that release, only if they are gone from the host:
rc pull -
Start that release:
rc up -d --no-build --remove-orphans -
Wait until
rc psshowsbotandwebashealthy. Then runririko-deploy status.Pendingmust be(none). -
Fix the cause. The files in
state/are unchanged, socurrentstill namesV. Tag a new patch version with the fix. The next deploy dumps, migrates and starts as usual. IfVis not the version instate/current, deploy a fixed version at once. Do not leave the host onV.
A restore removes every change since the dump. This includes what users did while the old release ran during the migration. Tell the maintainer before you restore production.
8.8. Adopt a Database Without Migration Records
A database made before the migration runner existed has tables but no ririko_schema_migrations table. The first deploy with the runner does not run 0000_baseline on it. It adopts the database (database.md section 1). It compares the live schema with the baseline, adds what is missing in one transaction, and records 0000_baseline as applied. If it cannot repair the difference by adding, it stops and changes nothing.
A database that the runner made from an empty one is never adopted. Do this section once for each host whose database has no migration records. Run ririko-deploy status on production before the first approval to see which case you have.
-
Update the host scripts. A deploy never does this. Run
bootstrap.shas root on the host. Set--refto the release that adds the runner. The private runbook has the full command.sudo bash <path-to>/bootstrap.sh --ref <ref>To keep the run going if your SSH session drops, start it as a transient unit, then read its output:
sudo systemd-run --unit=ririko-bootstrap --collect bash <path-to>/bootstrap.sh --ref <ref>journalctl -u ririko-bootstrapCheck that the migration step is in the installed script. The command must print a number above 0:
grep -c db:migrate /usr/local/bin/ririko-deployA host that skips this step still deploys safely. The bot adopts and migrates when it starts, and a failed start ends in the rollback (exit 4). But this is not the gated path of section 8.6. Do not skip the step before the first deploy with the runner.
-
Make a manual dump. The deploy makes its own, but the host keeps only the newest five. Your manual dump stays. As root on the host, do this:
- Run steps 1 and 3 of section 8.7.
- Set
Vto the version that step 3 prints. - Run steps 6 and 7 of section 8.7. They define
filesand the helperrc. - Run step 10 of section 8.7, but name the file
backups/before-adoption-<UTC>.dump.
-
Check the dump. The file is not empty, and this command lists tables:
rc exec -T postgres pg_restore --list < <file> | head -
Deploy the first release with the runner as a prerelease tag, for example
v2.1.3-beta.1. It runsrelease,deploy-lavalink-staginganddeploy-staging, and stops before production (section 8.1). -
Read the adoption lines in the job output or in
/opt/ririko/deploy.log. Each starts withdb:migrate:. They come after the linemigrating the database from the <version> bot image. A good adoption looks like this:Target: PostgreSQL, host postgres, port 5432, database <database>Adoption: the database has tables but no migration records; recorded 0000_baseline as applied.N table(s) createdN column(s) addedN index(es) created✔ Adopted the database and applied N migration(s): <ids>If the release has no migration after the baseline, the last line is
✔ Adopted the database: recorded 0000_baseline as applied.A
⚠line under the counts is a difference that was left as it is, for example a column type with the same storage. Read each one.PostgreSQL has no
Backupline, because the pre-deploy dump is the backup.Then the deploy logs
the database is migrated for <version>. It starts the stack and waits until it is ready. -
Check the result with
ririko-deploy status:ssh deploy@<DEPLOY_SSH_HOST> statusRun it through the same ProxyCommand as the job. You see
Pending: (none),Unknown: (none),Adopted: yes (the database existed before migration records)andhealthycontainers. Open the dashboard once. It answers ready only on a migrated schema. -
Tag the plain version. The pipeline offers production after the approval job.
If the adoption is refused, the job fails with exit 6 and prints a report:
✖ Migration failed: The existing database cannot be adopted. Nothing was changed.
- <one line per problem>
The old release still runs, and the database is as it was. A problem is one of these:
- a
NOT NULLcolumn without a default is missing; - a column cannot be added;
- a column type differs from the baseline (PostgreSQL compares types exactly);
- card serials are duplicated.
-
Read the whole report. It lists every problem at once.
-
Fix each problem, with the manual dump at hand.
- For a schema problem, change the database by hand in a transaction until it matches
packages/database/migrations/pg/0000_baseline.sqlof the tag. Add the column with a default, or correct the type. - For duplicate serials, decide which owner keeps each serial.
- If every old database would have the same problem, fix the adoption in the code and tag a new prerelease instead.
- For a schema problem, change the database by hand in a transaction until it matches
-
To check a fix without a new deploy, run the plan on the host. Set
Vto the tag that failed. Its release files are already on the host. The helperrcis from section 8.7. The command prints the same report, or the plan, and changes nothing:rc run --rm --no-deps -T bot ririko db:migrate --dry-run </dev/null -
Deploy again. If only the database changed, run the failed job again. The host downloads the release again and starts from its recorded state. If the code changed, tag the next prerelease.
9. The Website (ririko.ai)
The documentation site in website/ is published with GitHub Pages on https://ririko.ai. It hosts neither the bot nor the dashboard.
- The workflow
.github/workflows/docs.ymlruns on every push tomasterand on Run workflow (workflow_dispatch). It runspnpm install --frozen-lockfile,pnpm buildandpnpm docs:build, uploadswebsite/buildand deploys it withactions/deploy-pages. Other branches never deploy, so the site and the install files always match a release. website/static/CNAMEholdsririko.ai.pnpm docs:buildalso runspnpm docs:install. It copiesdocker-compose.production.ymltowebsite/static/install/docker-compose.ymland.env.production.exampletowebsite/static/install/.env.example. The copies are git-ignored and are never edited by hand. The site then serves https://ririko.ai/install/docker-compose.yml and https://ririko.ai/install/.env.example.- The Kanban board is published separately, as a read-only static site on the Vercel project
ririko-bot(vercel.jsonrunsnode scripts/kanban/build-board-site.mjsand servesboard-dist/). It is not part of ririko.ai.
9.1. One-Time Setup (Maintainer)
Do these once, after 2.0 is on master. Until then ririko.ai stays down.
- In the repository on GitHub, open Settings, then Pages. Under Build and deployment, set Source to GitHub Actions.
- In Cloudflare, open the DNS records of
ririko.ai. Replace the old apex record with the GitHub Pages addresses: fourArecords (185.199.108.153,185.199.109.153,185.199.110.153,185.199.111.153), or oneCNAMEon@toririkoai.github.io(Cloudflare flattens it). Replaceririkoaiwith the GitHub organization name if it differs: the value is<organization>.github.io. - Set the apex records to DNS only (grey cloud) until GitHub has issued the certificate. Leave the other records as they are.
- Back in Settings, Pages, wait for the DNS check to pass, set the custom domain to
ririko.aiif it is empty, then tick Enforce HTTPS when GitHub offers it. - Run the workflow once from the Actions tab (Docs site, Run workflow) or push to
master. Open https://ririko.ai and https://ririko.ai/install/docker-compose.yml to check. - Keep the Vercel project
ririko-bot: it hosts the Kanban board. In its Settings, check that the Production Branch is the branch whose board you want to publish, and that Root Directory is the repository root.
To build the site on your computer:
pnpm build
pnpm docs:build
pnpm docs:serve
10. Production Go-Live
This is the one list to follow on launch day. Every step says who does it, the exact command or setting, and the check that proves it. Do the steps in order. Do not go on until the check of a step passes.
- Maintainer: accounts, secrets, tags, approvals, and the GitHub, Docker Hub and Cloudflare settings.
- Coordinator: the commands on the hosts over SSH, only after the maintainer says go. The commands run as root on the host (
sudo). The maintainer's private runbook has the SSH details. - Workers do none of these steps.
Placeholders: <DEPLOY_SSH_HOST> is the tunnel hostname of a host (section 8.2). <dashboard-url> is the public HTTPS address of the production dashboard, for example the value of DASHBOARD_URL. <path-to> is the folder on the host that holds bootstrap.sh. Never put a real IP address, provider, region or machine size in a file of the repository.
Production starts with an empty database. The first deploy runs the migrations from 0000_baseline and does not adopt anything, so section 8.8 does not apply.
10.1. Before You Tag
-
Maintainer. Check that the release is on
masterand that its version is right. Updatemaster(section 4, step 4), then print the version:node -p "require('./package.json').version"Check: it prints
2.0.0, and theciworkflow of CircleCI is green for the newest commit ofmaster. -
Maintainer. Check that staging runs and is healthy before you start. The coordinator runs this on the staging host:
sudo ririko-deploy statusCheck:
Pending: (none),Unknown: (none), andbotandwebarehealthy. -
Maintainer. Make or choose the Discord application of the production bot. Follow deployment.md section 2.2. Under OAuth2, Redirects, add
<dashboard-url>/api/auth/callback. Keep the bot token, the application ID and the client secret for step 8.Check: the Developer Portal shows the redirect after Save Changes.
-
Maintainer. Make three new secrets for production. Do not reuse the staging values. Run the command three times: once for
SECRET_VAULT_KEY, once forLAVALINK_PASSWORDand once forPOSTGRES_PASSWORD:openssl rand -hex 32Check: each run prints 64 characters. Keep an offline copy of the vault key: without it the stored credentials cannot be read again.
-
Maintainer. Set up the Cloudflare tunnel for the production app host. In Cloudflare Zero Trust, create the tunnel and save its token in a file that you give to the coordinator. Add two public hostnames: the dashboard to
http://localhost:3000, and the SSH hostname (<DEPLOY_SSH_HOST>) tossh://localhost:22. Create a service token and give the Access application of the SSH hostname a Service Auth policy that includes it (section 8.2, step 6).Check: both hostnames show in the tunnel, and the service token is in the Service Auth policy.
-
Maintainer. Make a new key pair for the production app host, and one for the production Lavalink instance (section 8.2, steps 1 and 2). Keep each private key for step 10, and give the coordinator the two public keys (
deploy_key.pub).Check: each private key is one line of base64, and the two key pairs are different.
-
Coordinator. Bootstrap the production app host. Put
bootstrap.shon the host (the private runbook says how). Use the branchmaster: the tag does not exist yet. Pass the public key of the app host and the tunnel token file:sudo bash <path-to>/bootstrap.sh --ref master --role app --deploy-key "<public key of the app host>" --tunnel-token-file <path to the token file>Check: the output ends with
Done: ... (role app), and showsCI deploy key: installedandCloudflare tunnel: service present. This command prints a number above 0:grep -c db:migrate /usr/local/bin/ririko-deploy -
Maintainer gives the values, coordinator writes the files. Write the settings of the production app host. Never put these values in a commit, a ticket or a chat.
- Create
/opt/ririko/.env.production(owner root, mode 0600) from.env.production.example. Fill in the seven values of deployment.md section 2.4 with the secrets from steps 3 and 4. SetDASHBOARD_URLto<dashboard-url>. SetCLIENT_IP_HEADER=cf-connecting-ip, because every request comes through Cloudflare (deployment.md section 4). SetLAVALINK_HOSTto the WireGuard address of the Lavalink host andBOT_OWNER_IDto the Discord user ID of the owner. - Edit
/etc/ririko/ririko.conf(see/etc/ririko/ririko.conf.examplefor every key). SetRIRIKO_COMPOSE_FILES="docker-compose.production.yml docker-compose.remote-lavalink.yml",WG_ADDRESS,WG_PEERSandHEALTHCHECK_PING_URL. - Run the command of step 7 again, so that WireGuard is set up.
Check: this command prints
root:root 600:stat -c '%U:%G %a' /opt/ririko/.env.productionThe output of
bootstrap.shshowsWireGuard: wg0 up as <address>, notkey only. - Create
-
Coordinator. Make sure that the Lavalink host runs the production instance (section 8.5).
- If
LAVALINK_INSTANCESon the Lavalink host already includesproduction, skip to the check. - Otherwise run
bootstrap.sh --ref master --role lavalink --deploy-key-production "<public key of the Lavalink production instance>"on the Lavalink host. Add the WireGuard public key of the production app host toWG_PEERSwith the port 2333, then runbootstrap.shagain. Create/opt/ririko/lavalink-production.env(owner root, mode 0600) fromdeploy/lavalink/lavalink.env.example. ItsLAVALINK_PASSWORDis the value of step 4.
Check: on the Lavalink host,
sudo ririko-deploy status productionprintsstatus: lavalink-production current=none previous=none, andstat -c '%U:%G %a' /opt/ririko/lavalink-production.envprintsroot:root 600. - If
-
Maintainer. Create the two CircleCI contexts
deploy-productionanddeploy-lavalink-production, each with the five variables of section 8.2. The coordinator prints the value ofDEPLOY_KNOWN_HOSTSon each host (section 8.2, step 5). Then delete the private key files (section 8.2, step 4). Add the security group of the maintainers to both contexts (section 8.2).Check: Organization settings, Contexts lists both contexts with five variables each, and Security shows the group.
-
Coordinator. Print the state of the production app host before the first deploy:
sudo ririko-deploy statusCheck: it prints
status: current=none previous=none. This is the proof that nothing runs yet, so the first deploy starts from an empty database.
10.2. Tag and Approve
-
Maintainer. Create and push the release tag from
master(section 4, steps 4 to 6):git checkout mastergit pullgit tag -a v2.0.0 -m "Ririko 2.0.0"git push origin v2.0.0Check: the
releaseworkflow starts in CircleCI. -
Maintainer. Wait for
release,deploy-lavalink-staginganddeploy-stagingto turn green. Thenapprove-productionshows On Hold.Check: the three jobs are green. If one fails, production is not offered. Read the exit code in section 8.4 and fix the cause. Tag a new patch version: never reuse
v2.0.0for another commit. -
Coordinator. Check staging before the approval:
sudo ririko-deploy statusCheck: on the staging host,
Latestis the newest file inpackages/database/migrations/pgof the tag,Pending: (none), andbotandwebarehealthy. Also run this command on the staging host. It prints a body with"ready":true:curl -fsS http://127.0.0.1:3000/api/ready -
Maintainer. Open the workflow in CircleCI and click Approve on
approve-production.Check:
deploy-lavalink-productionand thendeploy-productionrun, and both turn green. A job that stays in Blocked means that you are not in the security group (section 8.2). -
Coordinator. Read the output of
deploy-production.Check: it shows the line
postgres is not running, starting it for the migration, then lines that start withdb:migrate:and end withApplied N migration(s): 0000_baseline, .... There is noAdoption:line. The job ends withdeployed 2.0.0and the messageThe host command succeeded.
10.3. Check Production
-
Coordinator. Print the state of the production app host:
sudo ririko-deploy statusCheck:
current=2.0.0,Latestis the newest file inpackages/database/migrations/pg,Pending: (none),Unknown: (none),Adopted: no, andbotandwebarehealthy. -
Coordinator. Check that the dashboard answers on the host and in public:
curl -fsS http://127.0.0.1:3000/api/readycurl -fsS <dashboard-url>/api/readyCheck: both commands exit with 0 and print a body with
"ready":true. The second command runs on any computer. -
Coordinator. Register the slash commands. Run steps 1, 3, 4, 6 and 7 of section 8.7 to get the helper
rc, then:rc exec -T bot ririko commands:sync --global --all-guildsCheck: the command prints the commands it registered, and exits with 0.
-
Maintainer. Add the production bot to your server (deployment.md section 2.6). Type
/ping. Open<dashboard-url>and sign in with Discord.Check: the bot answers
/ping, and the dashboard lists your server. Global commands can take a while to show; press Ctrl+R in Discord. -
Coordinator. Turn on the backup of production. Set the five backup keys in
/etc/ririko/ririko.conf(RIRIKO_ENV_NAME,RESTIC_REPOSITORY,RESTIC_PASSWORD,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY; see/etc/ririko/ririko.conf.example). The maintainer keeps an offline copy ofRESTIC_PASSWORD. Then run:sudo ririko-backup initsudo systemctl start ririko-backup.serviceCheck:
journalctl -u ririko-backupends without an error, and the monitor ofBACKUP_PING_URLshows a ping. The monitor ofHEALTHCHECK_PING_URLshows a ping every minute. -
Maintainer. Check the Docker Hub tags (section 4, step 8).
Check:
ririkoai/ririkobot:2.0.0andririkoai/ririkobot-dashboard:2show a manifest, andririkoai/ririkobot:lateststill shows the 1.4.0 digest.
10.4. Launch Day: Site, Texts and Settings
All of these steps are the maintainer's. The texts are written already. Paste them from the file named in each step.
-
Maintainer. Publish a GitHub release for the tag (section 4, step 9). Add a link to
docs/upgrading-from-1.4.mdin the notes.Check: the release shows on the Releases page of the repository.
-
Maintainer. Turn on HTTPS for the website. Section 9.1 must be done, and the workflow Docs site must have deployed from
masterat least once. Open Settings, Pages, and tick Enforce HTTPS. If the box is grey, wait for GitHub to issue the certificate and open the page again.Check: this command prints a
Location: https://ririko.ai/header:curl -sI http://ririko.aiAlso open https://ririko.ai/docs in a browser. It loads without a warning.
-
Maintainer. Set the description, the website and the topics of the GitHub repository. Click the gear next to About. The texts are in the entry "Proposed GitHub texts" of
docs/kanban/handovers/TASK-1913.md. Usehttps://ririko.aias the website.Check: this command shows the new description, the homepage and 16 topics:
gh repo view RirikoAI/RirikoBot --json description,homepageUrl,repositoryTopics -
Maintainer. Update the two Docker Hub overviews (section 3). Copy
docs/dockerhub/ririkobot.mdanddocs/dockerhub/ririkobot-dashboard.mdinto the Overview of their repositories.Check: both pages show the 2.0.0 tag table and the links to https://ririko.ai/docs.
-
Maintainer. Replace the wiki. Open the Home page of the wiki and replace all of it with the text in the entry "Replacement wiki Home text" of
docs/kanban/handovers/TASK-1894.md. In that text, change the branch in the GitHub link tomaster. Then open Settings, Features and untick Wikis.Check: before you untick it, the Home page of the wiki shows the pointer to https://ririko.ai/docs. After that, the repository has no Wiki tab.
10.5. The 1.4.0 Sunset and latest
-
Maintainer. Choose the date on which
latestmoves, at least 30 days after the release, and announce it (section 5).Check: the date is in the GitHub release notes, the README, the Docker Hub descriptions and on the support Discord server.
-
Maintainer. On that date, move
latestof both images (section 6, steps 1 to 4).Check: the digest of
ririkoai/ririkobot:latestequals the digest ofririkoai/ririkobot:2, and the same forririkoai/ririkobot-dashboard:docker buildx imagetools inspect ririkoai/ririkobot:latest -
Maintainer. Change the
latestrow in the tag table of both Docker Hub texts. The text of the new row is in the "Decisions & gotchas" part of thePROGRESSentry ofdocs/kanban/handovers/TASK-1913.md. Change it indocs/dockerhub/ririkobot.mdanddocs/dockerhub/ririkobot-dashboard.mdthrough a pull request, then paste both files into Docker Hub again (step 26).Check: both Docker Hub pages show
latestas the newest 2.x release.
10.6. Roll Back
Nothing in this section touches 1.4.0 data. A 1.4.0 user never let 2.x write to the ./data folder.
-
Coordinator. If
deploy-productionfails, read its exit code in section 8.4. On the first deploy there is no previous release, so the host does not roll back.- Exit 5: nothing runs. Find the cause in
/opt/ririko/deploy.loganddocker compose -p ririko ps, fix it on the host, and run the job again in CircleCI. - Exit 6: the migration failed or was refused. Nothing was started and the database is as the failed migration left it. Read the lines that start with
db:migrate:(section 8.6). If the cause is in the code, tag a new patch version. - Exit 255: the tunnel, the service token, the key or the pinned host key is wrong (section 8.2). Fix the context and run the job again.
Check: the job turns green, and step 17 passes.
- Exit 5: nothing runs. Find the cause in
-
Maintainer. If a later release breaks production, tag a new patch version with the fix and approve it. Never reuse a version number.
Check:
sudo ririko-deploy statusshows the new version ascurrentandPending: (none). -
Coordinator. If the new release did not become ready (exit 4), the host started the previous release again. The database keeps the migrations. Restore the dump only when the failed release damaged data (section 8.7). Tell the maintainer before you restore production.
Check:
sudo ririko-deploy statusshows the previous version ascurrentandhealthycontainers. -
Coordinator. To take the bot and the dashboard offline while you fix a problem, run steps 1, 3, 4, 6 and 7 of section 8.7, then:
rc stop bot webCheck:
rc psshows no runningbotandweb. A deploy job, orrc up -d --no-build, starts them again. -
Maintainer. If 2.x must be pulled back after
latestmoved, follow section 7.Check:
ririkoai/ririkobot:latestshows the 1.4.0 digest again.
11. npm Package
The npm package ririko is the starter that runs with npx ririko (apps/starter). It is not the bot. It looks up the newest bot release when it runs, so a bot release does not need an npm release.
- The workflow
.github/workflows/npm-publish.ymlruns only when you push a tag that starts withnpm-v, such asnpm-v2.0.0. Image releases usev*tags in CircleCI and never start it. - It uses npm trusted publishing. GitHub gives the job an identity token, and npm accepts it for the package you linked in section 11.1. The repository holds no npm token.
- The workflow builds the starter, runs its tests, and fails when the tag version differs from the
versioninapps/starter/package.json. Then it runsnpm publish --provenance --access public, so the npm page shows where the package was built. A version with a dash, such as2.1.0-beta.1, is published with the tagnextand notlatest. - The starter has its own version number. Raise it only when the starter itself changes.
11.1. One-Time Setup (Maintainer)
- Sign in to https://www.npmjs.com as an owner of the package
ririko. - Open the package page, then Settings. Find Trusted Publisher and choose GitHub Actions.
- Fill in the form. Spelling and case must match GitHub exactly:
- Organization or user:
RirikoAI - Repository:
RirikoBot - Workflow filename:
npm-publish.yml - Environment name: leave it empty.
- Organization or user:
- Click Set up connection.
- After the first successful publish, you can set Publishing access on the same page to Require two-factor authentication and disallow tokens. Trusted publishing keeps working.
Check: the Trusted Publisher section shows the repository and the workflow file name.
11.2. Release a New Version
-
Change
versioninapps/starter/package.jsonthrough a pull request, and merge it intomaster. The first release is2.0.0, which is the version in the file now. -
Go to
masterand update it:git checkout mastergit pull -
Create the tag. The part after
npm-vis the exact version:git tag -a npm-v2.0.0 -m "ririko 2.0.0 on npm" -
Push the tag. This starts the workflow:
git push origin npm-v2.0.0 -
Open the Actions tab on GitHub and watch Publish to npm.
Check: the workflow is green.
11.3. Check the Release
-
Show the newest version on npm:
npm view ririko versionCheck: it prints the version you tagged.
-
Show the dist-tags:
npm view ririko dist-tagsCheck:
latestis the version you tagged (for a prerelease,nextis). -
Run the starter from npm:
npx ririko@latest --versionCheck: it prints the same version.
-
Open https://www.npmjs.com/package/ririko. Check that the README shows the banner and that the page shows the provenance badge.
11.4. Deprecate the 1.x Versions (Once)
The versions before 2.0.0 are the old 1.x bot, and their installer no longer works. After 2.0.0 is on npm, mark them as deprecated:
-
Log in to npm. This asks for your password and a one-time code:
npm login -
Deprecate every version below 2.0.0:
npm deprecate "ririko@<2.0.0" "This is the old Ririko 1.x bot and its installer no longer works. Run npx ririko to install the current Ririko, or see https://ririko.ai"
Check: npm view ririko@1.0.0 deprecated prints the message. Use a version that exists in npm view ririko versions.
11.5. If the Publish Fails
-
The tag and the version differ. The check step fails before anything is published. Delete the tag, fix the version, and tag again:
git tag -d npm-v2.0.0git push origin :refs/tags/npm-v2.0.0 -
npm answers with a 404 or a sign-in error. The trusted publisher does not match. Check the four values in section 11.1 against the repository and the workflow file name. They are case sensitive. Then run the failed job again from the Actions tab.
-
The npm CLI is too old. Trusted publishing needs npm 11.5.1 or newer. The workflow installs npm 11. If the step that prints the npm version shows an older number, fix the workflow.
-
The version already exists on npm. npm never accepts the same version twice, not even after an unpublish. Raise the version and tag the next one.