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'