Skip to main content
Roadmap — not yet shipped

This document describes a feature in active design. It is not available in any deployed version of the platform yet. Schemas, APIs, and workflows described below are subject to change as the design evolves. Once the feature ships, this page will move to a dedicated Deployment section.

Deployment Manifest — Design Doc (Phase 1)

Status: Design only. No code. Reviewed and approved before any implementation begins.

Goal: Take the existing per-application DeploymentService (338 lines, real CRUD + state machine) and elevate to a manifest-versioned deployment model: a single deploy unit is a versioned bundle of {application, tag} pairs targeting one environment, stored permanently in DB for rollback.


Why this design

Today the platform has a complete per-application deploy backend (DeploymentController + DeploymentService) but no UI, no agent, and — more importantly — no concept of "release these N apps together as version v1.4.0." Teams shipping a multi-component release (e.g., user-service + user-service-ui + user-management-android) would have to issue N separate deploy calls and manually correlate them. The manifest model fixes this.

The proposal is a known industry pattern (GitLab Release, Spinnaker Pipeline Stages, Helm umbrella charts, Cloud Foundry deployments). It is being adopted here because:

  1. Multi-app releases need a single "what was deployed" record for audit + rollback.
  2. Release notes naturally aggregate across all apps in one manifest.
  3. The dependency-chain story (deploy A before B) needs a parent entity to scope the dependency graph.
  4. Self-tuning readiness gating (Phase 4) needs to bind score → deployment-as-a-whole, not per-app.

Architectural decisions

Decision 1: In-monolith, not a separate service

Keep manifest CRUD + orchestration in the main backend (src/main/java), not in a new deployment-agent Java module.

Rationale. The data model is tightly coupled to project_applications (FK), environments (FK), and the existing build_jobs / deployment_jobs tables. Splitting into a separate service would force one of:

  • Cross-service JOINs via RPC (high latency, harder to query)
  • Duplicating reference data in a deploy-DB (consistency problem)
  • Foreign-keys across DBs (not possible; would need application-level integrity)

None of these buy anything while deployment volume is zero. The existing readiness-agent split has costs we're still paying (missing from docker-compose.yml, has caused audit false-positives — see docs/SERVICE_MAP.md). Don't repeat that pattern unless the workload demands it.

When to revisit. If deployment volume reaches the point where independent scaling matters (e.g., 50+ apps deploying concurrently across many manifests) OR if the manifest workflow stabilizes and is genuinely independent from the rest of the platform.

Decision 2: Manifest-level orchestration; per-app execution stays in build_jobs + deployment_jobs

The new deployment_manifest table is the orchestration layer. The existing build_jobs and deployment_jobs tables become the per-app worker queue underneath a manifest. A manifest's lifecycle is the AND of its items' lifecycles.

This preserves all the existing per-app code paths. The new code is a thin orchestrator + a few new endpoints.

Decision 3: Environment is the deploy target, not minikube directly

Environments are already first-class (development, staging, production, etc.). The new model treats each environment as a target with its own K8s cluster context:

  • local → minikube on dev laptop (kubectl context: minikube)
  • dev → real cluster, namespace dev
  • int → real cluster, namespace int
  • stage, prod → same shape, different namespaces / clusters

This is a schema extension to environments, not a new table.

Decision 4: Per-application deployment workflow branches on application_tech

Two distinct paths:

Application typeBuildPushDeploy
SPRING_BOOT, REACT, NODE, GO, PYTHONdocker builddocker push <registry>/<app>:<tag>kubectl apply (or helm upgrade)
ANDROID, IOSgradle assembleRelease / xcodebuildupload APK/IPA to S3trigger mobile-test-agent (no "deploy", just publish)

So ProjectApplication.registry_type gets a new value S3. The deployment-agent (Phase 3) branches on application_tech to pick the right pipeline.


Data model

New tables

CREATE TABLE deployment_manifest (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL REFERENCES projects(id),
version_label VARCHAR(50) NOT NULL, -- user-provided semver: "v1.4.0"
target_environment_id BIGINT NOT NULL REFERENCES environments(id),
status VARCHAR(20) NOT NULL, -- see state machine below
release_notes TEXT, -- aggregated, generated on demand
created_by VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deployed_by VARCHAR(100),
deployed_at TIMESTAMP,
completed_at TIMESTAMP,
failure_reason TEXT,
version BIGINT NOT NULL DEFAULT 0, -- JPA optimistic lock
UNIQUE (project_id, version_label, target_environment_id)
);

CREATE TABLE deployment_manifest_item (
id BIGSERIAL PRIMARY KEY,
manifest_id BIGINT NOT NULL REFERENCES deployment_manifest(id) ON DELETE CASCADE,
application_id BIGINT NOT NULL REFERENCES project_applications(id),
tag VARCHAR(100) NOT NULL, -- "v1.2.3" or commit SHA
depends_on_application_id BIGINT REFERENCES project_applications(id), -- nullable; FK constraint optional
build_job_id BIGINT REFERENCES build_jobs(id), -- populated when build kicked off
deployment_job_id BIGINT REFERENCES deployment_jobs(id), -- populated when deploy kicked off
item_status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- mirrors manifest states at item level
UNIQUE (manifest_id, application_id)
);

CREATE INDEX idx_manifest_status ON deployment_manifest(status);
CREATE INDEX idx_manifest_item_status ON deployment_manifest_item(item_status);

Schema extensions to existing tables

-- environments: which K8s context/namespace is this env?
ALTER TABLE environments ADD COLUMN k8s_context VARCHAR(100); -- kubeconfig context name, e.g. "minikube" or "proofarc-dev"
ALTER TABLE environments ADD COLUMN k8s_namespace VARCHAR(63); -- K8s namespace name (DNS label limit)
ALTER TABLE environments ADD COLUMN deploy_strategy VARCHAR(30) DEFAULT 'ROLLING'; -- ROLLING | BLUE_GREEN | CANARY
-- ingress_hostname is already optional info; consider adding if needed for release notes link generation

-- project_applications: support S3 as a registry destination (for mobile APKs/IPAs)
-- Existing column registry_type is already VARCHAR; just adds 'S3' to the enum of valid values.
-- No schema change; just a documented expansion of valid values.

State machine

┌──── DRAFT ────┐
│ │ │
│ ▼ │
│ READY │ ◄── "all apps tagged, manifest finalized; user can click Deploy"
│ │ │
│ ▼ │
│ DEPLOYING ────┼──► FAILED (any item fails; halt + record failure_reason)
│ │ │
│ ▼ │
│ DEPLOYED │ ◄── steady state
│ │ │
│ ▼ │
└► ROLLED_BACK ─┘ ◄── superseded by a re-deploy of an older manifest
  • DRAFT — manifest created, items being added/edited
  • READY — manifest frozen, items locked, awaiting Deploy click
  • DEPLOYING — agent is processing levels
  • DEPLOYED — terminal success
  • FAILED — terminal failure; preserves state for diagnosis; new manifest required to recover
  • ROLLED_BACK — explicit user action to revert to this manifest from a later one; sets completed_at = NOW() of the new "deployment of old manifest"

Items have a parallel state on item_status for fine-grained tracking.


Dependency chain — algorithm

Each manifest_item may set depends_on_application_id referencing another item in the same manifest. The graph must be a DAG (cycle → manifest rejected at create time).

Deploy execution:

1. Build dep DAG from items.
2. Compute topological levels (Kahn's algorithm):
- Level 0 = items with no remaining deps
- Level N+1 = items whose deps all completed in levels ≤ N
3. For each level L:
a. Dispatch all items in L to the agent queue concurrently (build → push → deploy).
b. Wait until all items in L reach DEPLOYED (or FAILED).
c. If any FAILED, halt manifest with FAILED; do not enter level L+1.
4. When highest level completes, manifest → DEPLOYED.

Worked example for the User Service stack:

manifest v1.4.0:
user-service (no deps) -> Level 0
user-service-ui (deps: user-service) -> Level 1
user-management-android (deps: user-service) -> Level 1

Execution: user-service deploys alone first. Once green, user-service-ui and user-management-android deploy in parallel. Failure of either halts the manifest; the other continues until done (since they're already dispatched) but no further levels execute. (Decision point: cancel in-flight peers on failure, or let them complete? Current proposal: let in-flight complete, do not start new items. This avoids partial-K8s-rollout states.)


API surface

Six new endpoints under ApiScenarioController's sibling, a new DeploymentManifestController:

VerbPathPurposeAuth
POST/api/projects/{projectId}/manifestsCreate DRAFT manifest from {items: [{appId, tag, dependsOnAppId?}], versionLabel, environmentId}ADMIN
GET/api/projects/{projectId}/manifestsList manifests for project; pagination + filter by status/envADMIN, ANALYST, VIEWER
GET/api/projects/{projectId}/manifests/{manifestId}Manifest detail incl. items + job linksADMIN, ANALYST, VIEWER
PUT/api/projects/{projectId}/manifests/{manifestId}Edit DRAFT (add/remove items, change tag); rejected for non-DRAFTADMIN
POST/api/projects/{projectId}/manifests/{manifestId}/deployTransition DRAFT/READY → DEPLOYING; enqueue level 0 itemsADMIN
POST/api/projects/{projectId}/manifests/{manifestId}/rollbackRe-deploy this manifest as new "current"; marks previous DEPLOYED manifest ROLLED_BACKADMIN
GET/api/projects/{projectId}/manifests/{manifestId}/release-notesAggregate release notes across all items (uses existing per-app releases/generate-notes)ADMIN, ANALYST, VIEWER

Notes:

  • All 6 endpoints live on the main backend (per Decision 1).
  • The existing DeploymentController endpoints (per-app) stay; they're consumed under the manifest orchestrator, not deprecated. A direct per-app deploy is still possible for hotfix scenarios.
  • Auth uses existing @PreAuthorize roles; deploy/rollback restricted to ADMIN since they affect production.

What's in Phase 1 vs deferred

Phase 1 (this design)

  • Schema migrations (2 new tables + 3 columns on environments)
  • Java entities: DeploymentManifest, DeploymentManifestItem, with audit fields + @Version
  • Repository + service + controller (manifest CRUD + deploy/rollback transitions)
  • Integration tests covering: create-edit-deploy happy path, dep-chain ordering, failure halt, rollback
  • No agent. Manifest moves to DEPLOYING but stays there until Phase 3 ships the agent. Items are visible in the queue.
  • No UI. Backend-only.

Phase 2 — Manifest UI (security-agent-ui)

  • New page /projects/:id/manifests (list + create wizard)
  • Wizard steps: pick apps → pick tag per app → set dep edges (optional) → set version label + environment → review → create
  • Detail page with deploy/rollback buttons and live item-status

Phase 3 — deployment-agent

  • New sibling module deployment-agent/ (per the readiness-agent pattern, but WITH a docker-compose entry from day one — see Service Map rules)
  • Polls /api/.../agent/pending-builds and /api/.../agent/pending-deployments
  • Branches on application_tech:
    • Docker stack: clone → docker build → docker push → kubectl apply or helm upgrade
    • Mobile: build APK → upload S3 → notify mobile-test-agent
  • Reports completion via existing per-app /agent/{jobId}/complete and /agent/{jobId}/fail

Phase 4 — Closed-loop verification

  • On deploy completion, auto-trigger a UI test suite or mobile test suite against the new environment
  • Results feed back into the release readiness score for the next manifest
  • Score gate on the next manifest's Deploy action

Open questions to resolve before Phase 1 starts

  1. Manifest version label uniqueness. UNIQUE (project_id, version_label, target_environment_id) — is that right? Or should v1.4.0 be globally unique per project and only deployable to one environment at a time? Current proposal allows the same label across environments (deploy v1.4.0 to dev, then later to stage) which feels correct.

  2. Tag validation. Should the backend verify the tag exists in the upstream Git repo at manifest-create time, or trust the user? Verifying requires git creds + network call. Proposal: verify at deploy time (when the agent clones), not at manifest creation, to keep create-manifest fast.

  3. In-flight peers on failure. When a level-1 item fails, do we cancel the other level-1 items that are still running? Proposal: let them complete; don't start level 2. Rationale: cancelling an in-flight docker build or kubectl apply produces partial state worse than letting it finish.

  4. Concurrent manifests. Can two manifests target the same environment simultaneously? Proposal: yes at the schema level, but the agent should serialize per-(env, app) pair to avoid two manifests fighting over the same K8s deployment. Lock TBD — DB advisory lock or per-(env, app) Redis lock.

  5. Mobile manifest items. Does deploying a manifest with an Android item really "deploy" anything, or is it just "publish APK"? Proposal: same state machine, but the DEPLOYED state for mobile means "APK uploaded to S3 and mobile-test-agent notified" — not "running on a device." Different semantics, same lifecycle.

  6. Audit log. Should manifest state transitions be written to audit_log? Almost certainly yes — release decisions need a permanent record. To confirm with the user.


What this does NOT cover

  • Build pipelines, CI orchestration. The backend assumes a tag already exists in Git. CI/build is out of scope.
  • Multi-cluster atomic deploys. A single manifest targets one environment (one cluster/namespace). Multi-region atomic deploys deferred.
  • Canary or blue/green strategy specifics. deploy_strategy column accepts the value but Phase 1's agent will only implement ROLLING. Others future.
  • Drift detection. "Is what's actually running in K8s still what the latest DEPLOYED manifest said?" — defer.
  • Approval workflows. Deploy is a direct ADMIN action. Multi-stage approval (e.g., dev auto-deploy, prod requires sign-off) deferred.

Estimated effort

Backend-only Phase 1 (entities + repos + service + controller + tests, no agent, no UI):

  • Schema migration + Java entities: ~0.5 day
  • Service + state machine + dep-chain algorithm: ~1 day
  • Controller + DTOs: ~0.5 day
  • Integration tests: ~1 day
  • Total: 2.5–3 dev-days for a competent reviewer-ready PR.

This is backend logic only. Real visible value lands when Phases 2 + 3 land.