v0.20.1 Deployment Discipline Plan
Purpose
v0.20.1 is a structural hardening release. It exists to prevent the production drift discovered during the CT 115 reconciliation window from recurring.
This is not a product-feature release. It is a deployment-discipline release.
Background
During the CT 115 reconciliation window, production was found to have drifted across three layers:
Disk: v0.20.0
Container: stale image
Database: stale migration ledger / missing governance tablesThe system has since been reconciled:
Disk: v0.20.0
Container: v0.20.0
Database: migration 036The remaining risk is recurrence: future rebuilds or restarts must not rely on manual migration application, manual Redis configuration, or manually remembered version checks.
Release split
v0.20.1 is split into two scopes.
v0.20.1-A — Structural invariants
These changes materially alter the system’s failure modes and should ship first.
- Add a fail-closed Docker entrypoint that runs database migrations before app startup.
- Persist Redis memory ceiling and
noevictionpolicy in Docker Compose. - Expose version, git SHA, and migration status in health/readiness responses.
- Update deployment documentation with the operational lessons from the reconciliation window.
v0.20.1-B — Version centralization
This is quality-of-life and observability hardening. It should follow v0.20.1-A unless implementation is trivial.
- Introduce a single build-info source.
- Remove hardcoded version strings from app/dashboard surfaces.
- Ensure fresh clones, tests, local dev, Docker builds, and dashboard bundles all have a safe fallback.
v0.20.1-A Scope
1. Fail-closed migration entrypoint
Decision
The app container must run migrations before starting the application process.
If migrations fail, the container must exit non-zero. The system must fail closed rather than running newer code against an uncertain schema.
Rationale
The migration runner is already idempotent and transactional. A failed migration indicates the database is not safe for the code being started.
Silent continuation would recreate the exact drift class this release is meant to prevent.
Expected implementation
Add a Docker entrypoint script similar to:
#!/usr/bin/env sh
set -e
echo "[entrypoint] running database migrations"
node scripts/migrate.js
echo "[entrypoint] starting GitWire app"
exec node packages/web/src/index.jsThe final app start command should match the current production image start command.
Acceptance criteria
- App startup runs
node scripts/migrate.js. - If migration fails, the container exits non-zero.
- If all migrations are already applied, startup proceeds normally.
- Logs clearly show migration status before worker startup.
- Rebuilding/recreating the app container does not require manual migration commands.
2. Durable Redis memory ceiling
Decision
Redis must retain append-only persistence and add a durable memory ceiling through the Compose command.
Use noeviction, not allkeys-lru.
Required Compose command
redis:
image: redis:7-alpine
command:
- redis-server
- --appendonly
- "yes"
- --maxmemory
- 256mb
- --maxmemory-policy
- noevictionRationale
BullMQ depends on Redis keys for queue state. Evicting keys with allkeys-lru can silently delete waiting, delayed, or active job state. noeviction is the correct failure mode because Redis write failures are loud, recoverable, and observable.
Acceptance criteria
After container recreation:
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policyExpected:
maxmemory = 268435456
maxmemory-policy = noeviction3. Health/readiness deployment visibility
Goal
A remote health/readiness check should make deployment drift visible.
Suggested fields
Health or readiness should include:
{
"status": "ok",
"service": "gitwire",
"version": "0.20.1",
"git_sha": "abc1234",
"db_migrations_applied": 36,
"db_migrations_available": 36,
"db_migration_status": "current"
}The migration count should be reported as applied vs. available, not as a hardcoded target. The system computes available by counting migration files in the image and applied by counting schema_migrations rows. db_migration_status is "current" when applied == available, and "behind" (with readiness degraded) otherwise. This way the check stays correct after migration 037, 038, and beyond — no release ever needs to update a baked-in number.
Acceptance criteria
- Health/readiness exposes app version.
- Health/readiness exposes git SHA when available.
- Health/readiness exposes applied migration count AND available migration count (applied vs available, not a hardcoded target).
- If applied < available, readiness should fail or clearly report degraded state.
- This check can be used after every deploy to confirm disk/container/database alignment.
4. Deployment documentation updates
Update the deployment runbook and related installation docs with:
The canonical migration path:
bashdocker exec gitwire-gitwire-app-1 node scripts/migrate.jsThe rule that Postgres
docker-entrypoint-initdb.druns only on first database creation.The Redis caveat:
CONFIG SETis process-memory only when Redis runs without a config file.- Redis maxmemory must be persisted through Compose command flags.
The fail-closed entrypoint behavior:
- migrations run before app startup,
- migration failure prevents app startup.
CRLF normalization during emergency
scphotfixes:bashsed -i 's/\r$//' <file>
Rationale
CT 115 is a Linux deployment. Some emergency edits originate from Windows working copies. CRLF drift can create noisy full-file diffs and obscure real changes during incident response.
v0.20.1-B Scope
Version centralization
Problem
Version strings have drifted across root package, workspace packages, dashboard footer, and running container reports.
Preferred approach
Use a committed fallback build-info module that can be overwritten during build.
This avoids a generated-file chicken-and-egg problem where fresh clones, tests, or local dev fail because the generated module does not exist.
Recommended pattern
Commit a fallback module with safe defaults:
export const BUILD_INFO = {
name: "gitwire",
version: "0.0.0-dev",
gitSha: null,
builtAt: null
};Then have a generation script overwrite it during Docker/build:
scripts/generate-build-info.jsThe script should read:
- root
package.jsonfor version, - git SHA when available,
- build timestamp.
It should write build-info modules for both backend and dashboard use.
Why committed fallback is required
If buildInfo.js is generated but gitignored, a fresh clone can fail before generation:
ERR_MODULE_NOT_FOUNDA committed fallback guarantees imports never fail. Build-time generation then improves the values without making the module’s existence conditional.
Acceptance criteria
- Fresh clone works before running generation.
- Tests work before running generation.
- Docker build overwrites fallback with real version/SHA.
- Dashboard footer does not hardcode version.
- Backend health reports the same version as the dashboard.
- Version source is root package version unless explicitly overridden at build time.
Out of scope for v0.20.1
The following are intentionally deferred:
- production validator image,
- backend isolation evidence generation,
- post-apply proof chain,
- LLM provider abstraction,
- code graph indexing,
- operator audit bundle UI.
These belong to the product roadmap after deployment discipline is stable.
Recommended execution order
Implement v0.20.1-A.
Rebuild CT 115 once.
Verify:
- migrations report all applied,
- Redis maxmemory persists after recreation,
- health/readiness reports version/SHA/migration count,
- logs are clean,
- all containers healthy.
Tag and release v0.20.1.
Implement v0.20.1-B as follow-up if not included.
Resume product roadmap.
Success definition
v0.20.1 is successful when this statement is true:
A fresh container rebuild can safely start GitWire against the current production database without manual migration commands, without losing Redis memory limits, and with externally visible version/schema status.