# Part 1 · Deployment

## 1.1 Deploying a release

**Who** · The server's operator.

**Where** · The deployer (Forge, Envoyer, Deployer), which runs `deploy/server/release.sh` in the new release's folder.

**Before you start** · The release is committed on `develop`. The server's environment has every key in
`deploy/production.env.example` (reji/docs/DEPLOYMENT.md).

**The steps**
1. The deployer makes the release's folder and runs the script: the PHP and JavaScript packages, the build, a backup
   (`backup:run --kind=predeploy`), then `app:deploy`.
2. `app:deploy` runs its steps in order (migrations, the option lists, roles, notifications, mail partitions, the brand,
   the caches) and stops at the first that fails.
3. The deployer switches the site to the new release. Then `php artisan queue:restart`, then `php artisan app:check`.

**What the record shows** · The backup `backup-predeploy-<when>.bak` in the backups folder; each step's output in the
deployer's log.

**Who is told** · Nobody, unless a readiness check then starts failing: the Service is e-mailed.

**What can go wrong**
- *A step fails.* The deployment stops; the previous release stays live. Fix the cause and deploy again.
- *"Backups aren't set up … nothing was backed up."* The release carries on; set them up (1.2).

## 1.2 Setting up backups

**Who** · The server's operator, with the Service (who keeps the private half).

**Where** · A shell on the server, then the server's environment.

**Before you start** · `pg_dump`, `pg_restore` and `createdb` of the database's major version, `tar`, and `rclone` with
the remote configured if copies go off the server. A folder outside the release for the backups.

**The steps**
1. `php artisan backup:key`. It prints the key once and writes it nowhere.
2. The **private half** goes into the password manager now, and nowhere on the server.
3. In the environment: `BACKUP_PATH` (the folder), `BACKUP_PUBLIC_KEY` (the public half), `BACKUP_REMOTE` (the rclone
   remote). For a remote that only takes writes, also `BACKUP_REMOTE_PRUNE=false`.
4. `php artisan backup:run`, then `php artisan backup:list`: the first backup.
5. `php artisan app:check`: **Backups** passes.

**What the record shows** · The backups folder, and the remote, hold `backup-nightly-<when>.bak`. The scheduler makes
one every night at 02:00 and removes what is past its keeping time at 02:50.

**Who is told** · Nobody. The operations page shows **Backups**.

**What can go wrong**
- *Backups: Needs a look, "on this server only".* No remote is named: one copy, on the machine it protects.
- *Backups: Failing, "Not set up yet", in production.* The folder or the public half isn't set.
- *"pg_dump failed: … server version mismatch".* The client tools are older than the database: install its version.

## 1.3 The monthly drill

**Who** · The server's operator, on a machine that may hold the private half for a few minutes (never the server).

**Where** · That machine's shell, with the app's code at the same release.

**Before you start** · The newest backup, copied from the remote (`rclone copy`) or the server (`scp`). The private
half, from the password manager.

**The steps**
1. `BACKUP_PATH=<the folder it is in> php artisan backup:restore <backup> --into=drill_<date>`.
2. Type the private half when asked.
3. Look into `drill_<date>` (`psql`): the workspaces, people and records you expect.
4. `dropdb drill_<date>`, and delete the copy of the backup.

**What the record shows** · Nothing in the app. Note the date and the result in the operations runbook.

**Who is told** · Nobody.

**What can go wrong**
- *"This backup wasn't made for that key."* The private half doesn't belong to the public half the server holds.
- *"This backup is damaged or cut short".* The file changed or the copy is incomplete: copy it again, or try the one
  before. Tell the Service.
- *"createdb failed: … already exists".* `--into` makes a new database only: choose another name.

## 1.4 Restoring a backup

**Who** · The server's operator, with the Service's agreement.

**Where** · A shell on the server.

**Before you start** · Everything since the backup was made will be lost: agree it first. Any erasure carried out
since must be carried out again afterwards (BACKUP.md, *Known gaps*).

**The steps**
1. `php artisan backup:list`, and choose the backup.
2. `php artisan backup:restore <backup>`. Answer **yes**, then **yes** again.
3. Type the private half when asked.
4. It puts the app into maintenance, restores the database in one transaction, swaps the private folders, and brings
   the app back.
5. `php artisan app:check`.

**What the record shows** · The app's log: `backup.restored`, with what was restored. The database is the backup's,
the audit log included.

**Who is told** · Nobody by the app: tell the workspaces what was lost.

**What can go wrong**
- *"The restore failed … The app stays in maintenance".* Look at what was restored, fix it, then `php artisan up`.
- *Files don't open after a restore on a new server.* `FILE_VAULT_KEY` isn't the one the files were stored with.

## 1.5 Running an app's dev agents on a Mac

**Who** · A developer.

**Where** · The app's folder on this Mac.

**Before you start** · The app's port block, from the registry (`/Users/reji/dev/registry.html`).

**The steps**
1. `php artisan app:agents <port>`: the agents (the server, a queue worker per running pool, the scheduler), printed.
2. `php artisan app:agents <port> --write`: written to `~/Library/LaunchAgents` (one there already is left alone).
3. `mkdir -p ~/Library/Logs/tutelora`, then `launchctl bootstrap gui/$(id -u) <each file>`.
4. Add the agents to the house rules' dev-server table.

**What the record shows** · A file each: `com.tutelora.<short>-<port>`, `-queue`, `-queue-bulk` and `-schedule`; their logs in
`~/Library/Logs/tutelora/`.

**Who is told** · Nobody.

**What can go wrong**
- *"… isn't a port."* Give the block's first port, four or five digits.
- *The server doesn't start.* Read `<short>-<port>.err.log`; never start a manual `artisan serve` on the same port.

## 1.6 Starting a new app from a release

**Who** · A developer.

**Where** · A new folder, then a shell in it (the starter pack's `reji/docs/runbooks/new-app.md` has every command).

**Before you start** · The release's tag, the app's short name and product word, its port block, and its colour from the
brand kit.

**The steps**
1. `git archive v<version>` of the starter pack into the new folder; the first commit, "Start from Atrium <version>".
2. `composer install`, then `php artisan app:rename <Short> --product=<Word> --port=<port>`.
3. Read what it prints: the brand (`BRAND_*`, `brand:import`, `brand:tokens`, `brand:check`), the docs, the checks.
4. `.env` from `.env.example`, `createdb` the two databases, `php artisan migrate --seed` (the demo, for this machine: a
   server starts from nothing, with `service:create` and New workspace), `npm ci && npm run build`.
5. The whole suite, `brand:check`, `core:status`, `app:check`; commit.

**What the record shows** · The new app's `PROJECT.md` records the release it started from; its `CHANGELOG.md` starts
with it; `core.json` holds that release's manifest in the app's names.

**Who is told** · Nobody.

**What can go wrong**
- *"This isn't release <version> as released".* The tree isn't the tag's: start again from `git archive`.
- *"The starter pack's name is still here".* Rename the lines it lists, or mark one that must name it
  `brand-guard: allow`.
- *`brand:check` fails a contrast pair.* Tune the palette's recipe for the new colour, never the guard.

## 1.7 Checking an app for drift

**Who** · A developer.

**Where** · A shell in the app.

**Before you start** · Nothing.

**The steps**
1. `php artisan core:status`.
2. It lists each foundation file changed or missing since the release the app is on, and ends with a count.
3. Each listed file is drift unless the app's `PROJECT.md` lists it as an exception: record it there, or bring it back
   to the release's (or lift the improvement into the starter pack).

**What the record shows** · Nothing: it reads only.

**Who is told** · Nobody.

**What can go wrong**
- *"No foundation manifest (core.json) here".* The app didn't start from a release: start a port plan.
