feat(deploy): prisma migration history, /version, galactus standalone deploy
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m49s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m2s

Closes the gap between "what tag did I deploy" and "what is actually running",
and gives the schema a history that can be reasoned about across releases.

Migrations
- Baseline the existing schema as 0000_init (migrate diff --from-empty). The
  schema had only ever been applied with `prisma db push`, so no history
  existed and schema state was disconnected from app version. Existing
  databases must be baselined once with `migrate resolve --applied 0000_init`;
  the workflows print this remedy on P3005.
- Run `prisma migrate deploy` as a deploy STEP, not the container CMD — as a
  CMD, N replicas would race each other applying the same migration.

Version reporting
- GET /version on the API reports the APP_VERSION / GIT_SHA / BUILD_DATE that
  build.yml already baked into both images but nothing ever read.
- The web footer shows the web build and flags an api/web mismatch. The two
  cannot drift at build time (one matrix run) but can at deploy time.
- Both deploy workflows now fail if the running API does not report the tag
  that was dispatched — a stack naming a tag is not proof of what is running.
- scripts/set-version.mjs stamps every package.json, which had all sat at
  0.1.0 while real releases shipped as v1.x.

Pre-migrate backup
- deploy/scripts/pre-migrate-backup.mjs dumps the database from INSIDE the
  still-running old API container over Portainer's Docker API, so the file
  lands in the volume the Operaciones restore screen reads. A dump taken on
  the CI runner would be unreachable by the only restore path we have.
  Verifies the artefact with `gzip -t` before letting the migration proceed.

galactus
- deploy/galactus/*.compose.yml: standalone-Docker ports of the Swarm stacks.
  Plain compose silently ignores `deploy:`, so restart_policy becomes
  `restart: unless-stopped` — without it nothing returns after a host reboot.
- .gitea/workflows/deploy-galactus.yml drives endpoint 3 with its own secrets.

Fixes
- deploy.yml passed `endpoint_id` and `pull_image` to
  cssnr/portainer-stack-deploy-action, which has no such inputs (they are
  `endpoint` and `pull`). The endpoint was silently never set.

docs/DEPLOY_AND_MIGRATIONS.md documents expand/contract as the rule for schema
changes: Prisma has no down-migrations, so a code rollback never rolls the
schema back, and restoring the replication master from a dump diverges every
replica.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 11:41:12 -07:00
co-authored by Claude Opus 5
parent 9ba5d2d09a
commit 4ee7ec71f0
18 changed files with 1677 additions and 8 deletions
+12
View File
@@ -153,6 +153,18 @@ export function logout(): Promise<{ success: boolean }> {
return apiFetch<{ success: boolean }>("/auth/logout", { method: "POST" });
}
export interface ServiceVersion {
service: string;
version: string;
gitSha: string;
buildDate: string;
}
/** What the API container reports it is running. Unauthenticated by design. */
export function getApiVersion(): Promise<ServiceVersion> {
return apiFetch<ServiceVersion>("/version");
}
export function getStats(): Promise<CustomerStats> {
return apiFetch<CustomerStats>("/customers/stats");
}
+42
View File
@@ -0,0 +1,42 @@
/**
* The web image's own build identity.
*
* Same runtime-injection trick as API_ORIGIN (lib/api.ts): docker/web.Dockerfile
* bakes APP_VERSION / GIT_SHA / BUILD_DATE as ENV, layout.tsx reads them on the
* server per request and paints them into window.__APP_BUILD__. Reading
* process.env directly from a client component would return undefined — Next
* only inlines NEXT_PUBLIC_* into the browser bundle, and baking the version in
* at build time is exactly what we are avoiding elsewhere.
*/
export interface BuildInfo {
version: string;
gitSha: string;
buildDate: string;
}
export const UNKNOWN_BUILD: BuildInfo = {
version: "dev",
gitSha: "unknown",
buildDate: "unknown",
};
/** Server-side read, used by layout.tsx to produce the injected payload. */
export function readBuildInfoFromEnv(): BuildInfo {
return {
version: process.env.APP_VERSION ?? UNKNOWN_BUILD.version,
gitSha: process.env.GIT_SHA ?? UNKNOWN_BUILD.gitSha,
buildDate: process.env.BUILD_DATE ?? UNKNOWN_BUILD.buildDate,
};
}
/** Browser-side read of what layout.tsx injected. */
export function webBuildInfo(): BuildInfo {
if (typeof window === "undefined") return readBuildInfoFromEnv();
const injected = (window as { __APP_BUILD__?: BuildInfo }).__APP_BUILD__;
return injected ?? UNKNOWN_BUILD;
}
/** First 7 chars, the length git itself abbreviates to. */
export function shortSha(sha: string): string {
return sha === "unknown" ? sha : sha.slice(0, 7);
}