From e77dac0be766f84d8079620f7dbeb9af168efb31 Mon Sep 17 00:00:00 2001 From: Maxi Date: Mon, 7 Sep 2026 08:40:15 +0100 Subject: [PATCH] chore: declare the API's env in Railway IaC, and release 0.0.3 `SENTINEL0_SECRET_KEY` was documented as something to set on the Railway dashboard, and `.railway/railway.ts` was left declaring only `DATABASE_URL` -- so `railway config plan` showed the key as a *removal*, and an apply would have deleted it. Since that key is what decrypts every stored tracker credential, the next apply would have made all of them unreadable, and the shape of the failure is that the deploy succeeds and the Integrations screen starts throwing instead. A service's `env` is reconciled rather than merged: a variable set outside this file and absent from it is removed. That is not specific to the new key -- `DASHBOARD_URL`, `CORS_ORIGINS`, `LOG_LEVEL`, `DATABASE_POOL_MAX`, `DATABASE_SSL` and `SLACK_WEBHOOK_HOST` were all one apply away from the same deletion, silently, because nothing reads them until the moment they matter. So every variable the api service reads is now declared with `preserve()`. A declared name that is not set yet is a no-op, while an undeclared one is a deletion waiting to happen, which makes declaring the full set strictly safer than declaring the ones that happen to be in use today. `PORT` stays out: it is injected by Railway rather than configured. `pnpm railway:plan` reads clean again. The version goes to 0.0.3 across the workspace and the cli's internal `@sentinel0/*` pins, with the lockfile regenerated to record them. Stale version numbers in the docs move with it: the sample `/health` output in api.md and deploy-cloud.md said 0.0.1, releasing.md packed a 0.0.1 tarball, and both it and publish-cli.yml used 0.0.2 as their "next release" example, which is now 0.0.4. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V2EALFY5U8TCSXHMTnUmra --- .github/workflows/publish-cli.yml | 2 +- .railway/railway.ts | 27 ++++++++++++++++++++++++--- CLAUDE.md | 8 ++++++++ docs/api.md | 4 ++-- docs/deploy-cloud.md | 7 ++++++- docs/releasing.md | 4 ++-- package.json | 2 +- packages/cli/package.json | 6 +++--- packages/cloud-api/package.json | 2 +- packages/cloud-dashboard/package.json | 2 +- packages/common/package.json | 2 +- packages/orchestrator/package.json | 2 +- pnpm-lock.yaml | 4 ++-- 13 files changed, 53 insertions(+), 19 deletions(-) diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 29f59cb..16208c2 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: 'Version to publish (e.g. 0.0.2). Leave blank to publish the current one.' + description: 'Version to publish (e.g. 0.0.4). Leave blank to publish the current one.' required: false dry_run: description: Pack and inspect without publishing diff --git a/.railway/railway.ts b/.railway/railway.ts index a4795d0..b78f7fd 100644 --- a/.railway/railway.ts +++ b/.railway/railway.ts @@ -48,9 +48,30 @@ export default defineRailway(() => { restartPolicyMaxRetries: 5, }, replicas: { 'europe-west4-drams3a': 1 }, - // preserve() keeps the value already set on Railway without writing a - // credential into source. - env: { DATABASE_URL: preserve() }, + /* + * `env` is reconciled, not merged: a variable set in the Railway dashboard + * and absent here is *removed* on apply. So every variable the service + * reads is declared, whether or not it is set today — a name listed with + * `preserve()` that does not exist yet is a no-op, while a name omitted is + * a deletion waiting for the next `railway config apply`. + * + * `preserve()` keeps whatever value Railway already holds without writing + * a credential into source. That is not a convenience for the secrets: it + * is the only reason they can be declared here at all. + */ + env: { + DATABASE_URL: preserve(), + // Required before any tracker credential can be stored: 32 random bytes + // encrypting them at rest. Losing it makes every stored token + // unreadable, so it must survive an apply. + SENTINEL0_SECRET_KEY: preserve(), + DASHBOARD_URL: preserve(), + CORS_ORIGINS: preserve(), + LOG_LEVEL: preserve(), + DATABASE_POOL_MAX: preserve(), + DATABASE_SSL: preserve(), + SLACK_WEBHOOK_HOST: preserve(), + }, }) // The dashboard. Its own Dockerfile, and no pre-deploy command — it owns no diff --git a/CLAUDE.md b/CLAUDE.md index 465c3e6..2fd2d74 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -311,6 +311,14 @@ deprecated: services could no longer opt in from 2026-08-28, and it retires on unrepresentable — with no root `railway.json`, a new service cannot inherit another's builder and silently deploy the wrong image. +A service's `env` block is **reconciled, not merged**: a variable set in the Railway +dashboard and absent from `railway.ts` is *removed* on apply. So every variable a +service reads is declared there with `preserve()`, whether or not it is set today — a +declared name that does not exist yet is a no-op, an undeclared one is a deletion +waiting for the next apply. `SENTINEL0_SECRET_KEY` is the case that made this +concrete: it is required, it can only be set outside source, and losing it makes every +stored credential unreadable. `PORT` stays undeclared because Railway injects it. + `pnpm railway:plan` previews; `pnpm railway:apply` applies after review. Neither deploys — `pnpm railway:deploy:api` and `pnpm railway:deploy:dashboard` do that, and they reconcile no configuration, so a change to `.railway/railway.ts` needs an apply diff --git a/docs/api.md b/docs/api.md index 055996c..752430a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -61,7 +61,7 @@ GET /health Unauthenticated, because Railway's health check runs before any key exists. ```json -{ "status": "ok", "version": "0.0.1" } +{ "status": "ok", "version": "0.0.3" } ``` --- @@ -507,7 +507,7 @@ API; the source of truth is Hermes itself. { "name": "cerebro", "hostname": "cerebro.local", - "version": "0.0.1", + "version": "0.0.3", "started_at": "2026-09-01T20:00:00.000Z", "last_seen_at": "2026-09-02T08:14:31.000Z", "hermes_ok": true, diff --git a/docs/deploy-cloud.md b/docs/deploy-cloud.md index a3eb5bf..bf56735 100644 --- a/docs/deploy-cloud.md +++ b/docs/deploy-cloud.md @@ -150,6 +150,11 @@ exactly like a working one. **Losing or rotating this key makes every stored credential unreadable.** They are not recoverable; re-add them under Settings → Integrations. +It is declared in `.railway/railway.ts` as `preserve()`, which is what keeps +`railway config apply` from removing it. A variable set only in the dashboard and not +named in that file is deleted on the next apply — so anything you add there by hand +needs a line there too. + Optional variables: | Variable | Default | Purpose | @@ -187,7 +192,7 @@ Confirm: ```bash curl https://.up.railway.app/health -# {"status":"ok","version":"0.0.1"} +# {"status":"ok","version":"0.0.3"} ``` `/health` is unauthenticated on purpose: the health check has to pass before any key diff --git a/docs/releasing.md b/docs/releasing.md index 840251c..f96a9db 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -196,7 +196,7 @@ things that only fail on a user's machine: ### Doing it by hand ```bash -pnpm version:set 0.0.2 # every package, and the cli's internal pins +pnpm version:set 0.0.4 # every package, and the cli's internal pins pnpm install --lockfile-only # the lockfile records those pins pnpm lint && pnpm test && pnpm build @@ -215,7 +215,7 @@ To test a tarball without publishing: ```bash mkdir /tmp/t && cd /tmp/t && npm init -y -npm install /path/to/sentinel0-0.0.1.tgz +npm install /path/to/sentinel0-0.0.3.tgz ./node_modules/.bin/sentinel0 --version ``` diff --git a/package.json b/package.json index 58d2c5c..fa6aec8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sentinel0-monorepo", - "version": "0.0.2", + "version": "0.0.3", "private": true, "type": "module", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 8738fa2..b4cdb92 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "sentinel0", - "version": "0.0.2", + "version": "0.0.3", "description": "Trigger Hermes agents from your tickets and pull requests.", "type": "module", "keywords": [ @@ -35,8 +35,8 @@ "dependencies": { "@clack/prompts": "1.4.0", "@fastify/cors": "11.2.0", - "@sentinel0/common": "0.0.2", - "@sentinel0/orchestrator": "0.0.2", + "@sentinel0/common": "0.0.3", + "@sentinel0/orchestrator": "0.0.3", "chalk": "4", "fastify": "5.7.4", "p-limit": "6.1.0", diff --git a/packages/cloud-api/package.json b/packages/cloud-api/package.json index 26113ca..b22224e 100644 --- a/packages/cloud-api/package.json +++ b/packages/cloud-api/package.json @@ -1,6 +1,6 @@ { "name": "@sentinel0/cloud-api", - "version": "0.0.2", + "version": "0.0.3", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/cloud-dashboard/package.json b/packages/cloud-dashboard/package.json index c98ff72..b5ddf19 100644 --- a/packages/cloud-dashboard/package.json +++ b/packages/cloud-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@sentinel0/cloud-dashboard", - "version": "0.0.2", + "version": "0.0.3", "private": true, "type": "module", "scripts": { diff --git a/packages/common/package.json b/packages/common/package.json index 2a327fa..95cc7e6 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@sentinel0/common", - "version": "0.0.2", + "version": "0.0.3", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/orchestrator/package.json b/packages/orchestrator/package.json index 4dc5083..2a245bd 100644 --- a/packages/orchestrator/package.json +++ b/packages/orchestrator/package.json @@ -1,6 +1,6 @@ { "name": "@sentinel0/orchestrator", - "version": "0.0.2", + "version": "0.0.3", "private": true, "type": "module", "scripts": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06b8702..e4825b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,10 +57,10 @@ importers: specifier: 11.2.0 version: 11.2.0 '@sentinel0/common': - specifier: 0.0.2 + specifier: 0.0.3 version: link:../common '@sentinel0/orchestrator': - specifier: 0.0.2 + specifier: 0.0.3 version: link:../orchestrator chalk: specifier: '4'