AI agent skill · by @nightly-labs
Signal Deploy
Check and deploy Signal (the `remote-api` service in `remote/`) to production from a release tag. Use when asked to deploy, redeploy, update or roll back Signal, to check that a Signal deployment has all its secrets and no regressions, or when a release tag changes `remote/` or `packages/contracts/src/signal-protocol`.
Archived 2026-10-23 · Source updated 2026-10-11. Check the original repository for the latest version.
SKILL.md instructions
# Signal deploy
Signal runs on `sui-alexandria` in the Compose project `openbot-remote` (`/opt/openbot/remote`). It
has no database: a restart closes the signalling WebSockets and empties the in-memory rooms, routes
and resume-token cache. Active WebRTC data channels stay up. coturn is a separate container.
[docs/remote-session-deployment.md](../../../docs/remote-session-deployment.md) is the source for
the staging, build and rollback commands. This skill adds the checks before and after them.
## When to deploy
CI and the release workflow do not deploy Signal. For each release, find the running commit (step 1).
When `git diff --quiet <running> <tag> -- remote packages/contracts/src/signal-protocol` finds changes,
deploy Signal from the local tag before the tag is pushed. Compare with the running commit, not with
the previous tag: an earlier release can have left Signal behind. The push starts the release
workflow, which does not wait for Signal ([docs/RELEASING.md](../../../docs/RELEASING.md), preflight
item 16). Example: #1661. The v0.33.0 clients sent `webhookRoute`, but production Signal
was older than #1520. It had no `/v1/webhooks` route and refused webhook-only ingress sockets with
`authentication_required`.
## Rules
- Deploy only a release tag, from a detached worktree: `git worktree add --detach <dir> <tag>`. Do not
check out the tag in a checkout that another agent uses.
- Deploy Signal only. Do not run `remote:update` or `remote:up`: they also recreate coturn and end
relay sessions. Change coturn only when the developer asks.
- Never print, copy off the host, or log an environment value, `.env.keys`, a container environment,
a token, a ticket or raw connection logs. Compare secrets by key name, or by a hash on the host.
- The deploy reads the host files `/opt/openbot/remote/.env.production` and `.env.keys` through
symlinks, not the `remote/.env.production` of the tag. The two files can differ.
- Get explicit approval from the developer before each production change: the deploy, a change to the
host environment file, and a change to the host Nginx configuration.
## 1. Find the running version
First read `commit` from the public health route:
```sh
curl -fsS https://signal.openbot.run/health/live
```
If `commit` is missing (a build from before this field) or `unknown` (built without
`OPENBOT_SOURCE_COMMIT`), find the commit by checksum:
```sh
ssh sui-alexandria 'docker inspect openbot-remote-remote-api-1 --format "{{.Image}} {{.Created}} {{.State.Health.Status}}"'
ssh sui-alexandria 'docker exec openbot-remote-remote-api-1 sh -c "cd /app/remote/api/src && sha256sum *.ts"'
ssh sui-alexandria 'ls -t /opt/openbot/releases'
```
Each release directory has the full commit SHA as its name. Compare the container checksums with
`git show "${commit}:remote/api/src/<file>" | shasum -a 256` for the newest release directories and
tags. In zsh, write `"${commit}:remote/..."`: `$commit:r` is a modifier. Record the running commit.
## 2. Check for regressions
Diff the running commit against the tag:
```sh
git diff --stat <running> <tag> -- remote packages/contracts/src/signal-protocol
git merge-base --is-ancestor <running> <tag>
```
If the running commit is not an ancestor of the tag, stop and report: the deploy would remove
changes that are live now.
- **Protocol.** In `packages/contracts/src/signal-protocol`, released messages and auth events must
stay compatible. New message types and optional fields are safe; a removed or renamed field is not.
A new `hello` field or a new public route that released clients use needs this Signal before those
clients ship.
- **Account service.** A new Signal call to the Worker (for example `/v2/remote/<x>-route/...`) must
exist in the deployed Worker. An unauthenticated `POST https://api.openbot.run/<path>` gives 401
when the route exists and 404 when it does not.
- **`remote/compose.yaml`.** A new variable with `:?` stops the start when the host file lacks it.
- **`remote/api/Dockerfile`.** A new workspace package must be copied before `bun install`.
- **`remote/nginx/signal.openbot.run.conf`.** The host uses its own copy at
`/etc/nginx/conf.d/openbot-remote.conf`. A new `location` in the repository needs the same change on
the host, after a backup, `nginx -t` and `systemctl reload nginx`. Without it, the request goes
through `location /`, which has the default 1 MB body limit and writes the path to the access log.
- **Tests.** In the tag worktree: `bun install --frozen-lockfile`, then
`bun run --cwd remote/api test`.
## 3. Check the secrets
1. Compare key names. Required keys are those that `remote/compose.yaml` marks with `:?`; optional
keys turn a feature off when they are empty (Slack, Discord, Telegram).
```sh
git show <tag>:remote/.env.production | grep -vE '^#|^$' | cut -d= -f1 | sort
ssh sui-alexandria "grep -vE '^#|^$' /opt/openbot/remote/.env.production | cut -d= -f1 | sort"
```
2. Make sure that `DOTENV_PUBLIC_KEY_PRODUCTION` is the same in both files. Then the host
`.env.keys` decrypts the encrypted values of the tag file.
3. For keys in both files, compare values by hash on the host: copy the encrypted tag file to a
`umask 077` temporary file, run `bin/dotenvx get -f <file> -fk .env.keys` for each file, and print
only `<key> same|DIFFERENT|only-in-tag|only-on-host`. Delete the temporary files.
4. Report each key that is only in the tag or that is different. The developer decides which value
goes live. To add a key, copy its encrypted line from the tag file into the host file after
`cp .env.production .env.production.bak-<date>-<reason>`. Do not replace the whole host file.
## 4. Deploy
Report the results of steps 1 to 3 and get approval. Then follow "Deployment procedure" in
[docs/remote-session-deployment.md](../../../docs/remote-session-deployment.md) with these changes:
- Use `git archive <tag>` and the release directory `/opt/openbot/releases/$(git rev-parse <tag>^{commit})`.
If the directory exists, do not write into it again.
- Tag the running image as `openbot-remote-api:before-<tag>` before the build.
- Build from the release directory. The document's build command passes its name, the tag commit,
as `--build-arg OPENBOT_SOURCE_COMMIT`, and stops when the name is not a full commit SHA.
- Run the Compose commands with `-p openbot-remote`, and build and start only `remote-api`.
## 5. Verify
- `docker inspect openbot-remote-remote-api-1 --format '{{.Image}} {{.State.Health.Status}}'` shows
the new image and `healthy`.
- `curl --fail https://signal.openbot.run/health/ready` succeeds.
- `curl --fail https://signal.openbot.run/health/live` shows `commit` equal to the tag commit (tags
from before this field show no `commit`; use the checksums then).
- The container `sha256sum` of `/app/remote/api/src/*.ts` matches the tag.
- `docker logs --since <deploy time> openbot-remote-remote-api-1 2>&1 | grep -iE 'error|disabled|off|telegram|discord|slack'`
shows no start errors, and shows each enabled feature. Do not print other log lines.
- Unauthenticated probes of each public route that the diff adds give 401 or 403, not 404 or 502.
For example, `POST /v1/webhooks/x` with `content-type: application/json` gives 401.
If a check fails, use the rollback commands of the deployment document with the `before-<tag>` image.
Keep that image until the developer confirms the device checks of the deployment document.
## Report
One table: running commit before and after (from `/health/live`, or from checksums), image IDs, each secret key status, each regression area,
health, test result, and the checks that need a device or the developer.