Software Development Life Cycle (SDLC)
- Project: Minimum Viable Health Dataspace v2 (EHDS reference implementation)
- Audience: contributors, reviewers, and anyone who wants to know how a one-person, AI-assisted project keeps a regulated-domain codebase shippable.
- Last updated: 2026-11-03
- Maintainer: Matthias (
@ma3u)
How a change gets from an idea to the live hub: issues and discussions, branches and pull requests, the hooks on your machine, the checks on GitHub, the deploys, the releases, and how to contribute from outside. The plain-language version is How We Build This Software; how the AI author fits in is Deterministic Agentic AI Development with Claude Code. Two companion pages go deeper: Integration tests through the APIs and The stack and the development environment. Section 12 is the recipe behind all of it: how to define an SDLC for a DevSecOps and AI project, stage by stage, with the tools.
Acronyms used in this document
| Acronym | Expansion |
|---|---|
| CI/CD | Continuous Integration / Continuous Delivery: the automated build, test and deploy |
| PR | Pull Request: a proposed change, checked and reviewed before it is merged |
| ADR | Architecture Decision Record: a short note of a decision and its reasons |
| OIDC | OpenID Connect; here the federated login of GitHub Actions to Azure |
| SBOM | Software Bill of Materials: the list of every third-party component |
| CVE | Common Vulnerabilities and Exposures: the public catalogue of known flaws |
| SARIF | Static Analysis Results Interchange Format: how scanners report to GitHub |
| DSP / DCP / TCK | Dataspace Protocol / Decentralised Claims Protocol / Technology Compatibility Kit |
| ACA | Azure Container Apps, where the live hub runs |
1. Philosophy: deterministic tooling around a non-deterministic author
The author proposes; deterministic tooling disposes.
A senior engineer, a newcomer and an AI agent are all non-deterministic: ask the same question twice and you may get two answers. The output becomes trustworthy when the author is wrapped in gates that run the same way every time, for everyone: formatters, linters, type checks, tests, security scanners and a CI/CD pipeline.
The project is a team of one plus an AI pair programmer. So it keeps every deterministic gate a large team would have and collapses the coordination ceremonies (mandatory human review, sprint rituals, release boards) that only pay off with more people. Section 11 says what is collapsed and why.
This matters more here than in most projects. It is a reference implementation of the European Health Data Space, a regulated domain with health data, patient rights and audit duties. The gates buy trust (whole classes of mistakes are caught before they ship), auditability (every change is logged with author, time and reason), repeatability, cheap mistakes (a local hook costs seconds, a production incident hours) and safe AI assistance.
2. The lifecycle at a glance
- Idea. An open question or a design to explore starts as a Discussion; a piece of work starts as an Issue (section 3).
- ADR when the decision is expensive to reverse or cross-cutting.
- Branch off
main, short-lived. - Change, by a human, Claude Code or both.
- Hooks on your machine: 31 at commit, 3 at push (section 5).
- Pull request. The PR Gate is the one required check; the other workflows are advisory (section 6).
- Squash merge once the PR Gate is green and every review thread is resolved.
- On
main: the live hub and the static demo redeploy, the smoke and compliance runs start, and a release is cut when the version changed (sections 7 and 8).
3. Discussions, issues and the planning folder
Discussion or issue?
Discussions are enabled, with the categories Announcements, General, Ideas, Polls, Q&A and Show and tell.
| Open a Discussion when | Open an Issue when |
|---|---|
| you have a question ("how does the hub map X?"), want to compare options, or propose a direction that is not yet a task (Ideas; recent examples are #776 on FHIR and OMOP tooling and #701 on Simpl-Open) | there is something to do: a bug, a feature with a clear outcome, a gap with an acceptance criterion |
| the answer may be "no" or "not now" | someone will close it with a commit or a PR |
| several people should weigh in before anything is decided | the decision is made, or an ADR will make it |
A discussion that reaches a decision becomes an issue (and an ADR when the decision is architectural), linked both ways.
What an issue carries
Use the templates in .github/ISSUE_TEMPLATE/
(bug report, feature request). Every issue that is being worked on carries:
- The open tasks as a checklist, kept current. When a PR leaves work undone, the issue says what is left; nothing open lives only in a chat or a commit message.
- Manual test steps with links (to the static docs, a page of the hub, a workflow run), so anyone can check the result without asking.
- The decisions, each linked to its ADR.
Labels in use: bug, enhancement, documentation, question, security,
infrastructure, azure, dependencies, the priorities P1 to P3, and the
area labels EHDS-compliance, dataspace-discovery, simpl, NLP,
Agentic AI, production, load test, stress test, pentest, trivy,
Cost. New contributors look for good first issue and help wanted.
The planning folder
docs/planning/index.md lists work items as files that
move future/ → current/ → done/ and are never deleted. A file in current/
is the long form of an issue (its phases, measurements, links); the issue
stays the place for the checklist. Roadmap detail lives in
docs/planning/roadmap-phases-*.md, and the issue and ADR tables in
docs/planning-health-dataspace-v2.md.
/plan in Claude Code grooms the folder.
Keep any document that is loaded routinely under about 15K tokens (ADR-026): the index stays small and the detail moves to archives.
ADRs
docs/ADRs/ holds the ADRs from ADR-001 on (051 is reserved). New ones follow the
Nygard template in docs/adr/0000-template.md; an accepted ADR is never
edited, only superseded. Write one when a decision is expensive to reverse or
cross-cutting: the database split (ADR-001),
the testing strategy (ADR-008), checks
that must be able to fail (ADR-031),
every API route needing a session (ADR-044).
4. Branches, commits and pull requests
- Trunk-based.
mainis always deployable. Work happens on short-lived branches namedtype/short-slug(feat/…,fix/…,docs/…,chore/…,ci/…), often with the issue number:fix/708-ui-sticky-sessions. - Conventional Commits with a
scope:
feat(spe): workspaces start on demand (ADR-072). All of the last 40 commits onmainfollow it; no hook enforces it yet. - Commits carry the author's name. No AI co-author trailer is added; how the AI works in this project is documented in the companion document.
- A PR links its issue and its ADRs and says how it was tested; the
PR template
asks for both.
CODEOWNERSnames the maintainer, so a PR from anyone else gets a review request. - Squash merge. The PR title becomes the commit on
main, so it follows the commit convention too.
4.1 Branch protection on main
A repository ruleset on the default branch:
| Rule | Effect |
|---|---|
| Require a pull request | No direct pushes to main. Zero approvals required (see below). |
| Require conversation resolution | Every review thread is resolved before merge, so review findings are answered rather than scrolled past. |
| Require status checks | PR Gate must be green. It is the only required check. |
| Require linear history | Squash or rebase only; merge commits are switched off. |
| Block force pushes, deletions | main cannot be rewritten or deleted. |
Zero required approvals is deliberate. With one maintainer, a required approval means a second account and a rubber stamp. Review is done by the AI reviewers and the gates; the mechanical protections (no direct push, no force push, no merge on red) are the part a solo maintainer cannot supply by discipline, so they are enforced. When a second regular contributor joins, this goes to one approval.
Admins can bypass only on a pull request. A stuck check can be overridden
on a PR, which is better than being unable to land a hotfix on a live demo; a
direct push to main is refused for everyone.
4.2 Why exactly one required check
Every other workflow is path-filtered, which is right for cost and wrong for
branch protection: a required check that never starts stays pending, and the
PR can never merge (#303 collected 28 checks, #305 none). So
pr-gate.yml
has no path filter and is the one required check. Section 6.1 lists what it
runs.
5. The hooks on your machine
.pre-commit-config.yaml,
run by the pre-commit framework. Install both
stages once per clone:
pip install pre-commit
pre-commit install --hook-type pre-commit --hook-type pre-push
pre-commit run --all-files # optional: everything, once
Most hooks are scoped by file pattern, so a docs-only commit skips the
TypeScript ones. Hooks that need a tool you have not installed (gitleaks,
Semgrep, lychee, actionlint) print a warning and pass; the PR Gate installs
gitleaks and actionlint and runs them anyway. Skip one hook once with
SKIP=<id> git commit, never the whole set with --no-verify: the PR Gate
runs the same hooks on the PR's diff, so a bypass only moves the failure.
Hooks that rewrite files (Prettier, the whitespace fixers, docs-facts)
fail the commit after changing the file. Stage the file again and commit
again, then check that HEAD moved: a failed commit leaves it where it was.
5.1 At commit: 31 hooks
| Hook (id) | What it does | When it fails |
|---|---|---|
trailing-whitespace, end-of-file-fixer |
strip trailing spaces, end every file with a newline (patches and the vendored HealthDCAT-AP shapes excepted) | it fixed the file: stage it again |
check-yaml, check-json |
the files parse (multi-document YAML allowed) | fix the syntax it names |
check-added-large-files |
refuses a file over 5,000 KB | keep it out of git, or make it smaller |
check-merge-conflict |
refuses leftover <<<<<<< markers |
finish the merge |
detect-private-key |
refuses a private key | remove it; if it was real, rotate it |
check-case-conflict, check-symlinks |
names that clash on a case-insensitive disk, symlinks that point nowhere | rename, or fix the link |
check-executables-have-shebangs, check-shebang-scripts-are-executable |
an executable file has a #! line, a #! file is executable |
chmod +x (or -x), and stage that path explicitly: a stray executable bit on a .gitkeep fails here |
shellcheck |
Bash at error severity | shellcheck --severity=error <file> and fix what it reports |
hadolint-docker |
Dockerfile best practice | hadolint <Dockerfile> |
prettier |
formats JS, TS, JSON, YAML and Markdown | it rewrote the file: stage it again. Do not fight its output |
tsc-ui |
tsc --noEmit -p tsconfig.build.json in ui/ (strict, tests excluded) |
run that command in ui/, fix the type error |
eslint-ui |
next lint --max-warnings 55 in ui/ |
an error, or a new warning above the budget of 55: cd ui && npx next lint |
gitleaks |
secrets in the staged diff | remove the secret and rotate it; a proven false positive goes in .gitleaksignore |
semgrep |
Semgrep CE on staged code: the registry's TypeScript, Next.js, React and secrets rules, and .semgrep/cypher.yml (request input reaches Cypher as a parameter) |
fix it, or // nosemgrep: <rule-id> -- <reason> above the line. Behind a TLS-inspecting proxy the registry download fails and the hook exits 2: that is not a finding |
semgrep-rule-tests |
the project's own Semgrep rules against .semgrep/cypher.ts |
the rule and its test disagree |
check-broken-links-md |
relative links in Markdown resolve (lychee offline, or markdown-link-check); translations under docs/i18n/ excepted |
fix the path |
check-no-screenshot-imports |
docs pages under ui/src/app/docs/ do not reference images/screenshots |
use a diagram or a guide image instead |
api-spec-drift |
every route under ui/src/app/api/ is in ui/public/openapi.yaml, and the other way round |
document the operation in the spec and delete its line from docs/api-spec-drift-allowlist.txt; adding a line is a decision to ship an undocumented route |
api-spec-shapes |
the spec describes the shape the hub answers with (checked against the fixtures in ui/public/mock/) |
correct the schema, or the fixture |
docs-facts |
regenerates ui/src/app/docs/docs-facts.json (routes, hooks, services, ADRs, each docs page's last-updated date) |
it rewrote the file: stage it and commit again |
docs-content |
the published copies of docs/ match their sources (gen-docs-content.py --check) |
python3 scripts/gen-docs-content.py, stage ui/src/content/ and ui/public/published-docs/ |
localized-images |
the manifest of translated screenshots and diagrams matches ui/public |
python3 scripts/gen-localized-images.py, stage the manifest |
bruno-coverage |
every API route has a request in bruno/MVHDv2/, every request can fail, no credential in a .bru file |
add the request in the persona's folder; delete the one whose route is gone (see the API tests) |
bruno-strength |
a 2xx request asserts more than a container type | assert a property's value |
workflow-paths |
a workflow's paths filter covers every script it runs |
add the path to the workflow's filter |
actionlint |
GitHub Actions workflows | fix the workflow it names |
image-scan-coverage |
the CVE scan in security-scan.yml covers every image in the compose files |
add the image to the scan matrix |
5.2 At push: 3 hooks
| Hook (id) | What it does | When it fails |
|---|---|---|
vitest-ui |
the whole UI suite, vitest run --bail 1, when the push changes ui/src or ui/__tests__ |
cd ui && npx vitest run <file> for the failing file. A hook that resets a mock uses braces: beforeEach(() => { m.mockReset(); }) |
npm-audit-ui |
scripts/check-npm-audit.py ui --omit=dev: no HIGH or CRITICAL advisory in production dependencies |
update the dependency; when no fix exists, a time-boxed entry in docs/npm-audit-exceptions.json with a reason, an issue and an expiry date |
codeql-swift |
CodeQL on the iPhone app when Swift sources, Package.swift or project.yml change (about 15 minutes, macOS) |
fix the finding, or accept it with its reason in clients/ios/Scripts/codeql-accepted.txt; SKIP=codeql-swift git push skips it once |
Quality Gates and the quality gates page have the coverage figures and the gates still to come.
6. The checks on GitHub
.github/workflows/ holds 37 workflows. Only the PR Gate blocks a merge.
6.1 PR Gate: the required check
On every pull request and merge-queue run, no path filter, about 15 minutes at most:
- the pre-commit hooks on the PR's diff (
pre-commit run --from-ref origin/main --to-ref HEAD), which catches a commit made with--no-verifyor from a clone without the hooks; - API spec drift (
scripts/check-api-spec-drift.py); - docs facts current, with every changed docs page's date moved
(
scripts/gen-docs-facts.py --check); - the API collection covers every route (
scripts/check-bruno-coverage.py); - HealthDCAT-AP Release 8 conformance of the catalogue's golden files (pySHACL against the official shapes);
- no unused files, exports or dependencies (
npm run --prefix ui knip); - gitleaks over the PR's commits, since the staged-only local hook sees nothing in CI.
6.2 The other workflows
| Workflow | Runs on | What it does | Blocks |
|---|---|---|---|
test.yml Test Suite |
push to any branch, PR to main (paths: ui/, the services, clients/ios/, the Klarbefund docs) |
8 jobs: Vitest for the UI, proxy, SPE loader and ePA ingest; OPA policy tests; iOS analyte parity (only when clients/ios/ changed, decided by a Changed paths job); one job of scans (Trivy, Kubescape, npm audit, SBOM, licences). Lint and gitleaks run in PR Gate |
no (advisory) |
codeql.yml |
push and PR to main, weekly |
CodeQL for JavaScript/TypeScript, Python and the workflows; results in the Security tab | no |
codeql-swift.yml |
weekly, dispatch, and PRs that change the workflow or its script | CodeQL for the iPhone app; app changes are checked by the codeql-swift pre-push hook |
no |
security-scan.yml |
PR, weekly, dispatch | source SBOM and CVEs, image CVEs, the deployed images' CVEs, a severity gate | no |
bruno-smoke.yml |
PR and main when the collection or ui/src/app/api/ change |
the API collection against the static fixtures (Static-mock) | no |
python-services-test.yml |
PR, push to main |
pytest for the Python services | no |
compliance.yml |
called by nightly.yml, dispatch |
DSP, DCP and EHDS domain checks and the full API collection against a fresh JAD stack; the report job fails below the recorded floor | no (nightly) |
nightly.yml |
once a night when main changed, dispatch (ADR-074) |
Playwright E2E with WCAG and the OWASP/BSI checks against a seeded Neo4j (reports go to the Pages site), Lighthouse, gitleaks over the whole history, compliance.yml; a red night opens an issue, a green one closes it |
no |
deploy-azure.yml |
every push to main, dispatch |
builds the UI and proxy images, deploys them to Azure Container Apps, checks that sign-in works, then runs Playwright against the live hub | main only |
pages.yml |
every push to main, dispatch |
builds the static export with the latest nightly test reports and publishes it to GitHub Pages (6.4) | main only |
release.yml |
push to main changing ui/package.json, dispatch |
tags and publishes a GitHub Release (section 8) | main only |
demo-smoke.yml |
daily 06:00 UTC (starts hours late), dispatch | is the live hub up, the API collection against it (Azure-Dev), Playwright on public routes, a ZAP baseline, then removes what the collection created | main only |
catalog-crawler.yml, catalog-enricher.yml |
push to main changing the service |
tests and deploys the catalogue services | main only |
aca-schedule.yml |
twice a day, dispatch | fallback for the off-hours stop and start (ADR-053, ADR-058) | n/a |
The remaining 22 are operations workflows run by hand (workflow_dispatch)
against Azure: seeding Neo4j, EDC participants, data assets and CFM; the
Vault bootstrap; the FHIR loader; GraphRAG; the EUDI verifier; the Keycloak
domain; cost guards; one-off repairs and retirements, each named after its
issue or ADR. They follow the same rule as everything else: an environment
change is a versioned, re-runnable workflow, not a click in the portal. Those
that delete or rewrite have a plan mode that only reads.
6.3 Protocol compliance
compliance.yml brings up the JAD stack inside the job, seeds it, and runs
the DSP, DCP and EHDS domain checks and the API collection. Every suite step
may fail without stopping the job; the Compliance Report job then compares
each result with its floor in scripts/compliance-baseline.json and fails on a
regression. It gates on a recorded baseline, not on green, so a suite that
does not pass yet still cannot get worse. The suites are ours and drive our
APIs; they are not the Eclipse TCKs and do not prove conformance (#338).
Current numbers and how to run them are in
Integration tests through the APIs.
6.4 GitHub Pages: the static demo
pages.yml builds the public demo at
ma3u.github.io/MinimumViableHealthDataspacev2:
it seeds a Neo4j container, refreshes the mock fixtures from the live API
shape, renames ui/src/app/api/ out of the way, builds with
NEXT_PUBLIC_STATIC_EXPORT=true, adds the test reports of the last
nightly.yml run and publishes. The nightly run seeds its own Neo4j and runs
Vitest, Playwright, the WCAG audit and the security checks (report only,
continue-on-error). Without API routes every
page reads ui/public/mock/*.json, so a page that GETs an /api/... path
needs a fixture there, and static-mock-coverage.test.ts fails without one.
6.5 Deploy to Azure
Every push to main runs deploy-azure.yml:
- log in to Azure with OIDC (no stored password: a federated credential
trusts this repository's
mainbranch); - build the UI and the Neo4j proxy images, push them to the registry
acrmvhdehds; - wait while a load test runs (the
LOAD_TEST_RUNNINGvariable), so a merge does not redeploy under k6; - deploy both to Container Apps in
rg-mvhd-devas a new revision (ACA caches:latest, so a restart alone would not pick up the image); - verify the revisions, wait for the UI, and prove sign-in works end to end;
- if the hub is live (not stopped off hours), run Playwright, the WCAG audit and the security checks against it.
OIDC works on main only. The federated credential names
refs/heads/main, so an Azure step on a pull request, a fork or another
branch cannot log in. A job that needs Azure is gated to main, the schedule
or a dispatch on main; letting PRs in means adding a federated credential
in Azure, not editing the workflow. Off hours (Mon to Fri 07:00 to 20:00
Europe/Berlin is on) everything is stopped
(ADR-053), so a deploy then
updates the revision without being able to test it live.
7. Testing strategy
ADR-008 sets the tiers; the conventions
are in .claude/rules/testing.md.
| Tier | Framework | Scope | When it runs |
|---|---|---|---|
| Unit | Vitest | functions, components and API route handlers; Neo4j mocked at @/lib/neo4j, fetch stubbed per test |
pre-push, every push (test.yml) |
| Service | Vitest, pytest, opa test |
the Neo4j proxy, SPE loader, ePA ingest, the Python services, the Trustee policies | every push touching them |
| iOS | Swift Testing, XCUITest | the app's parser and model; its screens with a fixed demo seed | on the Mac; parity check in CI |
| API integration | Bruno, shell suites | the hub's routes per persona; DSP, DCP, the health domain | PR (fixtures), main, weekly, live daily |
| E2E | Playwright | numbered journeys in ui/__tests__/e2e/journeys/ |
main, dispatch, after each Azure deploy |
Route tests run with the auth guard mocked open, so they test business logic,
not the gate; a test that means to pin a gate closes it itself, and the
09 Access control folder of the API collection checks the gates against a
real server. Coverage is collected and published but no threshold is enforced
yet.
8. Releases and versions
What exists today, plainly:
- The hub has one version, the
versioninui/package.json, now2.0.0. A person decides the bump (semantic versioning, by judgement) and changes it in a PR. - On merge,
release.ymlcuts the release: it tagsvX.Y.Zif the tag is missing, writes notes fromgit logsince the previous tag, and publishes a GitHub Release. It does nothing when the release exists, and a dispatch withforce_versionre-runs it. - Release notes for a major version are rewritten by hand. v2.0.0's notes are an essay, not the commit list the workflow writes.
- There is no cadence. Eleven tags so far, v1.0.0 (2026-03-30) to v2.0.0
(2026-09-29);
mainhas moved more than 350 commits since v2.0.0 without a release.mainis what runs live, so a release marks a milestone rather than a deployment. v1.4.3 is a tag without a release. - The Neo4j proxy and the other services are not versioned separately
(
services/neo4j-proxy/package.jsonsays0.1.0); their images are tagged by commit.
The iPhone app: TestFlight builds
Klarbefund is versioned separately, in
clients/ios/project.yml:
MARKETING_VERSION (now 1.0) and the build number
CURRENT_PROJECT_VERSION (now 11). Each TestFlight upload is its own PR,
chore(ios): build N for TestFlight, with its What to Test, which raises the
build number and writes the tester notes in English and German in
clients/ios/appstore/texts.py. The upload is
clients/ios/Scripts/archive-and-upload.sh, run on the Mac; App Store Connect
refuses a build number it has seen. Testers join by the public TestFlight
link (ADR-050).
The app has no App Store release yet.
9. Contributing from outside
The repository is public under the
Apache License 2.0.
The short version is in
.github/CONTRIBUTING.md,
which GitHub links from the issue and PR forms.
- Start with a Discussion or an issue (section 3), so the change is agreed before the work. Small fixes to docs and typos can go straight to a PR.
- Fork, branch from
main, install the hooks (section 5) and set up the stack you need (The stack and the development environment). - Keep the PR to one change. Follow the conventions in
.claude/rules/: code style, testing and API conventions. A new API route needsrequireAuth(), a request in the API collection, a mock fixture and an entry inopenapi.yaml; the hooks tell you which is missing. - Fictional organisations only. Demo data, docs, tests and screenshots
name AlphaKlinik Berlin, PharmaCo Research AG, MedReg DE, Limburg Medical
Centre and Institut de Recherche Santé, never a real hospital, company,
insurer or authority. Real names appear only behind the
NEXT_PUBLIC_DEMO_TKbuild flag, and real third-party screenshots are never committed. No real patient data, ever: the patients are synthetic (Synthea). - No secrets. gitleaks runs locally and in the PR Gate; the local
defaults (
neo4j/healthdataspace) are for development only. - Open the PR. The PR Gate must be green; Azure steps do not run for a fork (6.5), and the maintainer runs what needs the live hub. Answer every review thread; the maintainer squash-merges.
You do not need to write code to contribute. Sharper issues, ADR proposals and reviews of the user-facing docs and journeys improve every later change, because they are the specifications the AI author reads.
10. Environments
| Environment | How | Purpose |
|---|---|---|
| Local, minimal | Neo4j in Docker and npm run dev |
day-to-day UI and graph work |
| Local, full JAD | docker-compose.yml + docker-compose.jad.yml, seed phases 1 to 7 |
every service, sign-in, the protocol flows |
| CI | compliance.yml brings the JAD stack up inside the job |
the compliance suites |
| GitHub Pages | pages.yml, static export with fixtures |
the public demo, no backend |
| Azure Container Apps | deploy-azure.yml and the operations workflows, rg-mvhd-dev, https://ehds.mabu.red |
the live hub, on Mon to Fri 07:00 to 20:00 Europe/Berlin |
11. The single-maintainer adaptation
| A team of 10 to 30 has… | This project does instead | Why it is acceptable for now |
|---|---|---|
| mandatory peer review | AI review (/review, /code-review, specialist agents) and the gates; self-merge |
the gates catch what a second reviewer would; review threads must be resolved |
| merge blocked on red CI | enforced: PR required, PR Gate green, linear history, no force push | admin bypass is limited to pull requests |
| a release manager | release.yml and a human decision on the version |
automation replaces the role, not the judgement |
| sprint ceremonies | issues with open tasks, ADRs, the planning folder | lightweight, asynchronous, written down |
Next steps
- Give
test.ymla final job that always runs and require it, so the unit suites become non-bypassable like the PR Gate. - Enforce Conventional Commits with a commit-message hook.
- Enforce coverage thresholds on the critical paths, then ratchet.
- Drive the report-only scanners (WCAG contrast, the pentest suite) to green
and drop
continue-on-error. - Automate the version bump and the changelog from the commit history, and decide a release cadence.
- Adopt the Eclipse DSP and DCP TCKs and make the compliance floors a PR signal; add provenance (SLSA) to releases.
12. How to define an SDLC for a DevSecOps and AI project
The sections above describe this project. This one is the recipe behind it, for a team starting its own: what to decide at each stage, which tool does it, and how this repository does it. One idea carries it:
Specs are the input, deterministic gates are the control, evidence is the output. An AI author is treated like any other contributor whose work you cannot fully predict: it works from a written spec, inside the same gates, and a person owns the merge.
12.1 The stages and their tools
| Stage | Decide | Tools | Here |
|---|---|---|---|
| Explore | where open questions and options are discussed before anything is a task | GitHub Discussions | Discussions, category Ideas (section 3) |
| Specify | the spec format: the problem, requirements, acceptance criteria, then a plan and tasks; where it lives in the repository | GitHub Spec Kit (introduction), Kiro specs, AGENTS.md, Claude Code memory (CLAUDE.md) | the issue's acceptance criteria and open tasks, a file in docs/planning/current/, CLAUDE.md and .claude/rules/ |
| Decide | which decisions need a record, the template, who accepts, that an accepted record is superseded and never edited | Nygard's ADR, adr.github.io, MADR, adr-tools, Log4brains | docs/ADRs/, the Nygard template in docs/adr/0000-template.md |
| Plan and track | issue types and templates, labels and priorities, where open tasks and test steps are written | Issues, issue templates, Projects | issues with open tasks and manual test steps, P1 to P3, the planning folder |
| Build with AI | what the agent reads, what it may run, which checks run on every edit, how commits are named | Claude Code hooks, subagents, pre-commit, Conventional Commits, commitlint | CLAUDE.md, .claude/ hooks and agents, 31 + 3 hooks (section 5) |
| Secure | secrets, static analysis, dependencies, images, SBOM; the severity that blocks | gitleaks, secret scanning, Semgrep, CodeQL, Dependabot, Renovate, Trivy, CycloneDX | gitleaks, Semgrep and npm audit locally; CodeQL, Trivy, SBOM and Dependabot on GitHub (sections 5 and 6) |
| Test | the tiers and where each runs; that every check can fail | Vitest, Playwright, Bruno | section 7, ADR-031 |
| Review and merge | required checks, approvals, code owners, merge method | pull requests, rulesets, CODEOWNERS, merge queue, Claude Code GitHub Actions | one required check, review threads resolved, squash merge (section 4) |
| Deliver | how CI reaches the cloud without stored passwords; how builds and releases are proven | GitHub Actions, OIDC, artifact attestations, SLSA, release-please, semantic-release | OIDC on main, release.yml; attestations and SLSA not yet (sections 6.5 and 8) |
| Measure the process | the framework you hold the SDLC against, and how often | NIST SSDF (SP 800-218), OWASP SAMM, OpenSSF Scorecard | the quality gates and the security reports |
| Govern the AI | which AI the product contains and which AI writes the code; the risks of each | EU AI Act (Regulation (EU) 2024/1689), NIST AI RMF, ISO/IEC 42001, OWASP Top 10 for LLM applications | the natural-language query refuses to read a patient's identity unless the UI allows it (api-conventions.md); the AI author is covered by the companion document |
12.2 Spec-driven development in practice
A spec is written before the code and lives in the repository, so a person and an agent read the same text. Spec Kit names the steps: specify (what and why, with acceptance criteria), plan (how, within the project's constraints), tasks (small, checkable steps), implement. Whatever the tool, the spec is good when:
- every acceptance criterion can be checked by a test or a manual step with a link;
- it names what is out of scope;
- it points to the ADRs it relies on and says when a new one is needed;
- the agent's standing instructions (
CLAUDE.md,AGENTS.md, rules files) hold what is true for every change, so the spec holds only this one.
This repository does not use Spec Kit. The same steps live in the issue (what,
why, acceptance criteria, open tasks), the ADR (the decision), the planning
file (the plan) and CLAUDE.md with .claude/rules/ (the constraints).
12.3 Where each conversation belongs
| It is… | Put it in | It ends as |
|---|---|---|
| a question, an idea, a comparison | a Discussion | an answer, or an issue |
| a decision that is hard to reverse | an ADR, proposed in a PR | accepted, rejected or superseded |
| something to do | an issue with acceptance criteria and tasks | closed by a PR, with test steps |
| the change itself | a pull request linking its issue and ADRs | a squash merge on green |
| what happened after the merge | the workflow run, the release, the issue | evidence anyone can open |
12.4 Where to start
In this order, each a day's work or less:
- A ruleset on
main: pull request required, one required check with no path filter, no force push. - pre-commit with formatting, type checks and a secret scan, and the same hooks re-run in that required check.
- Discussions on, issue templates, labels for type and priority.
- An ADR template and a first ADR: how you decide.
- Standing instructions for the AI author (
CLAUDE.mdorAGENTS.md) and the rules it must follow. - CodeQL, Dependabot or Renovate, and an SBOM.
- A spec template for features: acceptance criteria and out-of-scope.
- A release workflow, then provenance (attestations, SLSA) for what you ship.
References
CLAUDE.md: commands, layout, the top gotchas..pre-commit-config.yamland.github/workflows/..claude/rules/: code style, testing, API conventions.- Integration tests through the APIs · The stack and the development environment · Quality Gates · Gotchas
- ADR-008, testing strategy · ADR-026, token-efficient planning · ADR-031, checks must assert · ADR-032, the persona-organised API collection
- Planning index ·
docs/planning-health-dataspace-v2.md