Commit Graph

3 Commits

Author SHA1 Message Date
cesnimda 8f6f2ba8d6 fix(health): report configured application version
/health read the APP_VERSION environment variable directly, but
docker-compose passes App__Version, which binds to the App:Version
configuration key. The variable under that name never existed in the
container, so the endpoint always reported "unknown".

Read App:Version through IConfiguration, the approach AdminSystemController
already used for the same value. The resolution rule (configured version,
else assembly version) moves to a shared BuildMetadata helper rather than
being written twice; AdminSystemController now calls it, so the admin page
and /health cannot drift apart.

Local development is unaffected: nothing sets App:Version there, and the
assembly-version fallback still applies.

Tests pin the configuration KEY, not just the behaviour, including that an
App__Version environment variable binds to App:Version. The original bug
failed silently, so a behavioural test alone would not have caught it.

Verified against a running backend: App__Version=9.9.9-test reports
9.9.9-test; unset reports the assembly version rather than "unknown".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 18:59:26 +02:00
cesnimda 66b02bcab8 fix(deploy): load production environment before backup
deploy.sh symlinked /opt/job-tracker/shared/.env for docker compose but
never loaded it into its own shell. Its own decisions therefore ran
against an empty environment: DATABASE_PROVIDER fell back to sqlite on a
MariaDB host, so the deploy tarred the data volume, printed "Backup
verified" and continued with no database dump. The operator saw a green
backup line and a new file in the backups directory, and had no restore
point.

Load the shared env before any decision. Parsed line by line rather than
sourced, because a compose .env is not a shell script and an unquoted
value containing spaces would execute as a command. Values already in
the environment win, so CI-provided APP_VERSION and friends still
override the file. No value is echoed.

Remove the sqlite default. DATABASE_PROVIDER must be stated; missing or
unrecognised aborts the deploy.

Validate deployment configuration before the backup, and so before
anything is built, stopped or replaced: the connection string when the
provider needs one, AI_SERVICE_TOKEN (compose declares it with :?) and
AUTH_JWT_KEY (the backend throws on a blank key). Names in the output,
never values.

Verify each backup against its own format. A dump must be valid gzip,
contain CREATE TABLE, and carry the "Dump completed" trailer, so a dump
that died partway through is rejected. An archive must contain
jobtracker.db. A tar can no longer pass the dump check.

Also resolve the SQLite volume by its project-prefixed name and fail if
absent. The bare jobtracker_data name would have silently created an
empty volume and backed that up -- the same class of bug, found while
testing this fix.

Verified against a seeded MariaDB 11 container and real Docker volumes:
provider selection, all four validation failures, both backup formats
and their failure paths, truncated and trailer-stripped dumps, and zero
secret occurrences across every test's output.

Docs updated for the drift: deploy/README.md, deploy/first-production-
deployment.md, docs/release-candidate-review.md (B1 closed) and
.env.example, which now names the two database variables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 18:44:23 +02:00
cesnimda 25b64bee8a docs(ops): add first production deployment runbook
CI and Deploy / test (push) Failing after 1m3s
CI and Deploy / deploy (push) Has been skipped
Written against the actual implementation rather than the existing docs, and
validated locally against MariaDB 11 containers. No application behaviour
changed — this commit adds two documents.

deploy/first-production-deployment.md covers pre-deployment checks, the eight
deployment steps, smoke tests for backend, database and application, and
rollback. It documents what deploy.sh really does: it backs up first and aborts
on failure, and it replaces containers with up -d --force-recreate rather than
running compose down, so the window is container start time. It also records the
startup sequence as implemented — reconcile, migrate, reconcile — and that
Database.Migrate() throws rather than limping on.

Validation surfaced things worth writing down. The connection string resolves
from inside the backend container, so Server=127.0.0.1 means the container and
not the host; this broke a validation run before it could have broken a deploy.
DATABASE_PROVIDER defaults to sqlite, and if it goes missing the backend does not
quietly serve an empty database — it exits with "no such table:
INFORMATION_SCHEMA.TABLES", which is loud but baffling if unexplained. A blank
AUTH_JWT_KEY throws at startup when auth is required, which is the right
behaviour. The runbook maps each of these log lines to its cause.

Rollback is documented with the distinction stated plainly: a code rollback
keeps all data and is almost always the whole fix, while a database restore
discards everything written since the dump. Restore only when the data itself is
wrong.

docs/release-checklist.md records the completed architecture work, local
verification results, known risks with severities, the unresolved CI runner
blocker and what would unblock it, and seven first-deployment warnings.

Validated: compose build; compose up on a fresh MariaDB (42 tables, backend
healthy); the depends_on health gate holding the frontend until the backend is
healthy; restart against the populated database with rows preserved; backup and
restore; and the failure paths. Not validated, and said so in both documents: the
authenticated end-to-end journey, because signing in needs a password.

393 backend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 18:02:49 +02:00