Same build · run as CI

The right arm of the V, turned into a build

The verification work was done correctly and by hand, by one person, once. This page is that verification work expressed as continuous integration. Three jobs run today on a Jenkins controller defined entirely as code. The rest of the pipeline described below is designed and not yet built, and is marked that way everywhere it appears.

Jenkins + JCasC Docker cppcheck Requirement traceability
Running today 3 jobs, green Traceability 11 of 17 requirements Original build unchanged

01 — Premise

The process was right. The loop was too slow.

Nothing on the previous page is wrong engineering. Requirements were written, traced, and verified; the architecture was decomposed before implementation; three test stages ran in the correct order. The failure mode was throughput — 59 tethered trials to build the controller and three untethered runs to validate it, because every iteration was a person changing a constant, rebuilding, reflashing, and walking a track.

A team hitting the same wall does not fix it by working harder. It fixes it by making the verification arm of the V run without a human in it, so the human budget goes to the trials that need judgement.

Honest scope

This is additive work on top of a finished project, not a rewrite and not a claim about what was done originally. Automated results are labelled as automated and kept separate from the original trial data. The commit history reflects when each part was actually built — and the parts that are not built yet say so.

02 — Built

What actually runs

A Jenkins controller, in Docker, configured entirely through JCasC — no setup wizard, no clicked configuration. Deleting the volume and running docker compose up reproduces the same server and the same job. Three stages run in parallel on every build.

concept-car-verification · build #1 SUCCESS
[Verify] parallel branches: Static analysis, Traceability gate, Artefact integrity

  cppcheck  6/6 files checked 100% done
            4 findings, published through warnings-ng

  requirements:  17
  traced:        11
  untraced:      6  HR-F-02 HR-F-03 HR-F-05 HR-F-06 HR-F-07 SR-F-07
  broken links:  0
  coverage:      64.7%  (baseline 64.7%)
  PASS

  artefacts   5 models, entry point present in each generated directory

The traceability gate

This is the stage worth reading the source of. It pulls the seventeen requirement IDs directly out of docs/requirements-specification.xlsx — the specification itself is the input, not a transcription of it that can drift — resolves each against a committed map, and separates two different failures:

Six requirements are untraced deliberately. HR-F-02, HR-F-03, HR-F-05, HR-F-06 and HR-F-07 are satisfied by physical hardware, and SR-F-07 by material in the writeup — none of which is a file in this repository. Mapping them to something merely to raise the percentage would defeat the purpose of the gate.

Why there is no compile job

The generated C in firmware/generated includes eleven MathWorks and Arduino headers — MW_ArduinoHWInit.h, arduinoARM_M0plusScheduler.h, xcp.h, ext_mode.h, tmwtypes.h among them — that are vendor files and are deliberately not in this repository. Nothing here can link.

A pipeline reporting a green firmware build under those conditions would be measuring nothing at all. The three stages above were chosen because they verify things that are actually present. A real compile belongs on a labelled agent carrying gcc-arm-none-eabi, once the toolchain is available to put there.

03 — Mapping

Every right-arm box becomes a job

I annotated its V-model with a tool per stage — Excel, Visio, PlantUML, Simulink, Data Inspector. The retrofit keeps the same V and replaces "a person opens the tool" with "a job runs and reports".

V-model stage

REQRequirements engineering
ARCHSoftware architecture & UML
IMPLImplementation — model
IMPLImplementation — generated C
MILModel-in-the-Loop test
PILProcessor-in-the-Loop test
ROBRun-on-Board evaluation
VERVerification table

CI job

traceRequirement IDs read from the spec must resolve to artefacts that exist — a broken link fails the buildbuilt
umlPlantUML sources render in CI; diagrams published as artifacts, never hand-exporteddesigned
milHeadless MATLAB batch run; path error asserted against the ±1% tolerance of SR-F-04designed
codegenGenerated C diffed against a committed baseline — a silent model change shows up as a code diffdesigned
staticcppcheck over the generated sources, findings published and trended per buildbuilt
artefactsEvery generated directory carries an entry point; provenance record presentbuilt
compilearduino-cli build on a pinned toolchain, flash-size and RAM report per builddesigned
hilSelf-hosted runner with a board physically attached; flash, run, capture encoder telemetrydesigned
verifyVerification table generated from job results — always current, never a stale documentdesigned
built runs on every build today designed specified here, not yet implemented

Pipeline shape

Fast, hardware-free jobs run on every push and gate the merge. Anything that needs a physical board — or a long parameter sweep — runs behind that gate, on hardware nobody has to babysit.

The diagram below is the target shape, not the current one. Of the jobs drawn, trace and static exist and run; the remaining seven are specified and unbuilt. Nothing below the merge gate exists at all, because none of the hardware is wired up.

ON EVERY PUSH — NO HARDWARE — GATES THE MERGE tracerequirement IDs staticcppcheck · clang-tidy milheadless MATLAB codegendiff vs baseline compilearduino-cli · size AFTER MERGE — SELF-HOSTED RUNNER WITH A BOARD ATTACHED hilflash · run · capture sweepPID matrix scoredeviation metric verifytable + release HIGHLIGHTED JOBS NEED PHYSICAL HARDWARE — THE REASON JENKINS IS IN THE STACK

04 — Definitions

One build, three orchestrators

The same pipeline is expressed three ways on purpose. Automotive suppliers run GitLab and Jenkins far more than they run GitHub Actions, and the interesting engineering is in where the three disagree — hosted runners cannot hold a development board, GitLab and Jenkins disagree about where configuration lives, and only one of them treats "config as code" as the default rather than a plugin.

Which of these are real files

The Jenkinsfile and the Jenkins as code definitions are in the repository and are what build #1 above actually ran. The GitHub Actions and GitLab CI tabs are illustrative — they show how the same stages would be expressed on those two systems, and neither file exists yet. Both also reference jobs from the target pipeline rather than the three that are implemented.

Public-facing build. Fast jobs only — GitHub-hosted runners have no board attached, so the hardware stages are deliberately absent and delegated to Jenkins.

.github/workflows/ci.ymlYAML
name: ci
on: [push, pull_request]

jobs:
  trace:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Requirement IDs referenced must exist
        run: python tools/trace_check.py docs/requirements.csv models/ tests/

  static:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt-get install -y cppcheck clang-tidy
      - run: cppcheck --error-exitcode=1 --enable=warning,portability firmware/
      - run: tools/tidy.sh firmware/

  codegen-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Generated C must match the committed baseline
        run: git diff --exit-code -- firmware/generated/

  compile:
    needs: [static, codegen-diff]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: arduino/setup-arduino-cli@v2
      - run: arduino-cli core install arduino:samd@1.8.14
      - run: |
          arduino-cli compile \
            --fqbn arduino:samd:nano_33_iot \
            --output-dir build/ firmware/
      - uses: actions/upload-artifact@v4
        with:
          name: firmware-${{ github.sha }}
          path: build/*.bin

The toolchain version is pinned. That is the hidden requirement the project discovered when a MATLAB update silently broke the Arduino library link — expressed here as a line of config that fails loudly instead of a week of debugging.

05 — Scale

What changes when it is a team, not a person

A solo V-model and a team V-model fail differently. Alone, the risk is throughput. With five engineers, the risk is that two of them change the same model and nobody can tell.

Simulink models are binaries, and that is the hard problem

A .slx is a zip container. Git will happily store it, show binary files differ, and offer no merge. On a solo project this never surfaces. On a team it is the single largest source of lost work in model-based development.

.gitattributesGit
# Simulink and Stateflow artifacts are binary — no textual merge exists
*.slx   binary lockable
*.mdl   binary lockable
*.mlx   binary lockable
*.sldd  binary lockable

# Generated C is the reviewable surface of a model change
firmware/generated/** text eol=lf

Hardware is a shared, finite resource

One board, many engineers. The HIL agent is a single-executor Jenkins node with disableConcurrentBuilds(), so hardware jobs queue rather than collide. Scaling means adding labelled agents — hil-nano33, later hil-samd51 — and letting the label selector route work, not rewriting the pipeline.

Traceability stops being paperwork

In the project the verification table was written once, by hand, at the end. At team scale that document is stale the day after it is written. The retrofit inverts it: SR- IDs appear in commit trailers, model annotations, and test names, the trace job fails the build when an ID does not exist, and the table is regenerated from job results on every run.

commit message conventionGit
feat(control): clamp integral term in distance PID

Windup on the straight section pushed the correction past the
motor duty limit, which showed up as overshoot at the first corner.

Verifies: SR-F-02, SR-F-04
Trial-data: reports/sweep/2026-09-05T14-22Z/

The merge gate, written down

GateEnforced byBlocks merge
Referenced requirement IDs existtrace jobYes
No new cppcheck or clang-tidy warningsstatic jobYes
MiL path error within ±1%mil jobYes
Generated C matches committed baselinecodegen-diff jobYes
Firmware compiles and fitscompile jobYes
Model file lock held by the author.gitattributes + LFS locksYes
HIL deviation not worse than previous mainscore jobWarn — reviewer decides
One human approval on the MRBranch protectionYes

06 — Result

What the retrofit is actually worth

Original — manualPipeline — targetStatus
Requirement traceabilityA table, written once, correct on the day it was writtenRead from the spec on every build; a broken link fails itbuilt
Static analysisNot performedcppcheck per build, findings trendedbuilt
Reproducing the CI servern/aWhole controller rebuilt from a file in the repobuilt
Tethered tuning trials59, by handMatrix cells, unattendeddesigned
Untethered validation runs3Bounded by hardware time, not patiencedesigned
Run-on-board verdictA person watching a trackNumeric deviation, trended across buildsdesigned
Toolchain breakageDays of debugging, found by accidentPinned version, fails on the first builddesigned
Verification tableWritten once, at the endGenerated per run from job resultsdesigned
Model change reviewNot possible — binary fileGenerated-C diff on the merge requestdesigned
Reading the firmware that shippedGenerated C for both run-on-board models was cleaned up after the build — only the linked binaries surviveGenerated sources archived per build; the code behind any binary stays readabledesigned

Three of ten. That is the honest position, and it is the reason the first three rows are the ones worth reading — they are the rows where a claim can be checked against a build log rather than taken on trust.

The engineering conclusion of the project does not change: the V-model is the right shape for safety-critical embedded work, and it was applied correctly. What changes is that the right arm of the V stops being a thing one person does once, and starts being a thing the repository does continuously — which is the only version of it that survives contact with a team. The remaining seven rows are what finishing that argument costs.

← Back to the project and the V-model