Survive the Cutover, Then Redesign
Prove CI/CD cutover parity, then plan a separate redesign with evidence.
Introduction
30 Second Summary
Moving a working release process to a new platform creates an awkward question: did the software stay the same? Redesigning it during the move makes every changed file harder to explain.
In this project, you will rehearse a CI/CD cutover inside a synthetic local lab. You will use an artifact oracle to prove why preserving behavior through cutover creates stronger evidence for a separate redesign.
What You'll Build
You will finish with a private release whose preserved red and green evidence shows exactly why the accepted wrapper survives cutover.
By the end of this project, you'll have:
- A synthetic local rehearsal where the Jenkins stand-in and GitLab destination build the same commit under held-equal inputs.
- A red-to-green artifact oracle that catches a deliberately omitted build flag before proving the accepted wrapper produces matching SHA-256 digests.
- A closed delivery record where another engineer can inspect nine classified jobs. They can also review command-produced schedule projections. The record closes with two evidence-first readouts and a private tagged GitHub release linked to a completed Linear project.
- Secret Mission: Change the synthetic program output on a separate redesign branch. Define a new acceptance test that replaces byte equality.
Do I need any experience before starting?
You need existing GitHub and Linear accounts. You also need a Linux Docker Engine host with at least 16 GB of memory.
Use Ubuntu under WSL2 on Windows. Use a pre-existing Linux virtual machine on macOS.
Before We Start
Before any hands-on work, commit to preserving behavior through cutover with a wrapper before redesigning under a separate acceptance test. This sequencing keeps one cause per difference because combining migration with redesign makes every changed byte ambiguous.
Establish the Host and Delivery Record
Your delivery record needs a Linux host running Docker Engine. Git preserves every decision that shapes the rehearsal.
The architecture, assessment plan, and reversal triggers must exist before the first build. This ordering prevents the evidence from being rewritten to fit a later result.
In this step, get ready to:
- Prepare a Linux Docker Engine host without Docker Desktop.
- Create a private delivery repository plus one five-issue Linear project.
- Commit the pre-build decisions before recording their baseline commit.
Prepare the Linux host
The stand-in services must share one Linux runtime so each runner receives the same local toolchain. Docker Desktop stays outside this project.
macOS
macOS uses a pre-existing Linux virtual machine for this rehearsal. The complete lab remains inside that Linux environment.
- Switch to the pre-existing Linux virtual machine reserved for this rehearsal.
- Follow the official Ubuntu installation guide if Docker Engine or the Docker Compose plugin is missing.
- Install the documented Ubuntu package set that includes Docker Engine plus the Docker Compose plugin.
- Keep Docker Desktop outside this rehearsal.
Windows
Use Ubuntu under WSL2 as the Linux host. Systemd allows Ubuntu to manage the Docker daemon as a service.
- Start Ubuntu under WSL2.
- Create or update /etc/wsl.conf with this systemd configuration:
[boot]
systemd=true
What does this configuration do?
The boot setting starts systemd when the Ubuntu distribution launches. Docker Engine can then run as a managed Linux service.
- Save /etc/wsl.conf.
- Confirm your editor shows both configuration lines in the saved file.
- Return to Windows PowerShell.
- Restart the WSL2 distribution by running this command:
wsl.exe --shutdown
What does this command do?
The command stops the current WSL2 distribution. The next Ubuntu launch reads the new systemd setting.
- Start Ubuntu under WSL2 again.
- Follow the official Ubuntu installation guide if Docker Engine or the Docker Compose plugin is missing.
- Keep Docker Desktop outside this rehearsal.
Linux
Use your Linux machine directly for this rehearsal. A workstation with at least 16 GB of memory provides the constrained minimum for the local lab.
- Follow the official Ubuntu installation guide if Docker Engine or the Docker Compose plugin is missing.
- Install the documented Ubuntu package set that includes Docker Engine plus the Docker Compose plugin.
- Keep the entire lab on this Linux host.
Before you check the host, predict which command proves that the Docker daemon is running.
- Verify Docker Engine, Docker Compose, and Git by running these commands:
sudo systemctl status docker
docker compose version
git --version
What do these checks prove?
- The systemd check confirms that the Docker daemon is running inside Linux.
- The Compose check confirms that the Docker Compose plugin is available.
- The Git check confirms that the host can create the delivery history.
You should see Docker reported as running. You should also see version output from Docker Compose and Git.
Is the Docker service unavailable?
Confirm that you installed Docker Engine inside the Linux host. On Windows, confirm that Ubuntu restarted after you enabled systemd.
Docker Desktop cannot satisfy this project boundary. Help me diagnose Docker Engine inside my Linux host.
Create the delivery rails
A private GitHub repository holds the reproducible delivery record. Linear supplies the issue IDs that connect branches and pull requests to planned work.
Why keep the repository private?
The repository describes a synthetic workstation-only rehearsal. Private visibility limits access to you and the collaborators you explicitly authorize.
Existing GitHub and Linear Free-plan accounts keep the incremental cost at zero. Hosted runners and metered interfaces stay disabled.
- Open the new repository page while signed in to your existing GitHub account.
- Select your account from the Owner menu.
- Enter survive-cutover-redesign as the repository name.
- Set the repository visibility to Private.
- Turn Add README on so GitHub creates the initial main branch.
- Leave GitHub Actions and Codespaces unused.
- Click Create repository.
- Copy the repository URL into https://github.com/your-name/survive-cutover-redesign.git.
- Move to your Desktop and clone the private repository by running these commands:
cd ~/Desktop
git clone [[YOUR_REPO_URL="https://github.com/your-name/survive-cutover-redesign.git"]]
cd survive-cutover-redesign
What do these commands do?
The first command gives the project a predictable location. The clone command copies the private repository into survive-cutover-redesign.
The final command makes that repository your current directory. Every baseline file now lands in the tracked project.
- Confirm that the clone exists by locating README.md inside the survive-cutover-redesign folder.
Did GitHub reject the clone?
Confirm that your Git credentials can access the private repository. Check that the saved URL belongs to the repository you just created.
If authentication still fails, help me connect Git to my private GitHub repository.
- Open the Projects page in your existing Linear workspace.
- Click the + button in the project view.
- Enter Survive the cutover, then redesign as the project name.
- Create the project.
The five issues mirror the five hands-on phases after setup. Their IDs become the traceability key for branches and pull requests.
- Press C from the project view to create an issue named Establish host and pre-build baseline.
- Record its ID as your setup issue ID.
- Press C to create an issue named Bring up the day-zero lab.
- Record its ID as your lab issue ID.
- Press C to create an issue named Run the negative-control oracle.
- Record its ID as your red oracle issue ID.
- Press C to create an issue named Restore parity and run the green oracle.
- Record its ID as your green oracle issue ID.
Where are you now?
Your Linear project now represents the setup, lab, red oracle, and green oracle phases. One final issue closes the delivery record.
- Press C to create an issue named Publish the decision pack and close the release.
- Record its ID as your release issue ID.
- Click your workspace name in the upper-left corner.
- Select Settings.
- Select Features.
- Select Integrations.
- Select GitHub.
- Click Enable.
Keep the integration narrow
Repository-level access is enough for this rehearsal. Selecting only this repository keeps the integration boundary explicit.
- Select the GitHub account that owns the private repository.
- Choose Only select repositories.
- Select survive-cutover-redesign.
- Review the requested permissions.
- Click Install.
- Authenticate with your existing personal GitHub account when prompted.
- Return to the Linear project.
- Confirm that the project contains exactly five issues.
Good progress. Your private repository and five-issue project now form the delivery rails for every later evidence change.
Commit the pre-build baseline
A pre-build baseline makes the original architecture and risk decisions auditable. The recorded commit must already contain the three ADRs, topology, and 60-minute plan.
- Create the ticket-linked baseline branch by running this command:
git switch -c [[BASELINE_ISSUE_ID="your setup issue ID"]]-design-baseline
What does this branch name prove?
The Linear issue ID connects the source branch to planned setup work. The suffix identifies the branch as the design baseline.
- Create the required folders inside survive-cutover-redesign by running this command:
mkdir -p cmd/hello docs evidence records scripts
What does this command create?
The folders separate source code, documentation, evidence, decision records, and executable scripts. This layout keeps the delivery record readable.
- Create records/ADR-001-wrapper-first.md with this decision record:
Decision: Preserve behavior through cutover; redesign next quarter. Alternative defeated: Combine migration and redesign under one notice. Tradeoff accepted: Carry the disliked behavior for one quarter. Reason: One change class preserves a single-cause artifact oracle and limits the active blast radius to one job. Reversal trigger: All wrappers are green, or an approved separate window provides a new redesign acceptance test.
What does ADR 001 establish?
The record chooses wrapper-first sequencing. It also names the tradeoff plus the condition that permits a later redesign.
- Save records/ADR-001-wrapper-first.md.
- Confirm that the file appears inside the records folder.
- Create records/ADR-002-sha256-verdict.md with this artifact verdict:
Decision: Accept parity only when artifacts from one synthetic commit have equal SHA-256 digests. Alternative defeated: Accept unnamed metadata differences. Tradeoff accepted: Spend time normalizing four archive properties and investigating every red result. Reversal trigger: An approved redesign explicitly replaces byte equality with a new product-level contract.
What does ADR 002 establish?
The record makes SHA-256 equality the migration verdict. Every difference therefore needs an explicit cause.
- Save records/ADR-002-sha256-verdict.md.
- Confirm that the file appears beside ADR 001.
- Create records/ADR-003-measure-one-wrapper.md with this measurement decision:
Decision: Time the sample wrapper by command and use that result in the projection. Alternative defeated: Use a guessed per-job average. Tradeoff accepted: One idle-workstation sample is optimistic and must be labelled as such. Reversal trigger: Additional measured wrappers show the sample is not representative.
What does ADR 003 establish?
The schedule projection starts with command-produced wrapper time. Additional measurements can reverse the decision if the first sample proves unrepresentative.
- Save records/ADR-003-measure-one-wrapper.md.
- Confirm that the records folder now contains three ADR files.
- Create docs/architecture-at-a-glance.md with this synthetic topology:
One Linux Docker Engine hosts GitLab CE, one GitLab Runner, a Jenkins LTS stand-in, and diffoscope. Jenkins seeds nine synthetic jobs from one Job DSL file. The accepted Jenkins and GitLab paths use the same locally built image, Go toolchain, build script, GNU tar, stages, and artifact path. GitHub and Linear are delivery rails only.
What does the topology hold equal?
The legacy stand-in and destination runner share one local image. They also share the toolchain, scripts, archive tool, stages, and artifact path.
- Save docs/architecture-at-a-glance.md.
- Confirm that the architecture file appears inside docs.
- Create evidence/plan.md with this assessment plan:
# 60-Minute Cutover Assessment Plan
Authored before the first build.
| Work | Minutes |
| --- | ---: |
| Setup | 8 |
| Migration | 14 |
| Oracle | 12 |
| Inventory and command-produced projection | 14 |
| Readouts and escalation letter | 12 |
| Total | 60 |
Why record the 60-minute plan now?
The plan fixes the assessment boundary before any build result exists. Its five work categories total exactly 60 minutes.
- Save evidence/plan.md.
- Confirm that the table ends with a total of 60.
- Create go.mod with the module name plus the pinned Go version:
module cutover-assessment
go 1.27.1
What does the module file control?
The module identifies the synthetic assessment codebase. The 1.27.1 declaration pins the language version expected by the local build image.
- Save go.mod.
- Confirm that the file appears at the top level of survive-cutover-redesign.
- Create cmd/hello/main.go with the synthetic program:
package main
import "fmt"
func main() { fmt.Println("hello") }
What does the synthetic program provide?
The program produces one stable line of product output. Later runners package the same compiled result for byte comparison.
- Save cmd/hello/main.go.
- Confirm that the file appears inside cmd/hello.
- Create scripts/build.sh with the held-equal build inputs:
#!/usr/bin/env bash
set -euo pipefail
rm -rf out/bin
mkdir -p out/bin
CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags='-buildid=' -o out/bin/hello ./cmd/hello
What does the build script hold equal?
- The disabled C toolchain removes host compiler variation.
- The path trimming removes runner-specific source paths.
- The remaining flags omit repository metadata plus the linker build identifier.
- Save scripts/build.sh.
- Confirm that the script writes the binary to out/bin/hello.
- Create scripts/package.sh with the normalized archive command:
#!/usr/bin/env bash
set -euo pipefail
rm -f out/hello.tar
tar --create --file=out/hello.tar \
--mtime='UTC 2020-01-01' \
--owner=0 --group=0 --numeric-owner \
--sort=name \
--mode=go+u,go-w \
--directory=out/bin hello
What does the package script normalize?
GNU tar fixes modification time, numeric ownership, entry order, and file mode. Those four properties stop runner metadata from changing archive bytes.
- Save scripts/package.sh.
- Confirm that the script targets out/hello.tar.
Complete the cumulative scaffold
The baseline also needs the remaining repository files from the Complete Code reference. Preserve every path plus every character exactly.
- Add the environment example, ignore file, Compose definition, Jenkins image, plugin pins, Jenkins configuration, and Job DSL file.
- Add the GitLab CI include, accepted wrapper, negative control, oracle script, and nine synthetic legacy jobs.
- Add the inventory, inventory-state definitions, normalization rationale, held-equal build inputs, assessment command, license record, README, and remaining six documentation pages.
- Make the build and packaging scripts executable by running this command:
chmod +x scripts/build.sh scripts/package.sh
Why set executable permissions?
The permission allows both runners to invoke the scripts directly when needed. The script contents remain identical on each path.
Use these tabs to compare the target files before creating the baseline commit.
βοΈ Awesome, I've got everything!
Great. Save every file before you commit the pre-build baseline.
β§ I'd like to double check the full code
package main
import "fmt"
func main() { fmt.Println("hello") }
One Linux Docker Engine hosts GitLab CE, one GitLab Runner, a Jenkins LTS stand-in, and diffoscope. Jenkins seeds nine synthetic jobs from one Job DSL file. The accepted Jenkins and GitLab paths use the same locally built image, Go toolchain, build script, GNU tar, stages, and artifact path. GitHub and Linear are delivery rails only.
Contains the non-empty commit SHA from git rev-parse HEAD for the committed pre-build baseline.
# 60-Minute Cutover Assessment Plan
Authored before the first build.
| Work | Minutes |
| --- | ---: |
| Setup | 8 |
| Migration | 14 |
| Oracle | 12 |
| Inventory and command-produced projection | 14 |
| Readouts and escalation letter | 12 |
| Total | 60 |
module cutover-assessment
go 1.27.1
Decision: Preserve behavior through cutover; redesign next quarter. Alternative defeated: Combine migration and redesign under one notice. Tradeoff accepted: Carry the disliked behavior for one quarter. Reason: One change class preserves a single-cause artifact oracle and limits the active blast radius to one job. Reversal trigger: All wrappers are green, or an approved separate window provides a new redesign acceptance test.
Decision: Accept parity only when artifacts from one synthetic commit have equal SHA-256 digests. Alternative defeated: Accept unnamed metadata differences. Tradeoff accepted: Spend time normalizing four archive properties and investigating every red result. Reversal trigger: An approved redesign explicitly replaces byte equality with a new product-level contract.
Decision: Time the sample wrapper by command and use that result in the projection. Alternative defeated: Use a guessed per-job average. Tradeoff accepted: One idle-workstation sample is optimistic and must be labelled as such. Reversal trigger: Additional measured wrappers show the sample is not representative.
#!/usr/bin/env bash
set -euo pipefail
rm -rf out/bin
mkdir -p out/bin
CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags='-buildid=' -o out/bin/hello ./cmd/hello
#!/usr/bin/env bash
set -euo pipefail
rm -f out/hello.tar
tar --create --file=out/hello.tar \
--mtime='UTC 2020-01-01' \
--owner=0 --group=0 --numeric-owner \
--sort=name \
--mode=go+u,go-w \
--directory=out/bin hello
- Stage the complete pre-build baseline and commit it by running these commands:
git add .
git commit -m "Establish pre-build design baseline"
What does this commit preserve?
The commit freezes the decisions plus the cumulative repository scaffold before any build occurs. Its SHA becomes the point used to detect later design drift.
- Record the committed baseline SHA by running this command:
git rev-parse HEAD > evidence/design-baseline-commit.txt
What does the recorded SHA identify?
The file stores the exact commit that already contains the pre-build decisions. Later verification can compare those files against this immutable point.
- Commit the baseline record and push the ticket-linked branch by running these commands:
git add evidence/design-baseline-commit.txt
git commit -m "Record design baseline commit"
git push -u origin [[BASELINE_ISSUE_ID="your setup issue ID"]]-design-baseline
What does this push publish?
The first commit remains the recorded design baseline. The second commit adds its SHA to the delivery record.
πββοΈ Did the commit or push fail?
A commit failure can mean Git lacks your author identity. A push failure can mean the private repository credentials or branch name do not match.
Use help me diagnose my baseline commit or push.
- Return to the private repository on GitHub.
- Click Compare & pull request for the pushed baseline branch.
- Confirm that the base branch is main.
- Enter Establish the pre-build design baseline as the pull request title.
- Set the pull request description to this ticket link:
Fixes [[BASELINE_ISSUE_ID="your setup issue ID"]]
Why use the issue ID here?
The closing keyword links the pull request to its Linear issue. The branch name and description now carry the same traceability key.
- Click Create Pull Request.
- Review the proposed files in the pull request.
- Merge the pull request after confirming that it contains only the pre-build baseline.
- Switch the local repository back to the merged main branch by running these commands:
git switch main
git pull origin main
What does this synchronization do?
The local main branch receives the merged baseline from GitHub. Your next step starts from the same delivery state shown in the private repository.
Before the final check, predict which output proves that the design baseline existed before any build.
- Verify the host plus the committed baseline by running these commands:
sudo systemctl status docker
docker compose version
git rev-parse HEAD
cat evidence/design-baseline-commit.txt
What should this verification show?
Docker should report a running service. Docker Compose should print a version.
Git should print the current commit SHA. The evidence file should contain a non-empty SHA for the earlier commit that already holds the ADRs, topology, and plan.
- Return to the GitHub repository.
- Confirm that its visibility is Private.
- Confirm that the merged baseline files are visible on main.
- Return to the Linear project.
- Confirm that exactly five issues belong to the project.
- Confirm that the baseline issue contains the linked pull request.
That is the design boundary secured. Your host is ready while the repository already proves what was decided before the first build.
Your delivery record now has a fixed starting point. Next, you will bring up the local day-zero lab without changing that baseline.
Bring Up the Day-Zero Lab
Your pre-build decisions now form a fixed baseline. The next goal is to turn that design into a synthetic delivery lab without changing the evidence that came first.
One local Docker Compose runtime keeps the stand-in and destination inside the same toolchain boundary. This prevents Jenkins or GitLab Runner from quietly selecting different build inputs.
In this step, get ready to:
- Validate the local configuration and start the four-service lab.
- Create the synthetic GitLab project and register its local runner.
- Capture the day-zero versions and verify the nine seeded Jenkins jobs.
Validate and start the stack
The committed compose.yaml defines GitLab Community Edition, GitLab Runner, Jenkins, and diffoscope as one local stack. The .env file supplies only the disposable Jenkins administrator credentials.
You are about to set a local-only Jenkins password. The .gitignore file keeps .env outside the delivery record.
- Return to the private repository from the previous step.
- Duplicate .env.example from your repository file browser.
- Name the duplicate .env.
You should now see .env.example and .env beside compose.yaml.
- Open .env in your text editor.
- Replace replace-with-a-local-only-password with a password used only for this lab.
- Save .env.
- Validate the resolved Compose configuration by running this command:
docker compose config
What does this check?
- The command resolves the service definitions and substitutes the local values from .env.
- A successful result confirms that Compose can interpret the complete configuration before creating any containers.
You should see the resolved definitions for gitlab, gitlab-runner, jenkins, and diffoscope without a configuration error.
- Build the local Jenkins image and start the lab by running this command:
docker compose up --build -d
What does this command do?
Compose builds cutover-lab-jenkins:latest from jenkins/Dockerfile. It also creates the cutover-lab network and the named volumes declared in compose.yaml.
Expect the first build and GitLab initialization to take several minutes. The terminal keeps printing progress while images download and Jenkins installs its pinned plugins.
- Check the service state after the startup activity settles by running:
docker compose ps
What should this show?
The output should list gitlab, gitlab-runner, cutover-jenkins, and cutover-diffoscope as running services.
πββοΈ Is a service missing or restarting?
Confirm that your Linux host has enough available memory for GitLab initialization. Close unrelated containers before retrying the startup command.
Check that ports 8929, 2424, and 8080 are free on the host.
Ask for help with the failing service by sharing its name and recent logs without including your .env values. Help me diagnose a restarting service in my cutover-lab Compose stack
Create the local project and register the runner
The local GitLab project represents the synthetic destination. Its project runner uses the Docker executor so destination jobs execute inside the locally built Jenkins image.
The initial GitLab administrator password is a disposable lab credential. Keep it out of screenshots because the command reveals it in the terminal.
- Retrieve the initial GitLab administrator password by running this command:
sudo docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password
Where does this password come from?
GitLab writes the generated administrator password to /etc/gitlab/initial_root_password during first initialization. The file is automatically deleted after its short initialization window.
Use the value only to enter this workstation-local GitLab instance. No external credential enters the lab.
- Visit http://gitlab.localhost:8929 in your browser.
- Enter root as the username.
- Enter the generated administrator password.
- Submit the sign-in form.
- Select Create new.
- Select New project/repository.
Keep the destination synthetic
This project represents a local migration destination for the rehearsal. It contains synthetic code and synthetic delivery evidence only.
- Select Create blank project.
- Enter cutover-lab in the Project name field.
- Select Private under Visibility Level.
- Select Create project.
You should see the new cutover-lab project page. This confirms that the synthetic destination now exists inside the local stack.
- Select Settings in the project sidebar.
- Select CI/CD.
- Expand the Runners section.
- Select Create project runner.
- Select the Linux operating system.
- Select Run untagged in the Tags section.
- Enter cutover-lab in the Runner description field.
- Select Create runner.
Finding the runner authentication token is a little fiddly because GitLab displays it for a limited time. Keep this registration page open until the next command succeeds.
- Copy the displayed runner authentication token without sharing or storing it in the repository.
- Replace your-runner-token-here in the command below with the copied token.
- Register the existing runner container by running the completed command:
docker compose exec -T gitlab-runner gitlab-runner register \
--non-interactive \
--url "http://gitlab.localhost:8929" \
--token "your-runner-token-here" \
--executor "docker" \
--docker-image "cutover-lab-jenkins:latest" \
--docker-pull-policy "if-not-present" \
--docker-network-mode "cutover-lab" \
--description "cutover-lab"
How is the runner constrained?
- The docker executor runs each destination job in a container.
- The cutover-lab network connects the job container to the local services.
- The if-not-present policy preserves the locally built cutover-lab-jenkins:latest image.
- The shared image keeps the destination runner on the same Go and GNU tar toolchain as the Jenkins stand-in.
- Return to the expanded Runners section in the local GitLab project.
- Refresh the project page.
- Confirm that the cutover-lab runner is shown as online.
The local destination can now accept jobs through its registered Docker executor. The runner uses the intended network and image instead of selecting a different environment.
Does the runner stay offline?
Confirm that the token came from the cutover-lab project runner page. A token from another scope cannot attach this runner to the intended project.
Check that the gitlab-runner service remains running in the latest docker compose ps output.
Use this prompt without including the authentication token. Help me diagnose an offline GitLab Runner in my local cutover-lab stack
Capture the day-zero evidence
Day-zero evidence freezes the actual runtime identities before the timed assessment starts. This gives later oracle results a reproducible toolchain record.
- Create evidence/day-zero-versions.txt from the running services by running these commands:
docker compose images > evidence/day-zero-versions.txt
docker compose exec -T jenkins go version >> evidence/day-zero-versions.txt
docker compose exec -T jenkins tar --version >> evidence/day-zero-versions.txt
docker compose exec -T diffoscope diffoscope --version >> evidence/day-zero-versions.txt
docker compose exec -T gitlab-runner gitlab-runner --version >> evidence/day-zero-versions.txt
docker compose exec -T jenkins cat /usr/share/jenkins/ref/plugins.resolved.txt >> evidence/day-zero-versions.txt
What does this evidence preserve?
- The first command records the resolved Compose image identities.
- The next commands record the Go, GNU tar, diffoscope, and GitLab Runner versions from their running containers.
- The final command records the effective Jenkins plugin dependency set created during the image build.
- Open evidence/day-zero-versions.txt from your repository file browser.
- Confirm that the file contains output from all six commands.
- Return to records/tool-licenses.md from the pre-build baseline.
- Confirm that the record excludes Docker Desktop and every metered interface.
Before you run the final checks, what service count and pinned tool versions do you expect the lab to report?
- Verify the running stack and its pinned command-line tools by running:
docker compose ps
docker compose exec -T jenkins go version
docker compose exec -T jenkins tar --version
docker compose exec -T diffoscope diffoscope --version
docker compose exec -T gitlab-runner gitlab-runner --version
What do these checks prove?
The service list proves that the local Compose boundary is running. The version commands prove that evidence comes from the containers used by the rehearsal.
You should see Go go1.27.1, a GNU tar version banner, diffoscope 329, and GitLab Runner 19.3.2.
- Visit http://localhost:8080 in your browser.
- Enter admin as the Jenkins username.
- Enter the local Jenkins password stored in .env.
- Submit the Jenkins sign-in form.
- Count the jobs listed on the Jenkins dashboard.
You should see exactly nine jobs: build-hello, plain-lint, plain-test, plain-package, plain-docs, plain-sbom, shared-policy, shared-audit, and orphan-plugin.
That is the lab boundary established. Your four services are running, the destination runner is registered, and the nine-job stand-in is ready for the first measured wrapper test.
Wrap build-hello and Run the Oracle Red
Your day-zero lab now gives the Jenkins stand-in and GitLab Runner one local runtime. A shared runtime removes toolchain drift from the comparison.
Before trusting a passing oracle, you need proof that it catches a known difference. This step deliberately removes -trimpath from a temporary negative control so the resulting byte difference has one explicit cause.
In this step, get ready to:
- Pin one synthetic commit for the comparison.
- Produce both red artifacts from the held-equal source.
- Prove the oracle detects the controlled build-input difference.
Record the input and build the legacy artifact
A same-input oracle compares artifacts produced from one exact Git commit. Recording that commit now prevents later source edits from entering only one delivery path.
- Record the current synthetic commit in evidence/input-commit.txt by running this command:
git rev-parse HEAD > evidence/input-commit.txt
What does this command record?
The command resolves the current commit. Its output becomes the fixed source input for this red comparison and the later green comparison.
- Return to the repository file explorer.
- Confirm that evidence/input-commit.txt contains one non-empty commit SHA.
Is the input commit file empty?
Check that your terminal is still inside the private repository from earlier. Confirm that the repository has at least one commit.
Ask for help with the commit record.
The wrapper clock turns this migration sample into a measured schedule input. Starting it before the first delivery-path run captures the full wrapper exercise.
- Start the wrapper measurement clock by running this command:
go run ./cmd/assessment measure-start --name wrapper
What starts the measurement?
The assessment client records the current UTC time for the wrapper measurement. The timestamp is written to evidence/measurements/wrapper-start.txt.
- Confirm that evidence/measurements/wrapper-start.txt now contains a timestamp.
The seeded build-hello job represents the synthetic legacy path. It runs the shared build script before packaging out/hello.tar from the mounted repository.
- Return to the Jenkins page from the day-zero lab.
- Select the build-hello job.
- Start one build from the job page.
- Wait for the build to finish.
You should see the build, test, and package stages finish successfully. The completed build exposes out/hello.tar as an archived artifact.
- Open the archived files for the successful build.
- Download the individual out/hello.tar artifact.
- Create the evidence/artifacts/red/legacy folder inside the repository.
- Save the downloaded archive as evidence/artifacts/red/legacy/hello.tar.
Good work. The legacy side of the red comparison is now preserved before the temporary wrapper can produce its own archive.
Is the legacy artifact missing?
Check the Jenkins stage results to identify whether build, test, or package failed. Confirm that the mounted repository still contains scripts/build.sh and scripts/package.sh.
Ask for help with the Jenkins artifact.
Run the negative-control wrapper
The temporary control keeps the same Go toolchain. It also keeps the same three stages and GNU tar packaging path.
Its build command retains every held-equal flag except -trimpath. That single omission gives the oracle one known cause to detect.
- Create .gitlab-ci.yml inside the repository with this content:
include:
- local: '/ci/hello.yml'
stages: [build, test, package]
What does the root pipeline define?
The root configuration fixes the stage contract as build, test, and package. The local include keeps the wrapper configuration separate from the project entry point.
- Save .gitlab-ci.yml.
- Confirm that .gitlab-ci.yml appears at the top level of the repository.
- Create evidence/negative-control/hello-red.yml inside the repository with this content:
default:
image:
name: cutover-lab-jenkins:latest
entrypoint: [""]
build:
stage: build
script:
- rm -rf out/bin && mkdir -p out/bin
- CGO_ENABLED=0 go build -buildvcs=false -ldflags='-buildid=' -o out/bin/hello ./cmd/hello
artifacts:
paths: [out/bin/hello]
test:
stage: test
needs: [build]
script: ["go test ./..."]
package:
stage: package
needs: [build, test]
script: ["bash scripts/package.sh"]
artifacts:
paths: [out/hello.tar]
How is the control isolated?
- The build job retains CGO_ENABLED=0, -buildvcs=false, and -ldflags='-buildid='.
- The test job runs the same Go test command.
- The package job produces out/hello.tar through the shared package script.
- The omitted -trimpath flag supplies the known cause for the expected byte difference.
- Save evidence/negative-control/hello-red.yml.
- Confirm that the repository file explorer shows the negative-control file.
- Create a ticket branch whose name starts with the matching Linear issue ID.
- Push the ticket branch to the synthetic local GitLab project.
You should see the ticket branch in the local GitLab project. Its files should include .gitlab-ci.yml and evidence/negative-control/hello-red.yml.
- Configure the local GitLab project to read this branch's pipeline from evidence/negative-control/hello-red.yml.
- Start the pipeline for the ticket branch.
- Wait for the pipeline to finish.
You should see successful build, test, and package jobs. The negative control changes the artifact bytes without breaking the pipeline.
- Open the successful package job's artifacts.
- Download the individual out/hello.tar artifact.
- Create the evidence/artifacts/red/wrapped folder inside the repository.
- Save the downloaded archive as evidence/artifacts/red/wrapped/hello.tar.
Both red-run artifacts are now in place. One came from the Jenkins stand-in while the other came from the temporary GitLab control.
Did the negative-control pipeline fail early?
Confirm that the registered runner still uses the cutover-lab network. Confirm that its image remains cutover-lab-jenkins:latest.
Ask for help with the local pipeline.
Prove the oracle detects the difference
The oracle calculates two SHA-256 digests before inspecting the archives with diffoscope. Unequal digests are accepted as a diagnostic result only when every comparator hunk receives a named cause.
- Create scripts/oracle.sh with this digest and verdict section:
#!/usr/bin/env bash
set -uo pipefail
run="${1:?usage: oracle.sh red|green}"
if [[ "$run" != "red" && "$run" != "green" ]]; then exit 2; fi
out="evidence/oracle/$run"
legacy="/workspace/evidence/artifacts/$run/legacy/hello.tar"
wrapped="/workspace/evidence/artifacts/$run/wrapped/hello.tar"
mkdir -p "$out"
docker compose exec -T jenkins sha256sum "$legacy" "$wrapped" | tee "$out/oracle.sha256"
legacy_digest="$(awk 'NR == 1 {print $1}' "$out/oracle.sha256")"
wrapped_digest="$(awk 'NR == 2 {print $1}' "$out/oracle.sha256")"
if [[ "$legacy_digest" == "$wrapped_digest" ]]; then
if [[ "$run" == "red" ]]; then
printf 'VERDICT=ERROR\nREASON=negative control did not create a difference\n' | tee "$out/status.txt"
exit 2
fi
printf 'VERDICT=PASS\nUNEXPLAINED_COUNT=0\n' | tee "$out/status.txt"
exit 0
fi
What does the digest section do?
- The argument check accepts only the red or green evidence path.
- The checksum command writes the legacy digest first and the wrapped digest second.
- Equal red digests produce an error because the negative control failed to create its known difference.
- Save scripts/oracle.sh.
- Confirm that the saved section ends with the outer fi.
- Append this comparison and accounting section to scripts/oracle.sh after the final fi line:
set +e
docker compose exec -T diffoscope diffoscope --no-default-limits --text=- \
"/evidence/artifacts/$run/legacy/hello.tar" \
"/evidence/artifacts/$run/wrapped/hello.tar" | tee "$out/diffoscope.txt"
diffoscope_status="${PIPESTATUS[0]}"
set -e
if [[ "$diffoscope_status" -eq 2 || "$run" == "green" ]]; then
printf 'VERDICT=FAIL\nUNEXPLAINED_COUNT=1\n' | tee "$out/status.txt"
exit 1
fi
hunks="$(grep -c '^@@' "$out/diffoscope.txt" || true)"
if [[ "$hunks" -eq 0 ]]; then exit 2; fi
printf 'hunk,section_or_comparator,cause,status\n' > "$out/difference-accounting.csv"
for ((i=1; i<=hunks; i++)); do
printf '%s,Go artifact comparator hunk %s,temporary negative control omitted -trimpath,explained\n' "$i" "$i" >> "$out/difference-accounting.csv"
done
printf 'VERDICT=EXPECTED_RED\nNAMED_DIFFERENCE_COUNT=%s\nUNEXPLAINED_COUNT=0\n' "$hunks" | tee "$out/status.txt"
What does the comparison section do?
- The detailed comparison writes its complete output to diffoscope.txt.
- The hunk count creates one named accounting row for each detected comparator hunk.
- The final status preserves the expected-red verdict with zero unexplained differences.
- Save the completed scripts/oracle.sh file.
The detailed comparison may take a moment while diffoscope inspects both archives. A short quiet period means the comparator is still working.
Before you run the oracle, do you expect the two artifact digests to match?
- Run the preserved red comparison with this command:
bash scripts/oracle.sh red
What evidence does the oracle produce?
The command stores both digest lines before running the detailed comparison. It then writes comparator-hunk accounting and a final status under evidence/oracle/red.
You will see two different digest lines. The terminal finishes with VERDICT=EXPECTED_RED and UNEXPLAINED_COUNT=0.
That is the diagnostic proven. Your oracle detected the deliberately omitted flag and assigned an explicit cause to every comparator hunk.
- Confirm that evidence/oracle/red/oracle.sha256 contains two unequal digest lines.
- Confirm that evidence/oracle/red/diffoscope.txt contains the full archive comparison.
- Confirm that evidence/oracle/red/difference-accounting.csv contains one explained row for every detected hunk.
- Confirm that evidence/oracle/red/status.txt reports a positive named-difference count and zero unexplained differences.
Did the oracle miss the expected difference?
Confirm that the wrapped artifact came from evidence/negative-control/hello-red.yml. Check that its Go build command omits -trimpath.
Confirm that the legacy artifact appears first in evidence/oracle/red/oracle.sha256.
Ask for help with the red oracle.
Use this reference to compare the files that define the diagnostic and its oracle.
βοΈ Awesome, I've got everything!
Your negative control and oracle are ready. Keep the red evidence unchanged so the next step can compare it with the accepted green result.
β§ I'd like to double check the full code
include:
- local: '/ci/hello.yml'
stages: [build, test, package]
package main
import "fmt"
func main() { fmt.Println("hello") }
module cutover-assessment
go 1.27.1
#!/usr/bin/env bash
set -euo pipefail
rm -rf out/bin
mkdir -p out/bin
CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags='-buildid=' -o out/bin/hello ./cmd/hello
#!/usr/bin/env bash
set -euo pipefail
rm -f out/hello.tar
tar --create --file=out/hello.tar \
--mtime='UTC 2020-01-01' \
--owner=0 --group=0 --numeric-owner \
--sort=name \
--mode=go+u,go-w \
--directory=out/bin hello
default:
image:
name: cutover-lab-jenkins:latest
entrypoint: [""]
build:
stage: build
script:
- rm -rf out/bin && mkdir -p out/bin
- CGO_ENABLED=0 go build -buildvcs=false -ldflags='-buildid=' -o out/bin/hello ./cmd/hello
artifacts:
paths: [out/bin/hello]
test:
stage: test
needs: [build]
script: ["go test ./..."]
package:
stage: package
needs: [build, test]
script: ["bash scripts/package.sh"]
artifacts:
paths: [out/hello.tar]
#!/usr/bin/env bash
set -uo pipefail
run="${1:?usage: oracle.sh red|green}"
if [[ "$run" != "red" && "$run" != "green" ]]; then exit 2; fi
out="evidence/oracle/$run"
legacy="/workspace/evidence/artifacts/$run/legacy/hello.tar"
wrapped="/workspace/evidence/artifacts/$run/wrapped/hello.tar"
mkdir -p "$out"
docker compose exec -T jenkins sha256sum "$legacy" "$wrapped" | tee "$out/oracle.sha256"
legacy_digest="$(awk 'NR == 1 {print $1}' "$out/oracle.sha256")"
wrapped_digest="$(awk 'NR == 2 {print $1}' "$out/oracle.sha256")"
if [[ "$legacy_digest" == "$wrapped_digest" ]]; then
if [[ "$run" == "red" ]]; then
printf 'VERDICT=ERROR\nREASON=negative control did not create a difference\n' | tee "$out/status.txt"
exit 2
fi
printf 'VERDICT=PASS\nUNEXPLAINED_COUNT=0\n' | tee "$out/status.txt"
exit 0
fi
set +e
docker compose exec -T diffoscope diffoscope --no-default-limits --text=- \
"/evidence/artifacts/$run/legacy/hello.tar" \
"/evidence/artifacts/$run/wrapped/hello.tar" | tee "$out/diffoscope.txt"
diffoscope_status="${PIPESTATUS[0]}"
set -e
if [[ "$diffoscope_status" -eq 2 || "$run" == "green" ]]; then
printf 'VERDICT=FAIL\nUNEXPLAINED_COUNT=1\n' | tee "$out/status.txt"
exit 1
fi
hunks="$(grep -c '^@@' "$out/diffoscope.txt" || true)"
if [[ "$hunks" -eq 0 ]]; then exit 2; fi
printf 'hunk,section_or_comparator,cause,status\n' > "$out/difference-accounting.csv"
for ((i=1; i<=hunks; i++)); do
printf '%s,Go artifact comparator hunk %s,temporary negative control omitted -trimpath,explained\n' "$i" "$i" >> "$out/difference-accounting.csv"
done
printf 'VERDICT=EXPECTED_RED\nNAMED_DIFFERENCE_COUNT=%s\nUNEXPLAINED_COUNT=0\n' "$hunks" | tee "$out/status.txt"
Your oracle now catches one controlled build-input difference and explains every resulting hunk. Next, you will restore parity and require byte equality from the accepted wrapper.
Restore Parity and Run the Oracle Green
Your expected-red run proved that the SHA-256 oracle detects a missing build input. That gives the green run a trustworthy comparator.
The accepted wrapper must preserve every build input across Jenkins and GitLab. Its only changes are CI/CD syntax and runner.
In this step, get ready to:
- Restore the accepted GitLab wrapper.
- Produce passing green oracle evidence.
- Record the wrapper duration before merging its pull request.
Restore the accepted parity contract
The accepted wrapper delegates building to the same scripts used by the stand-in. This keeps the runner change isolated from product behavior.
- In ci/hello.yml, replace the temporary negative-control configuration with this accepted wrapper:
default:
image:
name: cutover-lab-jenkins:latest
entrypoint: [""]
build:
stage: build
script: ["bash scripts/build.sh"]
artifacts:
paths: [out/bin/hello]
test:
stage: test
needs:
- job: build
artifacts: true
script: ["go test ./..."]
package:
stage: package
needs:
- job: build
artifacts: true
- job: test
artifacts: false
script: ["bash scripts/package.sh"]
artifacts:
paths: [out/hello.tar]
What does this wrapper preserve?
- The default section keeps the locally built cutover-lab-jenkins:latest image.
- The build job delegates compilation to scripts/build.sh.
- The test job uses the same Go test command under version 1.27.1.
- The package job delegates archive creation to scripts/package.sh.
- The final artifact remains at out/hello.tar.
- Save ci/hello.yml.
- Confirm .gitlab-ci.yml still includes /ci/hello.yml.
- Confirm .gitlab-ci.yml still declares build, test, and package.
The accepted configuration now preserves the same three-stage contract used by the Jenkins stand-in.
Wrapper files do not match?
Check that .gitlab-ci.yml points to /ci/hello.yml. Check that ci/hello.yml no longer contains the negative-control build command.
Ask for help comparing the accepted wrapper with the negative control:
Byte equality depends on holding source-level build inputs constant. Recording those inputs makes the green verdict reproducible.
- Create evidence/held-equal-build-inputs.txt in the repository from earlier by pasting these contents:
CGO_ENABLED=0 | disables host C toolchain variation
-trimpath | removes runner-specific source paths
-buildvcs=false | omits repository metadata from the binary
-ldflags='-buildid=' | removes the linker build identifier
Why hold these inputs equal?
- CGO remains disabled so a host C toolchain cannot alter the binary.
- The source path remains trimmed so runner-specific directories cannot enter the output.
- Repository metadata remains omitted so the runner cannot add a different revision record.
- The linker build identifier remains empty so it cannot create a byte difference.
- Save evidence/held-equal-build-inputs.txt.
- Rebuild the local binary with the restored inputs by running this command:
bash scripts/build.sh
What does this check prove?
The shared build script removes the previous binary before compiling out/bin/hello with all four held-equal inputs. A successful return proves the accepted build path remains runnable.
You will return to the prompt with a rebuilt out/bin/hello file.
Build script does not finish?
Confirm the local Docker Compose lab remains running. Check that scripts/build.sh still contains all four held-equal inputs.
Ask for help diagnosing the shared build path:
Build the green comparison
A reproducible archive also needs stable metadata. Four GNU tar controls prevent runner-specific metadata from changing the packaged bytes.
- Create evidence/tar-normalization.txt in the repository from earlier by pasting these contents:
modification time | --mtime='UTC 2020-01-01' | prevents source or runner file times from changing archive bytes
ownership, uid and gid | --owner=0 --group=0 --numeric-owner | prevents runner account identity from changing archive headers
entry order | --sort=name | prevents filesystem traversal order from changing member order
file mode | --mode=go+u,go-w | prevents runner umask from changing archived permissions
What gets normalized?
- A fixed modification time removes source-file timestamp variation.
- Numeric ownership removes runner-account identity from archive headers.
- Sorted entry order removes filesystem traversal variation.
- A normalized file mode removes runner-specific permission variation.
- Save evidence/tar-normalization.txt.
- Package the rebuilt binary with all four normalization controls by running this command:
bash scripts/package.sh
What does this packaging check prove?
The shared packaging script recreates out/hello.tar from the rebuilt binary. Its fixed archive metadata makes byte equality meaningful across runners.
You will return to the prompt with a newly packaged out/hello.tar file.
Archive was not created?
Confirm out/bin/hello exists before packaging. Check that scripts/package.sh still reads from out/bin.
Ask for help tracing the archive input:
- Return to the build-hello job in Jenkins from the red run.
- Run build-hello against the exact commit stored in evidence/input-commit.txt.
- Save its artifact as evidence/artifacts/green/legacy/hello.tar.
- Return to the local GitLab project from the red run.
- Run the accepted wrapper against the exact commit stored in evidence/input-commit.txt.
- Save its artifact as evidence/artifacts/green/wrapped/hello.tar.
You now have two green artifacts built from the same recorded synthetic input.
βοΈ Everything matches
Your accepted wrapper files preserve the parity contract. Save every file before running the green oracle.
β§ I'd like to double check the full code
Compare each parity file with the complete reference below.
include:
- local: '/ci/hello.yml'
stages: [build, test, package]
This file includes the accepted wrapper and declares the three shared stages.
default:
image:
name: cutover-lab-jenkins:latest
entrypoint: [""]
build:
stage: build
script: ["bash scripts/build.sh"]
artifacts:
paths: [out/bin/hello]
test:
stage: test
needs:
- job: build
artifacts: true
script: ["go test ./..."]
package:
stage: package
needs:
- job: build
artifacts: true
- job: test
artifacts: false
script: ["bash scripts/package.sh"]
artifacts:
paths: [out/hello.tar]
This file restores the accepted image, scripts, dependencies, stages, and artifact path.
CGO_ENABLED=0 | disables host C toolchain variation
-trimpath | removes runner-specific source paths
-buildvcs=false | omits repository metadata from the binary
-ldflags='-buildid=' | removes the linker build identifier
This evidence file records the four source-level inputs held equal across both paths.
modification time | --mtime='UTC 2020-01-01' | prevents source or runner file times from changing archive bytes
ownership, uid and gid | --owner=0 --group=0 --numeric-owner | prevents runner account identity from changing archive headers
entry order | --sort=name | prevents filesystem traversal order from changing member order
file mode | --mode=go+u,go-w | prevents runner umask from changing archived permissions
This evidence file records the four archive properties normalized before comparison.
Before you run the oracle, do you expect the two green digests to match?
- Compare the green artifacts by running this command:
bash scripts/oracle.sh green
What does the green oracle record?
The oracle calculates both digests through the Jenkins container. It writes them to evidence/oracle/green/oracle.sha256.
Equal values produce VERDICT=PASS with UNEXPLAINED_COUNT=0 in evidence/oracle/green/status.txt.
You will see two equal digest lines followed by VERDICT=PASS and UNEXPLAINED_COUNT=0. That is the core proof that the runner changed while the artifact stayed equal.
Green digests are different?
Confirm both green artifacts came from the commit in evidence/input-commit.txt. Check that ci/hello.yml calls the shared build and package scripts.
Ask for help tracing the parity break:
Stop the clock and close the wrapper
The measured wrapper duration becomes a schedule input in the decision pack. Stopping the existing clock preserves a command-produced value.
- Stop the wrapper measurement by running this command:
go run ./cmd/assessment measure-stop --name wrapper
What does the measurement command save?
The assessment client reads the existing UTC start timestamp. It writes a positive whole-minute duration to evidence/measurements/wrapper-minutes.txt.
You will see a positive measured duration in the terminal. The same value now exists in evidence/measurements/wrapper-minutes.txt.
Measurement did not stop?
Confirm evidence/measurements/wrapper-start.txt still contains the timestamp created in the previous step. Keep the measurement name set to wrapper.
Ask for help checking the measurement files:
- Commit the accepted wrapper files on the existing ticket branch.
- Push the ticket branch to the private GitHub repository from earlier.
The existing wrapper pull request now shows the accepted parity files instead of the negative control.
- Return to the ticket-linked wrapper pull request.
- Confirm its Fixes reference points to the existing Linear issue.
- Merge the wrapper pull request.
The accepted wrapper is now part of the private delivery record.
Before the final check, do you expect the oracle and validator to agree on every parity constraint?
- Run the end-of-step verification with this command:
bash scripts/oracle.sh green && docker compose exec -T jenkins sh -c 'cd /workspace && go run ./cmd/assessment validate'
What does the final check cover?
The first command reruns the green digest comparison. The validator runs only after that comparison passes.
The validator checks the nine-job inventory, three-stage contract, shared artifact path, four normalization properties, and equal green digests.
You will see VERDICT=PASS, UNEXPLAINED_COUNT=0, stages=3, path-difference=0, Unknown=0, normalized-properties=4, and green-digests-equal=true. You have closed the cutover wrapper with reproducible evidence.
Final validation does not pass?
Confirm the two green artifacts still exist under evidence/artifacts/green. Check that the red evidence remains preserved in its separate directory.
Ask for help interpreting the failed parity check:
Your wrapper now preserves the original artifact while changing the delivery system. Next, you will turn this evidence into the decision pack and close the release.
Publish the Decision Pack and Close the Release
The green oracle has proved that the accepted wrapper preserves the artifact bytes. A credible sequencing recommendation still needs reproducible schedule inputs.
The redesign estimate must stay visibly separate from command-measured values. The private release must close with every evidence artifact intact.
In this step, get ready to:
- Measure the synthetic orphan-replacement exercise.
- Generate the projection and evidence-first decision pack.
- Audit the evidence before closing the GitHub release and Linear project.
Measure the orphan replacement and project the schedule
Schedule projections can hide assumptions inside polished dates. The assessment command keeps measured work separate from the redesign estimate.
- Start the orphan-replacement clock by running this command:
go run ./cmd/assessment measure-start --name orphan-replacement
What does this command do?
The command writes a UTC timestamp to evidence/measurements/orphan-replacement-start.txt. This timestamp becomes the start of the measured exercise.
- Use the orphan-plugin row in evidence/inventory.csv to assess the named stand-in-only capability.
- Complete the synthetic orphan-replacement exercise without changing the released wrapper evidence.
- Stop the measurement by running this command:
go run ./cmd/assessment measure-stop --name orphan-replacement
How is the duration recorded?
The command calculates elapsed minutes from the saved timestamp. It writes a positive whole number to evidence/measurements/orphan-replacement-minutes.txt.
A very short exercise still records one minute. This keeps every measured schedule input positive.
You should see orphan-replacement measured minutes: followed by a positive whole number.
Measurement did not finish?
Confirm you are running the command from the repository folder that contains go.mod. Check that the start timestamp exists before stopping the measurement.
Help me diagnose the orphan-replacement measurement.
- Record the value in evidence/measurements/wrapper-minutes.txt as measured wrapper minutes.
- Record the value in evidence/measurements/orphan-replacement-minutes.txt as measured orphan-replacement minutes.
- Choose the redesign input as redesign estimate minutes per job.
- Set the scenario date as YYYY-MM-DD.
- Set the notice window as whole-number working days.
- Set the engineering capacity as whole-number minutes per day.
- Generate the projection with those explicit inputs by running the command below:
Before you run it, which input do you expect the output to label as the sole estimate?
go run ./cmd/assessment project \
--measured-wrapper-minutes [[MEASURED_WRAPPER_MINUTES="measured wrapper minutes"]] \
--redesign-estimate-minutes-per-job [[REDESIGN_ESTIMATE_MINUTES_PER_JOB="redesign estimate minutes per job"]] \
--measured-orphan-replacement-minutes [[MEASURED_ORPHAN_REPLACEMENT_MINUTES="measured orphan-replacement minutes"]] \
--notice-date [[NOTICE_DATE="YYYY-MM-DD"]] \
--notice-working-days [[NOTICE_WORKING_DAYS="whole-number working days"]] \
--engineer-minutes-per-day [[ENGINEER_MINUTES_PER_DAY="whole-number minutes per day"]]
What does the projector produce?
- The first line projects wrapper completion from the measured wrapper duration and seven command-counted Wrapped jobs.
- The second line adds the redesign value labelled ESTIMATE.
- The third line adds the measured orphan replacement. It compares the resulting date with the explicit notice window.
- The command writes both readouts and evidence/escalation-letter.md. Each readout quotes the preserved equal green digests.
You should see exactly three projection lines in the terminal. The same three lines now exist in evidence/projection.txt.
What completes the decision pack?
The readouts sit beside the preserved red evidence and green evidence. The inventory definitions keep the result at three allowed states with Unknown at zero.
The held-equal input list records four build flags. The tar-normalization record gives a reason for each of four archive properties.
The three ADRs preserve the defeated alternatives and accepted tradeoffs. The fidelity note limits every claim to the synthetic rehearsal.
πββοΈ Projection command rejected an input?
Check that every minutes field contains a positive whole number. Confirm that the notice date follows the requested date format.
Help me fix the projection inputs.
Audit the complete decision pack
The final audit checks the evidence as one connected record. It also protects the decisions written before the first build.
Before you run the audit, do you expect the nine-job pack to pass without changing the pre-build baseline?
- Validate the complete evidence pack by running this command:
go run ./cmd/assessment validate
What does the validator check?
- The inventory must report one Migrated job and seven Wrapped jobs. It must also report one Orphaned job with Unknown at zero.
- The stage contract must remain build, test, and package.
- The artifact path must remain out/hello.tar. Both green digests must remain equal.
- The pack must contain four normalization properties and four held-equal inputs. It must also contain exactly eight documentation pages.
You should see inventory=9/9 Migrated=1 Wrapped=7 Orphaned=1 Unknown=0. The line also confirms three stages and eight documentation pages.
Validator stopped on missing evidence?
Use the validator message to identify the missing file or changed contract. Restore that artifact from the completed synthetic workflow before continuing.
Help me interpret the validator failure.
- Prove the pre-build decisions did not drift by running this command:
git diff --exit-code "$(cat evidence/design-baseline-commit.txt)" -- records/ADR-001-wrapper-first.md records/ADR-002-sha256-verdict.md records/ADR-003-measure-one-wrapper.md docs/architecture-at-a-glance.md evidence/plan.md
What does the baseline check prove?
The command compares the three ADRs against the commit recorded before any build. It also compares the architecture overview and 60-minute plan.
An exit status of zero means the named files still match that baseline. The recommendation cannot be rewritten to fit the result.
A successful baseline check returns silently. That quiet result proves the pre-build record remained unchanged.
Merge the final record and publish the release
The final Git commit holds the generated decision artifacts. Each delivery rail then points to that record.
- Record the relevant Linear issue ID as <ISSUE-ID>.
- Create the final ticket-linked branch by running this command:
git switch -c [[LINEAR_ISSUE_ID="<ISSUE-ID>"]]-decision-pack
Why put the issue ID in the branch?
The issue ID connects the branch to the relevant Linear work item. The final pull request then becomes part of the delivery trail.
- Commit the generated decision artifacts and push the branch by running these commands:
git add evidence/measurements/orphan-replacement-minutes.txt evidence/projection.txt evidence/readouts/principal-engineer.md evidence/readouts/program-manager.md evidence/escalation-letter.md
git commit -m "Publish decision pack"
git push -u origin [[LINEAR_ISSUE_ID="<ISSUE-ID>"]]-decision-pack
What do these commands publish?
- The first command stages the command-produced measurement and projection.
- The second command records the two readouts and escalation letter in the repository history.
- The third command pushes the ticket-linked branch to the private GitHub repository. It also sets the upstream branch.
You should see the new branch named in the push output. The private repository now contains the final decision-pack commit.
Branch push failed?
Confirm the repository still uses origin as its GitHub remote. Check that your signed-in account has write access to the private repository.
Help me fix the final branch push.
- Return to the private GitHub repository from earlier.
- Open the pull request page for the final decision-pack branch.
- Enter Publish the decision pack in the pull request title.
- Enter Fixes followed by <ISSUE-ID> in the pull request description.
- Create the final pull request.
- Review the changed files for the five generated decision artifacts.
- Merge the final pull request.
The final evidence is now reviewed and merged. Its linked Linear issue moves with the delivery record.
- Record the repository default branch as default branch name.
- Switch to the merged default branch and retrieve the merged commit by running these commands:
git switch [[DEFAULT_BRANCH="default branch name"]]
git pull
Why sync before tagging?
The local default branch must contain the merged decision-pack commit. The release tag then marks the reviewed delivery record.
- Create and push the annotated release tag by running these commands:
git tag -a v1.0.0 -m "Survive the cutover, then redesign"
git push origin v1.0.0
What does the annotated tag record?
The annotated v1.0.0 tag names the closed green cutover baseline. Pushing it makes that release point available to GitHub.
πββοΈ Tag push failed?
Confirm the final pull request is merged before tagging. Check that v1.0.0 does not already point to another commit.
Help me diagnose the release tag.
- Return to the main page of the private GitHub repository.
- Click Releases beside the file list.
- Click Draft a new release.
- Select v1.0.0 from the Choose a tag menu.
- Enter v1.0.0 in the Release title field.
- Enter Closed synthetic cutover record with preserved expected-red evidence and passing green digest parity. in the Describe this release field.
- Click Publish release.
Your private release now points to the reviewed v1.0.0 tag. The synthetic evidence remains available to authorized repository readers.
- Return to the Linear project from earlier.
- Confirm the project contains five linked issues.
- Set the project status to Completed.
Before the final check, do you expect the repository release and Linear project to show the same closed delivery state?
- Confirm the GitHub release page shows v1.0.0.
- Return to the repository main page.
- Confirm the repository still shows Private beside its name.
- Return to the Linear project.
- Confirm the project status shows Completed.
You now have a private tagged release and a completed five-issue Linear project. The disposable local lab remains separate from that closed delivery record.
That closes the rehearsal with measured schedule inputs and preserved oracle evidence. Another engineer can now reproduce the synthetic result without confusing cutover parity with redesign.
Secret mission
Break the Oracle with an Intentional Redesign
Change the synthetic programβs greeting on a separate ticket branch. Then prove that the resulting byte difference represents an accepted product change while the private `v1.0.0` release remains the green cutover baseline.
Clean Up Your Resources
Clean Up Your Resources
The local Docker Compose lab creates no ongoing software or service charge. Choose one cleanup option below.
Resources you used:
- Four local service containers defined in compose.yaml.
- One cutover-lab Compose network.
- Five named volumes holding disposable service state.
- The locally built cutover-lab-jenkins:latest stand-in image.
The private GitHub repository preserves the v1.0.0 green baseline. The completed Linear project preserves the delivery trail.
Keep everything running
No action needed. Choose this if you are still reviewing the lab or extending the redesign branch.
- Leave the local Compose stack running.
- Keep the private v1.0.0 release as the immutable green cutover baseline.
- Keep the redesign branch separate from v1.0.0.
- Keep the completed Linear project as the closed delivery trail.
Pause - I'll come back to this later
Shut down the running services to release workstation memory. The containers plus their local state remain available for later.
- Pause the lab by running this command from the repository that contains compose.yaml:
docker compose stop
What Does This Command Do?
This pauses the Compose services. It preserves the existing runtime state.
The private delivery records stay untouched.
You should see Compose report each lab service as stopped. The lab is paused with its local evidence ready for another session.
- Keep the private v1.0.0 release available as the accepted green baseline.
- Keep the completed Linear project available as the delivery record.
Delete - I don't want to use this again
Deleting the local runtime permanently removes its container state. Your private release stays separate as the closed delivery record.
- Remove the disposable local runtime by running this command from the repository that contains compose.yaml:
docker compose down -v --rmi local
What Gets Removed?
This removes the Compose service containers. It also removes the cutover-lab network.
The -v option removes the named volumes. The --rmi local option removes the locally built stand-in image.
You should see Compose report the disposable lab resources as removed. You have cleared the local lab while preserving the released evidence.
- Keep the private v1.0.0 release as the unmodified green cutover baseline.
- Keep the completed Linear project as the closed delivery trail.
- Keep the redesign branch separate from the released baseline.
Nice Work!
Nice Work!
Outstanding work! You closed a synthetic cutover rehearsal with its green evidence intact before testing redesign under a separate acceptance boundary.
You've learned how to:
- Preserve behavior through a wrapper-first cutover from Jenkins to GitLab. Prove artifact parity with equal SHA-256 digests from held-equal build inputs.
- Use an expected-red negative control to prove the same-input oracle can detect a known build difference. Give every Diffoscope hunk a named cause.
- Generate command-produced projections from a 9/9 three-state inventory. Publish evidence-first readouts for the cutover recommendation. Close the delivery record through the private GitHub release at v1.0.0. Complete the Linear project.
- Secret Mission: Changed the synthetic product output on a separate ticket branch. Defined a product-level acceptance test for the redesign. Kept v1.0.0 as the immutable green cutover baseline.
Ready to quiz yourself?