SDD: Version Control Service (Release Manifest) — ft-internal-tools
-
Status: DRAFT
-
Task grade: architecture-led
-
Ticket: DEV-2755 — CI/CD manager > Finalize sprint > Implement version control service
-
Deadline: 12 August 2026
-
Long-form companion: Release Manifest — design reference
|
Baseline: Section 5 defines the storage so the later move to Postgres is additive: the stored form is
already a single self-contained document per manifest, which is exactly the shape a The one thing this feature needs that |
0. Requirements and assumptions
Elicited answers
-
Where the service lives → a module inside CI/CD Manager, not a fourth application.
-
How the deployment checklist is filled → generated draft from the deployment contracts and a manual editor, because the generated part cannot be trusted for every case.
-
How support receives the page → a link in the finalize-sprint e-mail; the page itself published on the documentation portal, with the authenticated UI reserved for the release manager.
-
Scope against the deadline → keep all phases in, reduce depth per phase, and state which items may not make the date.
Assumptions taken
-
The feature is built on
mainand stores manifests as JSON, deliberately not waiting for the Postgres work — see the baseline note above and the migration path in section 5. -
Manifest volume stays in the order of tens per year. At roughly 50 KB per snapshot that is about 1 MB after a year in one file; if it grows past that, section 5 names the split.
-
The manifest reproduces the prototype
release-manifest-v11.htmland the reference guide v1.0 plus its Docker Pull addendum. Two documented deviations are listed in section 10. -
Rendering is a port of the working prototype, not new development. This is the largest schedule assumption.
-
The documentation portal is an acceptable reading surface for support engineers, i.e. no per-customer access control is required.
1. Problem and goal
The Release Manifest replaces the legacy VersionReleaseControl spreadsheet and the
deployment guides on the shared disk: one page that tells a support engineer which version
and build of every component is current and compatible, what changed this sprint, and what
a customer on an older version must apply. Today it exists only as a prototype whose data
arrays are hand-edited. Goal: the manifest is produced automatically when a sprint is
finalized, curated by the release manager, and published where support can read it.
2. Scope
In
-
A manifest snapshot per sprint release and per hot-fix build, generated from the release record, the Jira items and the deployment contracts.
-
An admin-only Version Control tab in CI/CD Manager for reviewing the draft and editing the deployment checklist.
-
A self-contained HTML artifact published to the documentation portal, linked from the finalize e-mail.
-
Both views from the guide: Releases and Upgrade Path.
Out
-
Editing releases themselves. The manifest is a read model; corrections happen upstream.
-
Per-customer access control or customer-facing distribution.
-
Kubernetes deployments. The checklist is Docker Compose only.
3. Affected components
CI/CD Manager backend — new routes/manifest/ (_crud.py, _publish.py) and
services/manifest/ (storage.py, catalog.py, solution.py, builder.py,
contract_diff.py, contracts.py, renderer.py). No schema change: storage.py is a
storage service in the shape of services/release_storage.py.
Touched — routes/workflow/release/_changelog.py (persist the resolved Jira items),
routes/workflow/release_build.py (hot-fix snapshot), routes/workflow/email.py (CTA
button), frontend/js/sprint-release.js (one call after the docs-portal phase).
CI/CD Manager frontend — new manifest-render.js, manifest-upgrade.js,
manifest-admin.js, manifest-editor.js, css/manifest.css.
Shared libraries — ft-jira-client gains an additive customers field alongside the
existing hasCustomer flag; ft-github-client gains a REST transport for reading a
repository file at a given ref, which the contract reader needs.
Documentation portal — release-process.adoc and sprint-end-checklist.adoc cover the
release and sprint-end procedure this feature changes, so they are updated in the same
change.
4. Design and approach
A manifest is an immutable snapshot written when a release is finalized. It embeds the
component catalogue, the dependency edges, the versions, the Jira items and the checklist
as they were at that moment, so re-reading an old release is unaffected by later renames.
The catalogue is composed rather than re-entered: FT components come from
data/projects.json (adding a layer attribute), third-party components and their pinned
tags from the deployment.yaml contracts. The Solution version is derived from the FTACS
major.minor version shipped in the release.
The deployment checklist is a generated draft with a human override: a semantic diff of the deployment contracts produces the mechanical part, the release manager corrects it, and regeneration never overwrites an edited field. Diffs are computed between manifest snapshots — each manifest stores a contract fingerprint — rather than between git refs, so nothing depends on the contracts repository being tagged per release.
One renderer serves both consumers: it is a pure function from the snapshot bundle to DOM, fed from the API inside the application and inlined into the published artifact.
Alternatives considered
-
A standalone
version-control-managerapplication — rejected: every input already lives in CI/CD Manager, so it would fetch all of its data back over HTTP and pay for its own auth, database, chart and CI first. -
A module in Deployment Manager — rejected: it owns the contracts but none of the release, version or Jira data, and it deliberately dropped its
platform_versionconcept in migration20260520_0003.
5. Data model and DB changes
No DB changes. Manifests live in data/release_manifests.json, written through the same
storage service as the rest of the application, with services/manifest/storage.py in the
shape of services/release_storage.py — a read-modify-write under a module-level lock so
concurrent draft edits cannot clobber each other.
{ "manifests": [
{ "id": "sprint-7", // stable, also the DOM anchor and deep-link fragment
"type": "sprint", // 'sprint' | 'hotfix'
"releaseId": "4390807f-...", // sprint manifests: the release record
"buildRunId": null, // hot-fix manifests: the release-build run
"solutionId": "sol-7",
"solutionVersion": "7.0",
"targetBranch": null, // hot-fix only, e.g. '7.0.x'
"name": "Sprint 7",
"date": "2026-06-18",
"status": "draft", // 'draft' | 'published'
"publishedAt": null,
"publishedBy": null,
"createdAt": "...", "updatedAt": "...",
"snapshot": { } // the self-contained document, see below
}
] }
The snapshot document carries components, layers, solutions, deps, items,
versions, checklist, contracts and source; the full shape is in
the design reference. Everything a
published manifest renders from is inside it — the record above is only the index the
sidebar and the publish flow need.
JsonStorage.write truncates and rewrites in place, and JsonStorage.read
swallows a parse error and returns the default. Together that turns an interrupted write
into a silently empty history, and the next write makes the loss permanent. Manifest
storage writes to a temporary file in the same directory and os.replace-s it into
position, and treats an unreadable file as an error rather than as an empty list. This is
release history — losing it quietly is the one failure mode that is not recoverable.
|
Two additive changes elsewhere: an optional layer attribute on each project, and the
resolved Jira items persisted on the release record (they are currently computed during
changelog generation and discarded).
Migration path to Postgres
The storage choice is deliberately the only thing that has to change when the database
work lands. The record above maps one-to-one onto a table whose snapshot is a jsonb
column:
CREATE TABLE cicd.release_manifests (
id text PRIMARY KEY,
release_id uuid NULL, -- no FK, per the DB contract
build_run_id uuid NULL,
type text NOT NULL,
solution_id text NOT NULL,
solution_version text NOT NULL,
target_branch text NULL,
name text NOT NULL,
release_date date NOT NULL,
status text NOT NULL,
snapshot jsonb NOT NULL,
published_at timestamptz NULL,
published_by text NULL,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL
);
CREATE UNIQUE INDEX release_manifests_release_id_idx
ON cicd.release_manifests (release_id) WHERE release_id IS NOT NULL;
CREATE UNIQUE INDEX release_manifests_build_run_id_idx
ON cicd.release_manifests (build_run_id) WHERE build_run_id IS NOT NULL;
Three rules keep that migration additive, and they constrain the code written now:
-
Callers never touch the file. Everything goes through
services/manifest/storage.py; no route, builder or renderer readsdata/directly. The migration then replaces one module’s internals, following the dual-read/write facade already used for release history. -
Field names are the column names. The JSON record uses the names above so the mapping is mechanical, and
snapshotstays one opaque document rather than being spread across columns. -
No JSON-only affordances. Nothing relies on ordering within the file, on rewriting unrelated records in the same write, or on reading the whole list to answer a single-id lookup.
When the move happens, snapshot becomes a JSONB column, and SQLAlchemy does not
track in-place mutation of JSON column contents — a nested list appended to a shallow copy
compares equal and the UPDATE is skipped. The repo layer must deep-copy before mutating,
the same trap already handled in the release and cleanup-schedule repos.
|
6. FE-BE contract changes
New admin-only routes under /api/manifest, all returning the app’s existing JSON envelope.
GET /api/manifest/releases -> sidebar list
GET /api/manifest/<id> -> one snapshot
GET /api/manifest/bundle -> all published snapshots (renderer input)
POST /api/manifest/draft -> build or rebuild a draft
PUT /api/manifest/<id> -> release-level edits
PUT /api/manifest/<id>/checklist/<compId> -> one checklist entry, marks it edited
POST /api/manifest/<id>/publish -> render, commit to the portal, publish
GET /api/manifest/export.html -> the self-contained artifact
No change to any existing endpoint.
8. Logging and observability
Draft generation and publication log against the release id at INFO with
releaseId=<id> manifestId=<id> action=<draft|publish> result=<ok|failed>; a generation
failure logs at WARNING and is recorded as a release-level log entry, visible in the
release history, because generation must never fail the finalize flow. The contract reader
logs which resolution source answered, so a stale contract can be traced.
9. Test plan
pytest — test_manifest_catalog.py (projects and contracts compose; the database role
expands to mysql and oracle; an unknown alias is reported, not dropped);
test_manifest_solution.py (FTACS 7.0.0 gives sol-7 / 7.0; a release without FTACS falls
back instead of crashing); test_manifest_builder.py (status new / updated / stable /
hotfix / breaking; unreleased components inherit from the previous snapshot; the pull
command echoes the recorded imageUrl verbatim — the regression guard for deviation 1);
test_manifest_contract_diff.py (variable added, removed, default changed; image tag
changed; no change gives a quiet row); test_manifest_editor_precedence.py (regeneration
preserves every edited field); test_routes_manifest.py (auth on every route, draft and
publish transitions); test_manifest_storage.py (round-trip of a full snapshot;
concurrent draft edits serialise instead of clobbering; a write interrupted before
os.replace leaves the previous file intact; an unreadable file raises instead of
reporting an empty history).
vitest — manifest-upgrade.test.js (chronological order, the from-exclusive to-inclusive
window, later supersedes earlier, no-change entries skipped, same-version and downgrade
validation); manifest-render.test.js (pull cell, status badge class, diff row builder).
Playwright — draft, edit a checklist entry, publish; sidebar groups hot-fixes above sprints; a breaking entry shows its banner and the diff tabs switch; Upgrade Path builds a combined checklist; the pull command copies to the clipboard; a hot-fix release shows no version table.
A Playwright quality mark on the finalize flow must be green before sprint-release.js is
touched.
10. Risks, rollout and follow-ups
Two deviations from the source documents. Implementing them verbatim would ship something wrong.
-
The addendum specifies a Harbor tag built as
<version>_<build>, e.g.7.0.6_0.0.2. The pipeline actually pushesv<version>-b<build>— a real release record carrieshub.friendly-tech.com/configs/ft-configs-service:v1.0.1-b0.0.1. The manifest therefore renders the recordedimageUrlverbatim and never recomputes a tag, so a support engineer cannot be handed a pull command the registry will reject. -
The guide shows a customer name on bug items. The Jira integration reduces the "Project (assets)" field to a boolean, discarding the identity. Whether the API response carries a readable label or needs a second Assets call is unverified; a half-day spike decides, and the fallback is a manual field.
Schedule. The window from 31 July to 12 August is 9 working days against roughly 19 days of design. Every phase ships at reduced depth, there is no buffer, and the backfill of previous releases is already out of the window. The three items most likely to follow it, in order: the Upgrade Path view, customer names, and hot-fix manifests. Generation at finalize and publication cannot slip — without either, nothing reaches a support engineer.
Storage. JSON is chosen against the deadline, not on merit: the Postgres work is not
close to release, and coupling this feature to it would hand the date to another project.
The cost is a file rewritten in full on every save and a history that is one file rather
than one row per manifest. Section 5 constrains the code so the later move is a change to
one module; the risk is that those three rules erode under time pressure, at which point
the migration stops being additive. The test_manifest_storage.py cases are what keep
them honest.
Decay risk. Hot-fix manifests are the one deferral that loses data:
release_build_history prunes after 10 days, so hot-fixes shipped while it is outstanding
cannot be reconstructed and must be entered by hand.
External dependencies, not development time. A layer assigned to each of the 19
projects and the third-party list confirmed by the end of day 1; agreement on who writes
the breaking-change prose at finalize.
11. Lens exemptions
-
Protocol — no device-facing parameter paths are touched.
-
Performance — generation runs once per release on an admin path; the published artifact is a static file.
-
Migration and back-compat — a new data file and two additive fields; no schema change and no existing reader changes.