|
|
|
@@ -0,0 +1,200 @@
|
|
|
|
|
# Clean-host deployment and rollback audit
|
|
|
|
|
|
|
|
|
|
## Verdict
|
|
|
|
|
|
|
|
|
|
The current `hub/deploy` path is **not a production deployment mechanism**.
|
|
|
|
|
It can update a manually-prepared host in the happy path, but a clean supported
|
|
|
|
|
host cannot become a runnable Hub from the documented inputs, and an upgrade
|
|
|
|
|
failure can leave the live application tree partially replaced with no
|
|
|
|
|
application rollback path.
|
|
|
|
|
|
|
|
|
|
The accepted initial topology remains viable: one Linux/systemd host,
|
|
|
|
|
PostgreSQL, Feishu WebSocket ingress, and a non-root Hub service. The blocker is
|
|
|
|
|
the release contract and its verification, not a need to replace that topology.
|
|
|
|
|
|
|
|
|
|
## Executed evidence
|
|
|
|
|
|
|
|
|
|
### Static gates
|
|
|
|
|
|
|
|
|
|
- `bash -n hub/deploy/install_service.sh` and
|
|
|
|
|
`bash -n hub/deploy/deploy_platform.sh` pass.
|
|
|
|
|
- ShellCheck reports only informational SC2029 findings for intentional
|
|
|
|
|
client-side expansion of remote paths. It does not find a shell syntax defect.
|
|
|
|
|
- A source-to-installer comparison finds six `requireEnv` settings in
|
|
|
|
|
`hub/src/server.ts`, but the generated production environment omits
|
|
|
|
|
`HUB_PROJECT_WORKSPACE_ROOT` and `HUB_SESSION_SECRET`.
|
|
|
|
|
|
|
|
|
|
### Seeded configuration cannot boot the built server
|
|
|
|
|
|
|
|
|
|
The installer was run against a temporary `BASE`, with the real built Hub as
|
|
|
|
|
`HUB_DIR`. It exited successfully after creating `platform.env`. The generated
|
|
|
|
|
file was then used as the production configuration while supplying the model and
|
|
|
|
|
Feishu values that its message asks the operator to fill. The built server
|
|
|
|
|
exited with status 1:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
[hub] missing required env: HUB_PROJECT_WORKSPACE_ROOT
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
After that is supplied, `HUB_SESSION_SECRET` is the next required omission.
|
|
|
|
|
`HUB_PUBLIC_BASE_URL` is also absent; its code fallback is localhost, which is
|
|
|
|
|
not a valid public OAuth callback for a production deployment.
|
|
|
|
|
|
|
|
|
|
### The service identity is referenced but not provisioned
|
|
|
|
|
|
|
|
|
|
The installer was executed with `SERVICE_USER=definitely-missing-cph-user` and
|
|
|
|
|
mocked `install`/`systemctl` boundaries. It returned success and rendered:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
User=definitely-missing-cph-user
|
|
|
|
|
WorkingDirectory=.../hub
|
|
|
|
|
EnvironmentFile=.../.secrets/platform.env
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
There is no `getent`, `id`, `useradd`, or equivalent validation/provisioning
|
|
|
|
|
path. It also creates no service home, cache, state, or project-workspace
|
|
|
|
|
directory. This contradicts ADR-0018's implemented deployment invariant that
|
|
|
|
|
the Hub runs as a dedicated unprivileged service user. A genuinely clean host
|
|
|
|
|
will fail at systemd user resolution; a manually-created user can still fail
|
|
|
|
|
later when the agent SDK, `cph` cache, or project workspace needs a writable
|
|
|
|
|
path.
|
|
|
|
|
|
|
|
|
|
### A failed build mutates the live tree before failure
|
|
|
|
|
|
|
|
|
|
`deploy_platform.sh` was run with mocked SSH and rsync boundaries. The first
|
|
|
|
|
remote build was forced to exit 42. The observed order was:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
ssh:mkdir -p '/srv/curriculum-project-hub/hub'
|
|
|
|
|
rsync:live-tree-mutated
|
|
|
|
|
ssh:cd '/srv/curriculum-project-hub/hub' && npm ci && npm run build
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The deploy command correctly propagated status 42, but it had already applied
|
|
|
|
|
`rsync --delete` to the one live directory. There is no staged release,
|
|
|
|
|
immutable release identity, current/previous pointer, deployment lock, or
|
|
|
|
|
rollback command. `npm ci` also replaces that live directory's dependency tree
|
|
|
|
|
before the new build has passed.
|
|
|
|
|
|
|
|
|
|
## Blocking gaps
|
|
|
|
|
|
|
|
|
|
### 1. Incomplete clean-host bootstrap
|
|
|
|
|
|
|
|
|
|
`install_service.sh` writes a unit containing `User=cph-hub` without creating or
|
|
|
|
|
validating that account. It does not create owned state/cache/workspace paths,
|
|
|
|
|
and its generated environment omits required settings. On its first invocation
|
|
|
|
|
it deliberately exits after seeding the environment, after which
|
|
|
|
|
`deploy_platform.sh` proceeds to restart a unit that has not yet been installed.
|
|
|
|
|
The documented workflow is therefore neither one-shot nor complete.
|
|
|
|
|
|
|
|
|
|
### 2. The runtime product is not fully delivered
|
|
|
|
|
|
|
|
|
|
The deploy uploads only `hub/`. The target prerequisite list does not include a
|
|
|
|
|
preinstalled `cph`, and no release step builds or installs `crates/cph-cli`.
|
|
|
|
|
The Hub starts without checking it: the first agent `cph check` or `cph build`
|
|
|
|
|
returns exit 127 while `/api/healthz` remains green. Bubblewrap is asserted by
|
|
|
|
|
the unit, but Node/npm, curl, PostgreSQL connectivity, service-user namespace
|
|
|
|
|
support, writable storage, and `cph --version` are not checked as one preflight.
|
|
|
|
|
|
|
|
|
|
### 3. Releases are in-place and have no application rollback
|
|
|
|
|
|
|
|
|
|
Source, dependencies, build output, and the running service all share one
|
|
|
|
|
directory. A build failure, interrupted rsync, or unsuccessful restart can
|
|
|
|
|
leave a mixed tree. No release manifest records the git revision, Node version,
|
|
|
|
|
dependency lock digest, migration set, or `cph` version that was deployed.
|
|
|
|
|
Concurrent deploys are not serialized.
|
|
|
|
|
|
|
|
|
|
### 4. The deployment health check is only liveness
|
|
|
|
|
|
|
|
|
|
`/api/healthz` returns a constant success response after the HTTP server starts.
|
|
|
|
|
It does not verify database access, migration state, workspace writability,
|
|
|
|
|
`cph`, the agent sandbox, or Feishu listener startup. The listener's start
|
|
|
|
|
promise is not awaited or reflected in readiness. A release can therefore be
|
|
|
|
|
declared successful while its defining workflows are unavailable.
|
|
|
|
|
|
|
|
|
|
The service unit also claims graceful shutdown, but `server.ts` installs no
|
|
|
|
|
`SIGTERM`/`SIGINT` handler and never calls `app.close()` or closes the Feishu
|
|
|
|
|
listener. Node's default SIGTERM handling terminates the process; Fastify's
|
|
|
|
|
documented graceful path begins only when `fastify.close()` is invoked. The
|
|
|
|
|
full data-integrity consequence belongs to the run-lifecycle audit, but the
|
|
|
|
|
current deployment contract must not claim draining behavior it does not have.
|
|
|
|
|
|
|
|
|
|
References:
|
|
|
|
|
|
|
|
|
|
- [Fastify shutdown hooks](https://fastify.dev/docs/latest/Reference/Hooks/)
|
|
|
|
|
- [Node.js signal events](https://nodejs.org/api/process.html#signal-events)
|
|
|
|
|
|
|
|
|
|
### 5. Application rollback and schema migration are not coordinated
|
|
|
|
|
|
|
|
|
|
The unit runs `prisma migrate deploy` immediately before every start. Current
|
|
|
|
|
migration history includes column drops, index replacement, and data
|
|
|
|
|
deduplication; there is no declaration that the previous application remains
|
|
|
|
|
compatible after each migration. Switching application code back after a
|
|
|
|
|
successful migration is therefore not generally safe.
|
|
|
|
|
|
|
|
|
|
Prisma documents that `migrate deploy` applies pending migrations but does not
|
|
|
|
|
detect schema drift, and a partially failed migration requires explicit
|
|
|
|
|
operator recovery. Its recommended zero-downtime direction is an
|
|
|
|
|
expand-and-contract sequence. The repository has no pre-deploy migration
|
|
|
|
|
status gate, production-data rehearsal, backup checkpoint, expand/contract
|
|
|
|
|
rule, or failed-migration runbook.
|
|
|
|
|
|
|
|
|
|
References:
|
|
|
|
|
|
|
|
|
|
- [Prisma production migration behavior](https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production)
|
|
|
|
|
- [Prisma failed-migration recovery](https://docs.prisma.io/docs/orm/prisma-migrate/workflows/patching-and-hotfixing)
|
|
|
|
|
- [Prisma expand-and-contract migrations](https://docs.prisma.io/docs/guides/database/data-migration)
|
|
|
|
|
|
|
|
|
|
### 6. Network binding configuration is misleading
|
|
|
|
|
|
|
|
|
|
The installer writes `HOST=127.0.0.1`, but `server.ts` ignores it and binds
|
|
|
|
|
`0.0.0.0`. An operator following the generated configuration can unintentionally
|
|
|
|
|
expose the Hub directly instead of binding it behind the intended TLS reverse
|
|
|
|
|
proxy. Public URL validation and proxy topology are not part of deployment
|
|
|
|
|
preflight.
|
|
|
|
|
|
|
|
|
|
## Required release contract
|
|
|
|
|
|
|
|
|
|
A production-capable implementation on the accepted topology should establish
|
|
|
|
|
these invariants:
|
|
|
|
|
|
|
|
|
|
1. **Provisioned identity and storage.** Installation creates or validates a
|
|
|
|
|
non-login `cph-hub` user and explicit persistent state, cache, home, and
|
|
|
|
|
workspace paths with tested ownership.
|
|
|
|
|
2. **Complete validated configuration.** A preflight validates every required
|
|
|
|
|
secret/value, the public HTTPS URL, bind address, PostgreSQL connectivity,
|
|
|
|
|
writable storage, `bwrap`, and the exact compatible `cph` binary before the
|
|
|
|
|
live service is touched.
|
|
|
|
|
3. **Immutable staged releases.** Each revision is built in a new release
|
|
|
|
|
directory, identified by revision and manifest. The live service points to a
|
|
|
|
|
`current` symlink only after build and preflight pass. Deploys are locked.
|
|
|
|
|
4. **Complete product artifact.** The release carries both the Hub and the
|
|
|
|
|
matching `cph` executable; readiness checks the binary/version rather than
|
|
|
|
|
deferring failure to an agent run.
|
|
|
|
|
5. **Real readiness.** Liveness remains cheap, while readiness proves the
|
|
|
|
|
dependencies needed to accept work. Deployment success waits for readiness,
|
|
|
|
|
not merely an open HTTP port.
|
|
|
|
|
6. **Application rollback.** A failed switch returns `current` to the previous
|
|
|
|
|
release and verifies readiness. Database changes are never silently reversed.
|
|
|
|
|
7. **Migration compatibility.** Every release states whether its migration set
|
|
|
|
|
is backward-compatible with the previous application. Future destructive
|
|
|
|
|
changes use expand/contract releases; failed migrations follow an explicit
|
|
|
|
|
recovery runbook tied to backup evidence.
|
|
|
|
|
8. **Observable lifecycle.** Release revision, migration status, readiness
|
|
|
|
|
failures, restart/drain outcomes, and rollback results are present in command
|
|
|
|
|
output and service logs.
|
|
|
|
|
|
|
|
|
|
## Verification route
|
|
|
|
|
|
|
|
|
|
The implementation is not complete until an automated Linux test drives these
|
|
|
|
|
scenarios with the real scripts and a production-like PostgreSQL instance:
|
|
|
|
|
|
|
|
|
|
- first install on a host without the service user or directories;
|
|
|
|
|
- incomplete configuration fails before unit installation/restart;
|
|
|
|
|
- staged build failure leaves the current release byte-for-byte unchanged;
|
|
|
|
|
- successful deploy reports revision and passes readiness;
|
|
|
|
|
- failed new release switches back to the previous application;
|
|
|
|
|
- missing/wrong `cph`, unwritable workspace, unavailable database, and failed
|
|
|
|
|
migration each produce a distinct non-zero deployment failure;
|
|
|
|
|
- repeated installation/deployment is idempotent;
|
|
|
|
|
- concurrent deployments serialize or one fails clearly.
|