Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

A Setup Script Is Not Enough

The naive answer to onboarding pain is "write a setup script."

That helps for a week. Then the script grows. It installs everything, configures everything, checks almost nothing, handles special cases with flags, and becomes too risky to change. The company has replaced the wiki with executable mud.

Setup-as-code has to be more disciplined than that.

Installation should behave like CI:

  • it starts from a known state
  • it runs the same way repeatedly
  • it fails fast when prerequisites are missing
  • it emits diagnostics
  • it proves the actual project can build
  • it records evidence
  • it can be replayed on fresh instances

The exit code is not the proof. The proof is the readiness evidence.

The Pipeline Shape

A useful onboarding pipeline has phases:

Phase Intent
contract load, validate, and resolve the setup contract
discover identify OS, runner type, network, package manager, current tools
preflight test physical and institutional boundaries before installing
install converge missing tools to the pinned baseline
configure certificates, Git, editor, shell, environment variables
probe verify every tool and boundary independently
project clone, restore, build, test, run, open the starter feature
evidence write the readiness report with versions, probes, logs, and failures

In DSL language:

during setup
  load SetupContract
  preflight PhysicalAccess
  preflight IdentityAccess
  install ToolchainBaseline
  configure WorkstationBootstrap
  verify ProjectReadiness
  emit ReadinessEvidence
Diagram
The installation pipeline runs as ordered phases. A failed preflight boundary short-circuits straight to evidence rather than installing into a machine that can never become ready.

The sequence is intentionally boring. Boring is good. A newcomer setup path should not be clever.

SOLID in Setup-as-code

Setup code needs SOLID because setup code changes constantly.

New SDK. New Git server. New VPN. New package mirror. New IDE extension. New certificate policy. New container runtime. New security scanner. New operating system image.

Without design, every change edits the same file.

SOLID principle Setup-as-code application
Single Responsibility GitProbe checks Git; DotnetInstaller installs .NET; VsCodeProfile configures the editor
Open/Closed add an ApkPackageManagerAdapter or PodmanRuntimeProbe without rewriting the setup contract
Liskov Substitution VM, container, LXC, and real-device runners honor the same probe contract where their capabilities overlap
Interface Segregation a network probe does not depend on package-manager, Git, or editor methods
Dependency Inversion setup phases depend on ports such as package manager, process runner, Git server, identity, and evidence sink
Modular a DotnetInstaller ships in a versioned module a profile imports by reference; new tools arrive as new modules, not edits to the bootstrap

This is not architecture astronautics. It is the difference between safe iteration and fear. The last row is the one that scales across teams, and Part VII gives it a syntax.

The right abstractions are small and contract-shaped:

SetupContract
RunnerProfile
RunnerCapability
PackageManagerPort
GitServerPort
IdentityProviderPort
HumanBoundaryPort
FilesystemPort
ProcessRunnerPort
EditorProfilePort
ReadinessEvidenceSink

Once the ports exist, the setup domain becomes testable. You can fake the Git server. You can test diagnostics. You can run the installer against a temporary filesystem. You can prove a failed certificate probe produces the right message. The contract stays stable while adapters change.

OS-Specificity Belongs in Platform Adapters

Setup has to touch the operating system. That is not a reason to make the model OS-specific.

The mistake is to let if windows ... elif macos ... spread through the whole pipeline until the domain logic and the platform logic are the same tangle. The bootstrap becomes a per-OS fork, and every readiness rule has to be re-read three times to be trusted once.

The discipline is the opposite. The domain stays language-agnostic and platform-neutral. The contract says what must be true; adapters decide how to make it true.

toolchain:
  dotnet:
    version: "10.0.100"
    versionPolicy: exact
    install:
      capability: package.install
      packages:
        winget: Microsoft.DotNet.SDK.10
        apt: dotnet-sdk-10.0
        apk: dotnet10-sdk
        brew: dotnet-sdk

That snippet is not a script. It is desired state. A C# runner, a TypeScript runner, or a shell wrapper can all read it. The Windows adapter maps package.install to winget. The Debian adapter maps it to apt. The Alpine adapter maps it to apk. The macOS adapter maps it to brew. Outside the adapter, the domain only sees ToolchainPinned or a typed failure.

This is the mechanism behind the series' OS-agnostic stance. The claim is not "the setup never runs Windows-specific code." Of course it does. The claim is that Windows-specific code lives behind a platform adapter, so the setup contract, readiness evidence, and DSL do not have to be rewritten when another platform is supported.

From the Field: The Machine Part at a CAC40 Group

This is not a thought experiment. The most concrete version of this system that I shipped ran inside a large CAC40 industrial group, on its developer onboarding for an industrial software platform. My part was the machine part: everything that happens after a developer logs into a fresh corporate workstation for the first time.

It was a fully scripted, version-controlled Windows bootstrap. Once the developer was authenticated on the machine, a set of provisioning scripts converged it to a known state: package manager, developer tools, SDKs, editor, environment, and the corporate specifics that a real enterprise workstation carries. It shipped with onboarding documentation for newcomers and, crucially, a continuous update strategy — because a baseline cannot be allowed to rot into a museum the moment it is written. New developers were meant to start from a machine that was already proven, not to debug it live.

Seen through this series, that system was one platform adapter. It was Windows, PowerShell, and the platform's package manager, and it was right for that context. What it taught me is everything around that adapter. The domain -- "a workstation is ready when the real project builds from a fresh clone" -- had nothing to do with Windows. The package manager was an implementation detail behind a port. And the part that no script could cross was real: badges, group membership, certificate enrollment, the human approvals that gate access. That is where I learned to treat the human handoff as a first-class boundary, not as a bug in the automation.

The sharpest of those constraints was privilege, and it is the detail most onboarding write-ups quietly skip. In a group that size the developer does not have free admin. There are grace periods and temporary local-admin windows, elevation through a just-in-time request or an explicitly elevated VS Code, WinGet hemmed in by AppLocker and execution policy, and a constant choice between per-user and per-machine installs. In front of all of it sits a corporate proxy and a root-certificate chain that every download has to satisfy. A setup that assumes a clean admin shell on the open internet is not wrong by a little in that world; it simply does not run. This is exactly why the welcome doctor --fix guardrails later in this part insist it "must respect privilege boundaries instead of silently escalating" — the lived machine is where that line stops being a nicety and becomes the difference between a setup that works and one that gets a developer's account flagged.

I also met the failure modes this series argues against. A single bootstrap script grows until changing it is frightening, unless it is decomposed behind ports and given the same SOLID discipline as production code. And an update is never "just a new version number" — it is a transition that has to be tested on fresh instances before it reaches a newcomer, which is the whole subject of Part VI.

So the OS-agnostic framing of this series is not a retroactive boast. The lived system was OS-specific on purpose. The OS-agnosticism is the lesson extracted from it: keep the domain platform-neutral, push every platform decision into an adapter behind a port, and the next OS becomes a new provider for an existing contract instead of a rewrite.

DRY Is the Version Contract

DRY matters most where drift is expensive.

Tool versions should not be repeated in:

  • a wiki page
  • a setup script
  • a CI job
  • a Dockerfile
  • a VS Code devcontainer
  • a support document
  • a "known good versions" spreadsheet

There should be one contract file, or one generated artifact from that contract. This is the same welcome.yaml Part VII develops in full — here is the toolchain-and-project core of one profile:

version: welcome/v1

variables:
  repoHost: git.company.local

profiles:
  backend-developer:
    toolchain:
      git:
        version: "2.51.0"
        versionPolicy: exact
      node:
        version: "^22.20.0"
        versionPolicy: semver
      dotnet:
        version: "10.0.100"
        versionPolicy: exact
      docker:
        version: "29.1.3"
        versionPolicy: exact

    project:
      repository: "git@${repoHost}:product/api.git"
      readiness:
        - restore
        - build
        - test
        - smoke

Every generated install command, readiness probe, CI job, and support report should read from that contract or from a derived artifact. Part VII adds the rest of the same document — boundaries, runner matrices, package mappings, and evidence policy.

This connects directly to GitLab.Ci.Yaml: hand-written CI and hand-written setup both drift. Typed or generated surfaces reduce that drift because there is one source of truth and a compiler or validator between intention and execution.

The Onboarding Doctor

Every other tool a developer trusts ships a doctor. brew doctor. flutter doctor. npm doctor. Onboarding deserves the same: a single command, welcome doctor, that reads the baseline and tells a human — or a CI job — exactly how ready a machine is.

The Doctor is the read-only projection of the pipeline. It runs the probe and evidence phases without installing or changing anything. It does not converge the machine; it reports on it. Each probe is a typed concern, not a string match:

Probe Answers
OsProbe which OS, version, and architecture is this, and is it a supported platform
ToolchainProbe do installed tool versions match the pinned baseline
NetworkProbe DNS, TLS chain, proxy, package mirror, and Git server reachability
IdentityProbe account, MFA, SSH key, certificate, and repository group membership
ProjectReadinessProbe can this machine clone, restore, build, and smoke-test the real project
SecurityProbe corporate root certificate, required scanners, and pre-commit hooks
EvidenceProbe does a complete readiness report exist for this run

Each probe returns a typed result with at least three fields:

health       Ok | Warn | Fail | Blocked
remediation  AutoFixable | ManualOwned | ObserveOnly
owner        optional team, person, or system

That split matters. A missing package may be Fail + AutoFixable. A missing Git group may be Blocked + ManualOwned + Identity team. A slow package mirror may be Warn + ObserveOnly. The Doctor should not flatten those into one red bucket, because the next action is different in each case. That is the loop closed with Part III: a boundary that was named as ownable becomes a blocking result with an owner, not a vague setup failure.

The user experience has two faces from one run. A human gets a readable report; a machine gets evidence:

welcome doctor

OS            Ok     Windows 11 26100  (supported platform)
Toolchain     Fail   dotnet 9.0.100 installed, baseline pins 10.0.100
Network       Ok     git.company.local reachable, TLS chain trusted
Identity      Blocked ManualOwned repository group 'product-core' pending -> Identity team
Project       Fail   build skipped: toolchain not ready
Security      Warn   pre-commit hooks not installed
Evidence      Ok     report written to .welcome/evidence/2026-06-26.json

2 failures, 1 warning, 1 manually owned blocker.
Run 'welcome doctor --fix' to repair the automatable failures.

Three flags carry the rest. --fix repairs only the idempotent AutoFixable failures and re-probes; it never crosses a ManualOwned boundary. --ci runs the Doctor against fresh instances and fails the job on any Fail or Blocked, which is how the baseline gets tested before a newcomer ever sees it — Part V shows how Packer and Vagrant produce the fresh machines those runs stand on. And the machine-readable evidence is the same artifact support reads when something does go wrong, so a diagnosis is a file, not a screenshot.

That file is the literal form of "evidence, not confidence." The human report above and the JSON below are two faces of one run:

{
  "schema": "welcome/evidence/v1",
  "instance": "backend-dev-03",
  "profile": "backend-developer",
  "ranAt": "2026-06-26T08:14:22Z",
  "summary": { "failures": 2, "warnings": 1, "blockers": 1 },
  "probes": [
    { "probe": "OsProbe", "health": "Ok", "detail": "Windows 11 26100" },
    { "probe": "ToolchainProbe", "health": "Fail", "remediation": "AutoFixable",
      "detail": "dotnet 9.0.100 installed, baseline pins 10.0.100" },
    { "probe": "IdentityProbe", "health": "Blocked", "remediation": "ManualOwned",
      "owner": "Identity team", "detail": "repository group 'product-core' pending" },
    { "probe": "ProjectReadinessProbe", "health": "Fail",
      "detail": "build skipped: toolchain not ready" }
  ]
}

A manager can ask for that file. A CI job can diff it between runs. Support can read it without a screen-share. And in the TPE or PME of Part I — where the founder is first-line support — welcome doctor is the whole support team: it tells the one person who can fix the machine exactly what to fix, without making them rediscover the toolchain. The proof is the artifact, not the exit code.

--fix needs strict guardrails:

  • it must produce a plan before changing anything
  • it should support --dry-run and require explicit confirmation for local workstation changes
  • it must never install outside actions declared as fixable by the setup contract
  • it must respect privilege boundaries instead of silently escalating
  • it must redact secrets, tokens, usernames where policy requires it, and certificate material from evidence
  • it should write before/after evidence so support can see what changed
  • it should make rollback explicit where rollback is possible, and mark irreversible operations as such

That makes repair scriptable without making it reckless. The contract says what may be repaired; adapters say how to repair it; the Doctor proves the result.

welcome doctor is the smallest, most demonstrable piece of this whole series. It is also the answer to the series' one demand.

Test your onboarding.

⬇ Download