Build Policy-as-Code Promotion Gates

Build a Go promotion gate with policy checks, approvals, and evidence.

Introduction

30 Second Summary

A tight deadline can make skipping a checkpoint feel harmless. When nobody can show the route or proof behind a decision, speed turns into an argument.

In this project, you will build a five-rung delivery road that turns policy as code into command-backed promotion decisions. As principal engineer, you will own the map, command, pipeline contract, and evidence quality without claiming ownership of production approver identity.

What You'll Build

You will demo a five-rung delivery road where promote prints the approved policy table while refusing skipped rungs, weak evidence, unsafe data placement, or broken approval separation.

By the end of this project, you'll have:

  • A one-rung promotion route you can defend from dev through production. Its generated table shows the gate, evidence, approvers, scans, and data class at every boundary.
  • A set of command-backed proofs showing four valid requests passing. Four mandatory refusal cases expose the unsafe paths your policy closes.
  • A durable handover another operator can use with a change board or an ISSO. The tagged release carries one evidence set through both readouts, the fidelity note, eight documentation pages, and an honest CI rehearsal status.
  • Secret Mission: Prove an item-level missing-evidence refusal without changing the released source.

Are there any prerequisites?

The completed day-zero kit is required. It already contains the private delivery repository, planning project, integration, local GitLab Community Edition instance, and GitLab Runner used throughout this project.

Before We Start

This is the moment to define the delivery system you are building and the evidence standard it must meet. Your boundary covers road.yaml, the promote command, the pipeline contract, and evidence quality. Production approval identity stays with the change system of record.

Observe the Missing Policy Engine

A delivery system is only governable when its policy boundary can be located through evidence. The unchanged starter bundle gives you a controlled way to find that boundary.

As principal engineer, you own the map, the command contract, and the evidence quality. In this step, you will test the supplied policy as code path before authoring any enforcement logic.

In this step, get ready to:
  • Check the day-zero toolchain and pull the required container image.
  • Create the implementation branch and protect all thirteen supplied files.
  • Download the Go dependencies and capture the planned baseline failure.
Check the day-zero toolchain

The day-zero Bash workstation already has Git, Go `1.27`, and Docker Engine. These checks confirm that the active shell can reach each tool required by the supplied project.

  • Check Git, Go, and Docker Engine by running these commands:
git --version
go version
docker version

What should these checks show?

  • Confirm the Git output begins with git version.
  • Confirm the Go output begins with go version go1.27.
  • Confirm the Docker output contains both Client and Server sections.

Is a tool check failing?

A missing Git or Go result means the day-zero tool is unavailable in your current Bash environment. A missing Docker Server section means Docker Engine is unreachable.

Resolve the day-zero environment before continuing. Help me diagnose which day-zero tool is unavailable from these version-check results.

The supplied pipeline uses the golang:1.27-alpine container image. Pulling it now separates image availability from the policy failure you will test later.

  • Make the required Go image available by running this command:
docker pull golang:1.27-alpine

What does this command prove?

Docker retrieves the image layers required by the supplied pipeline contract. Expect the first pull to take a few minutes while those layers download.

  • Look for the layer status in the output.
  • Look for the image digest in the output.
  • Confirm the final status says the image was downloaded or is up to date.

Did the image pull fail?

Check that the Docker Server section appeared in the earlier version output. A pull failure can also point to a network or registry problem.

Retry after Docker Engine is reachable. Help me diagnose why Docker cannot pull the required Go image.

Protect the supplied boundary

The supplied files define the types, command path, table renderer, pipeline contract, and test requests. Keeping them unchanged ensures that later evidence measures only the three learner-authored policy files.

  • Switch back to the Bash terminal in the private delivery repository from day zero.
  • Create the implementation branch and switch to it by running this command:
git switch -c implementation

What does this command do?

The -c option creates a branch from the current commit. Git then switches the working tree to implementation.

Does the implementation branch already exist?

Stop if Git reports that the branch name already exists. Confirm that the existing branch belongs to this exercise before switching to it.

Avoid force-creating the branch because that could reset existing work. Help me inspect an existing implementation branch safely.

The immutable file boundary

These thirteen paths make up the supplied starter bundle:

  • go.mod.
  • go.sum.
  • .gitattributes.
  • .gitlab-ci.yml.
  • internal/road/road.go.
  • internal/road/table.go.
  • cmd/promote/main.go.
  • requests/1-dev-to-integration.json.
  • requests/2-integration-to-test.json.
  • requests/3-test-to-staging.json.
  • requests/4-staging-to-production.json.
  • bad/skip-test.json.
  • bad/self-approved-production.json.
  • Compare the starter bundle with the fixed path list in your repository file tree.
  • Stop the exercise if any supplied path is missing or different.
  • Remove write permission from all thirteen supplied files by running this command:
chmod a-w \
  go.mod \
  go.sum \
  .gitattributes \
  .gitlab-ci.yml \
  internal/road/road.go \
  internal/road/table.go \
  cmd/promote/main.go \
  requests/1-dev-to-integration.json \
  requests/2-integration-to-test.json \
  requests/3-test-to-staging.json \
  requests/4-staging-to-production.json \
  bad/skip-test.json \
  bad/self-approved-production.json

Why make the files read-only?

The a-w mode removes write permission for the file owner, group, and other users. This creates a local guardrail around the supplied boundary.

That boundary is now protected. Any policy behavior you add later has a clear authorship trail.

Could not change a file mode?

Confirm that your Bash terminal is inside the private delivery repository. Check that the reported path matches one of the thirteen supplied paths exactly.

Stop if a file is missing because the starter bundle must stay complete. Help me diagnose a file-permission failure in the supplied starter bundle.

Observe the planned build failure

The module dependencies must be available before the baseline build can reveal the enforcement boundary. Downloading them first keeps dependency problems separate from the intended compiler result.

  • Download the dependencies declared by the supplied module files by running this command:
go mod download

What should the download show?

A successful dependency download writes no standard output. Your terminal returns to the prompt with no error.

Any output needs attention before the baseline build because it can identify a dependency, network, checksum, or Go-version problem.

Did the dependency download print an error?

Read the named dependency or checksum before retrying. Confirm that the earlier Go output began with go version go1.27.

Keep the supplied go.mod and go.sum unchanged. Help me diagnose the dependency download output without editing the supplied module files.

Before you run the baseline build, what do you expect the unchanged starter bundle to reveal about its policy boundary?

  • Build every supplied Go package by running this command:
go build ./...

This failure is the evidence

You should see build errors naming road.Lint and road.Check as undefined. The supplied command reaches the exact boundary where policy enforcement must be added.

A successful build means authored policy files already exist. A different error means the starter bundle or repository location needs correction.

  • Copy the complete baseline build output into the evidence record for the implementation ticket.
  • Label the captured result as the planned undefined-symbol baseline failure.
  • Leave road.yaml, internal/road/lint.go, and internal/road/check.go absent.

Did you get a different build result?

Stop before continuing. Confirm that all thirteen supplied paths match the fixed inventory.

A pass means one or more learner-authored policy files already exist. Help me compare an unexpected baseline build result with the planned missing-policy failure.

You have preserved the starter boundary and captured the missing enforcement point. Next, you will author the promotion road and the two validators that close it.

Build the Promotion Road

The baseline build exposed the missing enforcement boundary in the delivery system I own. I can now close that gap without changing the supplied command or pipeline.

Policy as code puts delivery rules in version-controlled files. One ordered YAML map defines the permitted promotion road.

Two Go validators enforce that map. This keeps every policy decision out of the pipeline shell.

In this step, get ready to:
  • Create the ordered five-rung promotion map.
  • Implement both Go validators.
  • Verify the build, lint result, and change-board table.
Define the five-rung road

The order inside road.yaml defines the only permitted promotion path. Each rung names its gate, evidence, approvers, scans, and data class.

The thirteen supplied files remain read-only. This map is the first of the three files I own as the principal engineer.

  • Create road.yaml beside go.mod by matching the complete file in the double-check tab:

✔️ Awesome, I've got everything!

Your promotion road contains five ordered rungs. Keep road.yaml open for the visual check below.

ⓧ I'd like to double check the full code

rungs:
  - name: dev
    gate: automatic
    evidence: [test-record]
    approvers: []
    scans: [unit-tests]
    data_class: synthetic

  - name: integration
    gate: scan
    evidence: [test-record, sbom, scan-report]
    approvers: []
    scans: [sast, dependency, secrets, image]
    data_class: synthetic

  - name: test
    gate: manual
    evidence: [test-record, sbom, scan-report, dast-report]
    approvers:
      - role: qa-lead
        person: qa-ben
    scans: [sast, dependency, secrets, image, dast]
    data_class: masked

  - name: staging
    gate: manual
    evidence: [test-record, sbom, scan-report, dast-report]
    approvers:
      - role: release-manager
        person: rm-chen
    scans: [sast, dependency, secrets, image, dast]
    data_class: real

  - name: production
    gate: dual
    evidence: [test-record, sbom, scan-report, dast-report, change-request-id]
    approvers:
      - role: system-owner
        person: so-dara
      - role: change-board-chair
        person: cb-eli
    scans: [sast, dependency, secrets, image, dast]
    data_class: real

How does this map govern delivery?

The rung sequence fixes the route from dev through production. The checker uses this order to reject skipped boundaries.

Each target rung declares the evidence required for entry. Production declares two distinct fictional approvers.

  • Save road.yaml.
  • Count the entries beginning with name in your editor.

You should see exactly five rung names. They appear in the order dev, integration, test, staging, and production.

Implement the policy validators

The map needs its own validator before its claims can become evidence. Lint checks whether the road satisfies the delivery rules.

Promotion requests need a separate enforcement boundary. Check validates each request against the ordered map.

  • Create internal/road/lint.go by matching the complete validator in the double-check tab:

✔️ Awesome, I've got everything!

Your map validator now covers gates, data classes, approver counts, and the real-data boundary.

ⓧ I'd like to double check the full code

package road

import "fmt"

func Lint(m Map) []string {
	validGates := map[string]bool{
		"automatic": true,
		"scan":      true,
		"manual":    true,
		"dual":      true,
	}
	validDataClasses := map[string]bool{
		"synthetic": true,
		"masked":    true,
		"real":      true,
	}

	problems := []string{}
	stagingIndex := -1
	for index, rung := range m.Rungs {
		if rung.Name == "staging" {
			stagingIndex = index
			break
		}
	}
	if stagingIndex == -1 {
		problems = append(problems, "no rung named staging")
	}

	for index, rung := range m.Rungs {
		if !validGates[rung.Gate] {
			problems = append(problems, fmt.Sprintf("rung %s: unknown gate %s", rung.Name, rung.Gate))
		}
		if !validDataClasses[rung.DataClass] {
			problems = append(problems, fmt.Sprintf("rung %s: unknown data class %s", rung.Name, rung.DataClass))
		}
		if (rung.Gate == "manual" || rung.Gate == "dual") && len(rung.Approvers) == 0 {
			problems = append(problems, fmt.Sprintf("rung %s: gate %s names no approvers", rung.Name, rung.Gate))
		}
		if rung.Gate == "dual" && len(unique(people(rung.Approvers))) < 2 {
			problems = append(problems, fmt.Sprintf("rung %s: gate dual names fewer than two different people", rung.Name))
		}
		if stagingIndex >= 0 && rung.DataClass == "real" && index < stagingIndex {
			problems = append(problems, fmt.Sprintf("rung %s: real data below staging", rung.Name))
		}
	}

	return problems
}

What does this validator enforce?

  • The validGates map limits every rung to one supported gate.
  • The validDataClasses map limits every rung to one supported data class.
  • The stagingIndex boundary prevents real data from appearing below staging.
  • Manual gates require named approvers.
  • Dual gates require two different people.
  • Save internal/road/lint.go.
  • Confirm the file defines Lint, validGates, validDataClasses, and stagingIndex.

You should see one Lint function ending with return problems.

  • Create internal/road/check.go by matching the complete request validator in the double-check tab:

✔️ Awesome, I've got everything!

Your request validator now checks every promotion against its target rung.

ⓧ I'd like to double check the full code

package road

import (
	"fmt"
	"regexp"
)

var digestPattern = regexp.MustCompile(`^sha256:[0-9a-fA-F]{64}$`)

func Check(m Map, req Request) []string {
	problems := []string{}
	if !digestPattern.MatchString(req.Digest) {
		problems = append(problems, "invalid digest: want sha256 and 64 hex characters")
	}

	fromIndex := -1
	toIndex := -1
	for index, rung := range m.Rungs {
		if rung.Name == req.From {
			fromIndex = index
		}
		if rung.Name == req.To {
			toIndex = index
		}
	}
	if fromIndex == -1 || toIndex == -1 {
		problems = append(problems, fmt.Sprintf("unknown rung in %s -> %s", req.From, req.To))
		return problems
	}

	if toIndex != fromIndex+1 {
		if toIndex > fromIndex+1 {
			problems = append(problems, fmt.Sprintf("skips rung %s: a promotion moves one rung at a time", m.Rungs[fromIndex+1].Name))
		} else {
			problems = append(problems, fmt.Sprintf("move %s -> %s is not one rung forward: a promotion moves one rung at a time", req.From, req.To))
		}
	}

	target := m.Rungs[toIndex]
	providedEvidence := make(map[string]bool, len(req.Evidence))
	for _, item := range req.Evidence {
		providedEvidence[item] = true
	}
	for _, item := range target.Evidence {
		if !providedEvidence[item] {
			problems = append(problems, fmt.Sprintf("missing evidence for %s: %s", target.Name, item))
		}
	}

	for _, required := range target.Approvers {
		matches := []Approval{}
		for _, approval := range req.Approvals {
			if approval.Role == required.Role {
				matches = append(matches, approval)
			}
		}
		if len(matches) == 0 {
			problems = append(problems, fmt.Sprintf("no approval for %s as %s (%s)", target.Name, required.Role, required.Person))
			continue
		}

		approvedByExpectedPerson := false
		for _, approval := range matches {
			if approval.Person == required.Person {
				approvedByExpectedPerson = true
				if approval.Person == req.StartedBy {
					problems = append(problems, fmt.Sprintf("approval as %s by %s, who started the pipeline", required.Role, approval.Person))
				}
				break
			}
		}
		if !approvedByExpectedPerson {
			problems = append(problems, fmt.Sprintf("approval as %s by %s, want %s", required.Role, matches[0].Person, required.Person))
		}
	}

	return problems
}

What does this validator enforce?

  • The digestPattern check requires a fixed digest format.
  • The rung indexes permit exactly one forward movement.
  • The evidence loop checks every item required by the target rung.
  • The approval loop requires the expected fictional person.
  • The same loop rejects self-approval.
  • Save internal/road/check.go.
  • Confirm the file defines digestPattern plus Check.

You should see Check return one accumulated problems list. That list gives the supplied command a consistent refusal path.

Verify the command path

The baseline failed because the enforcement functions were absent. The same build now tests whether the supplied command compiles against both validators.

Before you run the build, do you expect the saved undefined-symbol failure to remain?

  • Compile every package by running this command:
go build ./...

What does this build prove?

The command compiles the supplied command path with the two authored validators. A silent result proves the missing symbols now resolve.

You should see no output. The command exits successfully.

Build still names undefined functions?

Check that lint.go defines Lint inside package road.

Confirm both authored files are saved under internal/road. Help me diagnose why the Go build still cannot find Lint or Check.

The successful build proves the code compiles. The lint operation now tests the policy map itself.

  • Validate the five-rung map by running this command:
go run ./cmd/promote lint road.yaml

What does this lint prove?

The supplied command loads road.yaml before calling Lint. A pass means the authored map satisfies every implemented rule.

You should see PASS lint: 5 rungs.

Seeing a lint refusal?

Compare road.yaml with the complete map. Pay close attention to the indentation beneath approvers.

A different refusal identifies the map rule that needs attention. Help me trace my lint refusal to the matching road.yaml field.

Before you render the final table, do you expect the map to produce one row for every rung?

  • Render the change-board view by running this command:
go run ./cmd/promote table road.yaml

What does this table command prove?

The supplied renderer reads the same map used by both validators. This makes the human-readable view traceable to the executable policy.

You should see this approved six-column table with one row for every rung:

NAME         GATE       EVIDENCE                                            APPROVERS                                          SCANS                                    DATA_CLASS
dev          automatic  test-record                                        -                                                  unit-tests                               synthetic
integration  scan       test-record,sbom,scan-report                       -                                                  sast,dependency,secrets,image            synthetic
test         manual     test-record,sbom,scan-report,dast-report           qa-lead=qa-ben                                     sast,dependency,secrets,image,dast       masked
staging      manual     test-record,sbom,scan-report,dast-report           release-manager=rm-chen                            sast,dependency,secrets,image,dast       real
production   dual       test-record,sbom,scan-report,dast-report,change-request-id  system-owner=so-dara,change-board-chair=cb-eli  sast,dependency,secrets,image,dast       real

How should I read the table?

  • The NAME column preserves the approved rung order.
  • The GATE column shows where automatic movement becomes a named human decision.
  • The EVIDENCE column grows as a request approaches production.
  • The DATA_CLASS column keeps real data at staging or above.

Table differs from the approved road?

A missing row usually means a rung is absent from road.yaml. A changed cell means the matching map value differs from the approved road.

Keep the supplied table renderer unchanged. Help me compare my rendered table with the approved five-rung output.

That closes the missing enforcement boundary. Your map now builds, passes lint, and renders one command-backed view for the next proof stage.

Prove Passes and Refusals

Your promotion road now builds successfully. Its lint result matches the approved five-rung map.

As principal engineer, I cannot treat delivery speed as evidence. I need command-backed passes plus command-backed refusals before I merge this policy.

In this step, get ready to:
  • Capture passing output for all four one-rung promotions.
  • Prove four mandatory refusal cases without changing the supplied files.
  • Complete the evidence-backed implementation merge.
Run every passing request

A passing request proves that the target rung received its required evidence. Manual gates also prove that the expected fictional approver is present.

Before you run the requests, predict which result should repeat across all four rung boundaries.

  • Run the four supplied requests in the terminal from earlier by entering these commands:
go run ./cmd/promote check road.yaml requests/1-dev-to-integration.json
go run ./cmd/promote check road.yaml requests/2-integration-to-test.json
go run ./cmd/promote check road.yaml requests/3-test-to-staging.json
go run ./cmd/promote check road.yaml requests/4-staging-to-production.json

What do these passes prove?

  • The first request prints PASS check: dev -> integration.
  • The second request prints PASS check: integration -> test.
  • The third request prints PASS check: test -> staging.
  • The fourth request prints PASS check: staging -> production.

Together, these lines prove that each supplied request moves exactly one rung through the ordered map.

Strong start. Every allowed one-rung promotion now has command-backed evidence.

Did a supplied request get refused?

Confirm that you are still inside the private delivery repository. Check that the request path matches the command exactly.

A refusal here means the request fixture or one of the three learner-authored files differs from the approved project state.

Help me diagnose an unexpected refusal from a supplied promotion request.

Prove every mandatory refusal

A fail-closed validator earns trust by rejecting unsafe inputs with named reasons. Two supplied fixtures test request checks. Two temporary map copies test lint rules without changing road.yaml.

  • Create the two temporary map copies under bad/ by running these commands:
cp road.yaml bad/no-staging-approvers.yaml
cp road.yaml bad/real-data-on-test.yaml

Why use temporary copies?

The cp utility copies the approved map into separate target files. The original road.yaml remains the source for every passing request.

Each temporary file isolates one lint violation. That keeps every refusal tied to one policy boundary.

  • Confirm that bad/no-staging-approvers.yaml appears under bad/ in your editor's file tree.
  • Confirm that bad/real-data-on-test.yaml appears under bad/ in your editor's file tree.
  • Open bad/no-staging-approvers.yaml in your editor.
  • Replace the three-line approvers block under staging with approvers: [].
  • Save bad/no-staging-approvers.yaml.

Before you lint this copy, predict how the map validator handles a manual gate with no named approvers.

  • Test the altered staging gate by running this command:
go run ./cmd/promote lint bad/no-staging-approvers.yaml

The manual gate is refused

You should see REFUSED: rung staging: gate manual names no approvers.

This proves that a manual gate cannot exist without a named approver.

  • Open bad/real-data-on-test.yaml in your editor.
  • Replace data_class: masked under test with data_class: real.
  • Save bad/real-data-on-test.yaml.

Before you run the remaining cases, predict which policy boundary each fixture challenges.

  • Test the one-rung rule, the data boundary, and production separation of duties by running these commands:
go run ./cmd/promote check road.yaml bad/skip-test.json
go run ./cmd/promote lint bad/real-data-on-test.yaml
go run ./cmd/promote check road.yaml bad/self-approved-production.json

Three more boundaries hold

  • The skipped rung prints REFUSED: skips rung test: a promotion moves one rung at a time.
  • The altered data class prints REFUSED: rung test: real data below staging.
  • The self-approval case prints REFUSED: approval as system-owner by so-dara, who started the pipeline.
  • The same production case also prints REFUSED: no approval for production as change-board-chair (cb-eli).

The two production lines prove that self-approval detection does not hide a missing required role.

Does a refusal line differ?

Check that each temporary edit sits under the named rung. Confirm that no other line changed in either map copy.

Help me diagnose a different refusal from the mandatory proof suite.

That is the defensive proof complete. Four refusal cases now produce five named policy lines.

  • Remove both temporary map copies by running this command:
rm -f bad/no-staging-approvers.yaml bad/real-data-on-test.yaml

What does this cleanup prove?

The command removes only the two temporary lint fixtures. Successful cleanup produces no output.

The supplied files plus the three learner-authored files remain unchanged.

  • Confirm that bad/no-staging-approvers.yaml no longer appears in your editor's file tree.
  • Confirm that bad/real-data-on-test.yaml no longer appears in your editor's file tree.
Ship the implementation proof

The private GitHub pull request becomes the durable review record. Its evidence must come from the same map plus the same command path.

  • Create the implementation pull request from your current implementation branch into the repository's default branch.
  • Paste one pull request description containing the command-generated table, the four pass lines, the five refusal lines, and the planning ticket URL.
  • Inspect the pull request diff for the complete implementation boundary.
  • Confirm that the diff contains road.yaml, internal/road/lint.go, and internal/road/check.go as the only learner-authored files.
  • Confirm that none of the thirteen supplied files appears as modified.
  • Confirm that neither temporary lint fixture appears in the pull request.

Before you merge, predict the final state of the planning ticket linked to this implementation proof.

  • Merge the implementation pull request.
  • Open the linked planning ticket in Linear to confirm its status is closed.

You should see the implementation pull request marked as merged. The linked planning ticket should show a closed state.

Your promotion policy now has evidence for every allowed move plus every mandatory boundary. Next, you will rehearse the unchanged pipeline and turn this proof into a durable handover.

Rehearse and Ship the Handover

Your merged implementation now carries four passing requests plus four mandatory refusals. The generated table ties those results back to one ordered promotion road.

Local proof is easy to lose after the terminal closes. This step turns the evidence into durable readouts, operating documentation, and a tagged handover.

As principal engineer, you own the quality of the policy-as-code evidence. Production approval identities remain with the change system of record.

In this step, get ready to:
  • Rehearse the unchanged four-job pipeline in local GitLab Community Edition.
  • Create two audience readouts from one evidence set.
  • Ship the complete handover as a private tagged release.
Rehearse the unchanged pipeline

The local GitLab Community Edition exercise tests the CI/CD orchestration around the same Go command path. Production identity remains outside this rehearsal.

  • Switch back to the local GitLab Community Edition instance from your day-zero kit.
  • Create a temporary exercise repository by using the repository workflow established during day zero.
  • Push the merged private delivery repository into the exercise repository without changing .gitlab-ci.yml.
  • Confirm to-integration starts automatically in the pipeline view.

What does the rehearsal prove?

The four jobs exercise the unchanged pipeline contract through the same map and Go command used locally.

  • The to-integration job checks the promotion from dev to integration.
  • The to-test job checks the promotion from integration to test.
  • The to-staging job checks the promotion from test to staging.
  • The to-production job checks the promotion from staging to production.
  • Every job runs the supplied lint invocation before its corresponding check invocation.
  • Shell remains glue because every policy decision stays inside Go.
  • Start to-test from the pipeline view after the automatic job succeeds.
  • Start to-staging after the test promotion succeeds.
  • Start to-production after the staging promotion succeeds.
  • Inspect each job log for the lint invocation followed by the corresponding check invocation.

The rehearsal status depends on captured logs. Choose the outcome that matches what you actually completed inside the allotted window.

✔️ Four job logs succeeded

  • Capture the successful log for to-integration.
  • Capture the successful log for to-test.
  • Capture the successful log for to-staging.
  • Capture the successful log for to-production.
  • Record the rehearsal as completed with four successful job logs.

Those four logs prove that the unchanged pipeline orchestrated every one-rung promotion.

ⓧ Four successful logs do not exist

A partial run cannot support a completed rehearsal claim. The honest status preserves the boundary between expected behavior and observed evidence.

  • Stop the rehearsal when its allotted window ends.
  • Record NOT REHEARSED as the literal rehearsal status.
  • Keep the available logs as partial context.
  • Leave the remaining jobs out of the project hour.

Pipeline jobs not completing?

  • Confirm the GitLab Runner from day zero is available to the exercise repository.
  • Compare the job log sequence with the unchanged pipeline contract.
  • Keep the status at NOT REHEARSED unless all four successful logs exist.

Ask for help with the local runner or pipeline log:

Freeze one evidence set

A single evidence set keeps the change-board story aligned with the ISSO story. Every claim in both readouts must point back to the same captured proof.

  • Start the evidence set with the command-generated five-rung table.
  • Add the four captured pass outputs.
  • Add the four mandatory refusal cases.
  • Add the merged implementation pull request.
  • Record the honest rehearsal status.

What belongs in the two readouts?

  • The change-board readout explains the policy delta.
  • The change-board readout identifies the rung owners.
  • The change-board readout includes the command-generated table.
  • The change-board readout summarizes the evidence, refusals, and decision.
  • The ISSO readout explains the named scans.
  • The ISSO readout explains the data-class boundary.
  • The ISSO readout explains separation of duties.
  • The ISSO readout traces CM-3, CM-5, AC-5, SA-3(2), and SSDF PO.5.
  • Both readouts report the adoption fraction as 4/4.
  • Both readouts use the same rehearsal status.
  • Both readouts record the limits and lessons learned.
  • Create readouts/change-board.md inside the private delivery repository from the evidence set.
  • Create readouts/isso.md inside the private delivery repository from the same evidence set.
  • Compare both readouts against the captured evidence.

You should see 4/4 in both readouts. You should also see NOT REHEARSED in both files unless four successful job logs were captured.

The fidelity note separates demonstrated behavior from the boundaries of this exercise. It prevents fixture approvals or named scans from being presented as production evidence.

What belongs in the fidelity note?

  • The compiled CLI returns 0 for a pass.
  • The compiled CLI returns 1 for a policy refusal.
  • The compiled CLI returns 2 for a usage or decode error.
  • The go run wrapper does not preserve the compiled program's exact exit status.
  • The guide accepts zero versus nonzero plus the printed result class.
  • A built binary is required to observe exact exit statuses 1 and 2.
  • Exercise approvals come from request fixtures.
  • Production approvals come from the change system of record.
  • Scans are named without being run.
  • The digest is fixed.
  • The approvers are fictional.
  • Protected environments are absent.
  • No data moved between environments.
  • The rehearsal status remains NOT REHEARSED unless four successful logs exist.
  • Add a standalone fidelity note to the handover.
  • Review the fidelity note against every boundary listed above.

The evidence package now has one policy story for decision makers plus one security story for the ISSO. The remaining pages make that story operable by the next engineer.

  • Create docs/architecture-at-a-glance.md with the approved topology plus the one-map command path.
  • Create docs/quickstart.md with the named build, lint, table, and request-check sequence.
  • Confirm both files appear under docs/ in the repository file tree.

You should see the architecture page plus the quickstart page listed under docs/.

  • Create docs/operate-it.md with the normal one-rung promotion workflow.
  • Create docs/three-failure-runbook.md with recovery guidance for build failures, lint refusals, and request refusals.
  • Confirm both files appear under docs/ in the repository file tree.

You should now see four handover pages under docs/.

Halfway through the documentation set

The first four pages explain the topology, the first run, normal operation, and failure recovery. The next four pages preserve the decisions, command design, configuration contract, and limits.

  • Create docs/decision-index.md with the four reviewed architecture decisions.
  • Create docs/agent-architecture.md with the map loader, validators, table renderer, and command flow.
  • Confirm both files appear under docs/ in the repository file tree.

You should now see six handover pages under docs/.

  • Create docs/configuration-reference.md with the rung, gate, evidence, approver, scan, and data-class fields.
  • Create docs/limits.md with the boundaries recorded in the fidelity note.
  • Confirm all eight required files appear under docs/ in the repository file tree.

You should see exactly eight named documentation pages. The two readouts plus the fidelity note complete the handover package.

Merge and publish the handover

The documentation pull request creates the durable review boundary for the handover. Its merged commit becomes the source for the private release.

  • Open a documentation pull request in the private delivery repository through the workflow used earlier.
  • Confirm the pull request adds the two readouts, the fidelity note, and the eight required documentation pages.
  • Confirm the pull request leaves all thirteen supplied files unchanged.
  • Confirm the pull request leaves road.yaml, internal/road/lint.go, and internal/road/check.go unchanged.
  • Merge the documentation pull request.
  • Record the merged documentation commit as the release commit in the evidence set.
  • Create tag v1.0.0 at the release commit.
  • Publish a private release from tag v1.0.0.

Before you inspect the release, predict whether every published rehearsal claim is backed by four successful job logs.

  • Inspect the private v1.0.0 release.
  • Confirm the release commit matches the commit recorded in the evidence set.
  • Confirm both readouts report the adoption fraction as 4/4.
  • Confirm both readouts show NOT REHEARSED unless four successful job logs exist.
  • Confirm the release contains the two readouts.
  • Confirm the release contains the fidelity note.
  • Confirm the release contains all eight documentation pages.

The release should contain one traceable evidence set plus two audience-specific readouts. Its rehearsal claim should match the logs that actually exist.

Strong finish. You can now defend each rung without claiming ownership of production approver identity.

The handover adds evidence and documentation around the implementation. Use this checkpoint to confirm the three learner-authored policy files still match the proven source.

✔️ Awesome, I've got everything!

Your policy source remains unchanged. The private release now carries the evidence needed to operate and review it.

ⓧ I'd like to double check the full code

package road

import (
	"fmt"
	"regexp"
)

var digestPattern = regexp.MustCompile(`^sha256:[0-9a-fA-F]{64}$`)

func Check(m Map, req Request) []string {
	problems := []string{}
	if !digestPattern.MatchString(req.Digest) {
		problems = append(problems, "invalid digest: want sha256 and 64 hex characters")
	}

	fromIndex := -1
	toIndex := -1
	for index, rung := range m.Rungs {
		if rung.Name == req.From {
			fromIndex = index
		}
		if rung.Name == req.To {
			toIndex = index
		}
	}
	if fromIndex == -1 || toIndex == -1 {
		problems = append(problems, fmt.Sprintf("unknown rung in %s -> %s", req.From, req.To))
		return problems
	}

	if toIndex != fromIndex+1 {
		if toIndex > fromIndex+1 {
			problems = append(problems, fmt.Sprintf("skips rung %s: a promotion moves one rung at a time", m.Rungs[fromIndex+1].Name))
		} else {
			problems = append(problems, fmt.Sprintf("move %s -> %s is not one rung forward: a promotion moves one rung at a time", req.From, req.To))
		}
	}

	target := m.Rungs[toIndex]
	providedEvidence := make(map[string]bool, len(req.Evidence))
	for _, item := range req.Evidence {
		providedEvidence[item] = true
	}
	for _, item := range target.Evidence {
		if !providedEvidence[item] {
			problems = append(problems, fmt.Sprintf("missing evidence for %s: %s", target.Name, item))
		}
	}

	for _, required := range target.Approvers {
		matches := []Approval{}
		for _, approval := range req.Approvals {
			if approval.Role == required.Role {
				matches = append(matches, approval)
			}
		}
		if len(matches) == 0 {
			problems = append(problems, fmt.Sprintf("no approval for %s as %s (%s)", target.Name, required.Role, required.Person))
			continue
		}

		approvedByExpectedPerson := false
		for _, approval := range matches {
			if approval.Person == required.Person {
				approvedByExpectedPerson = true
				if approval.Person == req.StartedBy {
					problems = append(problems, fmt.Sprintf("approval as %s by %s, who started the pipeline", required.Role, approval.Person))
				}
				break
			}
		}
		if !approvedByExpectedPerson {
			problems = append(problems, fmt.Sprintf("approval as %s by %s, want %s", required.Role, matches[0].Person, required.Person))
		}
	}

	return problems
}

Checker integrity

This file remains identical to the checker that produced the four passing requests plus the request-level refusals.

package road

import "fmt"

func Lint(m Map) []string {
	validGates := map[string]bool{
		"automatic": true,
		"scan":      true,
		"manual":    true,
		"dual":      true,
	}
	validDataClasses := map[string]bool{
		"synthetic": true,
		"masked":    true,
		"real":      true,
	}

	problems := []string{}
	stagingIndex := -1
	for index, rung := range m.Rungs {
		if rung.Name == "staging" {
			stagingIndex = index
			break
		}
	}
	if stagingIndex == -1 {
		problems = append(problems, "no rung named staging")
	}

	for index, rung := range m.Rungs {
		if !validGates[rung.Gate] {
			problems = append(problems, fmt.Sprintf("rung %s: unknown gate %s", rung.Name, rung.Gate))
		}
		if !validDataClasses[rung.DataClass] {
			problems = append(problems, fmt.Sprintf("rung %s: unknown data class %s", rung.Name, rung.DataClass))
		}
		if (rung.Gate == "manual" || rung.Gate == "dual") && len(rung.Approvers) == 0 {
			problems = append(problems, fmt.Sprintf("rung %s: gate %s names no approvers", rung.Name, rung.Gate))
		}
		if rung.Gate == "dual" && len(unique(people(rung.Approvers))) < 2 {
			problems = append(problems, fmt.Sprintf("rung %s: gate dual names fewer than two different people", rung.Name))
		}
		if stagingIndex >= 0 && rung.DataClass == "real" && index < stagingIndex {
			problems = append(problems, fmt.Sprintf("rung %s: real data below staging", rung.Name))
		}
	}

	return problems
}

Linter integrity

This file remains identical to the linter that enforced valid gates, named approvers, dual approval, and the real-data boundary.

rungs:
  - name: dev
    gate: automatic
    evidence: [test-record]
    approvers: []
    scans: [unit-tests]
    data_class: synthetic

  - name: integration
    gate: scan
    evidence: [test-record, sbom, scan-report]
    approvers: []
    scans: [sast, dependency, secrets, image]
    data_class: synthetic

  - name: test
    gate: manual
    evidence: [test-record, sbom, scan-report, dast-report]
    approvers:
      - role: qa-lead
        person: qa-ben
    scans: [sast, dependency, secrets, image, dast]
    data_class: masked

  - name: staging
    gate: manual
    evidence: [test-record, sbom, scan-report, dast-report]
    approvers:
      - role: release-manager
        person: rm-chen
    scans: [sast, dependency, secrets, image, dast]
    data_class: real

  - name: production
    gate: dual
    evidence: [test-record, sbom, scan-report, dast-report, change-request-id]
    approvers:
      - role: system-owner
        person: so-dara
      - role: change-board-chair
        person: cb-eli
    scans: [sast, dependency, secrets, image, dast]
    data_class: real

Road integrity

This file remains the exact five-rung road behind the generated table, the passing requests, and every refusal claim.

Secret mission

Prove a Missing-Evidence Refusal

Create a temporary request that omits one required evidence item. Capture the exact refusal without changing the released source. Delete the temporary fixture before proving that the supplied request still passes.

Clean Up Your Resources

Clean Up Your Resources

The project costs $0, so keeping its resources creates no ongoing charge. Choose whether to keep the local GitLab CE exercise project, pause its day-zero containers, or delete its temporary exercise resources.

Resources you used:

  • Local GitLab CE exercise project containing the unchanged four-job pipeline.
  • Temporary bad/no-staging-approvers.yaml refusal fixture.
  • Temporary bad/real-data-on-test.yaml refusal fixture.

Keep everything running

No action is needed. This option keeps the exercise ready for another command-backed demonstration.

  • Retain the local GitLab CE exercise project with its pipeline history.
  • Retain bad/no-staging-approvers.yaml for another refusal demonstration.
  • Retain bad/real-data-on-test.yaml for another data-boundary demonstration.
  • Keep the GitLab CE container running.
  • Keep the GitLab Runner container running.

Pause - I'll come back to this later

Shut down the day-zero containers to free local memory while retaining their data. Your private delivery repository remains available.

  • Return to the day-zero kit terminal from earlier.
  • Stop the GitLab CE container with the same day-zero kit procedure you used before this project.
  • Confirm the stop procedure reports the GitLab CE container as stopped.
  • Stop the GitLab Runner container with the same day-zero kit procedure.
  • Confirm the stop procedure reports the GitLab Runner container as stopped.
  • Preserve the container data volumes for your next session.

Delete - I don't want to use this again

Removing the exercise project permanently erases its local pipeline history. Your private delivery repository remains intact with its v1.0.0 release.

  • Return to the project overview for the local GitLab CE exercise project.
  • Select Actions.
  • Select Delete.
  • Complete the confirmation shown by GitLab CE.

If GitLab CE places the project in pending deletion, the project still exists until you remove it permanently.

  • Return to the project overview when you see the pending-deletion state.
  • Select Actions.
  • Select Delete permanently.
  • Complete the permanent-deletion confirmation shown by GitLab CE.
  • Switch back to the terminal in the private delivery repository from earlier.
  • Remove the two temporary bad-map copies by running this command:
rm -f bad/no-staging-approvers.yaml bad/real-data-on-test.yaml

What does this cleanup remove?

This command targets only bad/no-staging-approvers.yaml plus bad/real-data-on-test.yaml because they were temporary refusal fixtures. It leaves every supplied file plus all three learner-authored policy files unchanged.

  • Confirm the command returns to the prompt without output.
  • Confirm the bad/ directory no longer contains either temporary bad-map copy.

This cleanup leaves the reusable day-zero kit in place.

Nice Work!

Nice Work!

Excellent work on shipping a five-rung delivery governance system. Your ordered map now drives command-backed promotion decisions from development through production.

You've learned how to:

  • Build policy as code around the five-rung road.yaml map. Operate the promote command through its lint, check, and table modes.
  • Generate command-backed evidence for four passing promotions plus four mandatory refusal cases. Preserve all thirteen supplied files by authoring only road.yaml, internal/road/lint.go, and internal/road/check.go. Keep production approver identity in the change system of record.
  • Publish a tagged handover at v1.0.0 with the generated table, two readouts, fidelity note, and eight documentation pages. Reuse one evidence set to report the 4/4 adoption fraction. Record the four-job GitLab CE rehearsal as NOT REHEARSED until four successful logs exist. Preserve CM-3, CM-5, AC-5, SA-3(2), and SSDF PO.5 as traceability anchors.
  • Secret Mission: Proved that removing dast-report triggers the item-level test-rung refusal. Restored the unchanged request to PASS check: integration -> test without changing released source.

Ready to quiz yourself?