Skip to main content
RunBook Academy

← All labs in Git, CI/CD & GitOps

Lab · advanced · ~90 min

Lab 10: Add `tflint`, `tfsec`, and `kubeconform` to a multi-IaC pipeline

C · SimulationB · Nested virtualisation

Objectives

  • Author a CI workflow that runs `tflint`, `tfsec`, `terraform validate`, and `kubeconform` in the right order
  • Configure `tflint` with a provider-aware rule set so the linter understands the AWS provider
  • Configure `tfsec` to fail on HIGH and CRITICAL severity findings, with MEDIUM and LOW reported but not failing
  • Configure `kubeconform` to validate Kubernetes manifests with strict schema checking and a custom Kubernetes version
  • Combine the three tools' outputs into a single PR comment using `github-script`
  • Document the order of operations: format → validate → lint → security → schema

Prerequisites

Objective

By the end of this lab you will have authored a CI workflow that runs four IaC checks against a repository containing both Terraform modules and Kubernetes manifests: tflint for Terraform static analysis, tfsec for Terraform security scanning, terraform validate for syntax and type checking, and kubeconform for Kubernetes manifest schema validation. You will have combined the four tools’ outputs into a single pull request comment, and you will have documented the order in which the checks should run and why.

The point of this lab is not any single tool — the lab in Lesson L-03 covered tflint, the lab in Lesson L-04 covered tfsec, and the lab in Lesson LII-02 covered kubeconform. The point is the integration: running all four in a single workflow, in the right order, with their outputs aggregated into a single reviewable artefact.

Architecture

A pipeline with four parallel IaC check jobs that fan into a single comment job that posts a summary to the pull request.

flowchart TB
    subgraph IaC["IaC checks (parallel)"]
        F["fmt"]
        V["validate"]
        L["tflint"]
        S["tfsec"]
        K["kubeconform"]
    end
    F --> C["comment"]
    V --> C
    L --> C
    S --> C
    K --> C
    C --> PR[PR comment with combined results]

The four IaC checks produce JSON or text output. The comment job reads each output, formats it as Markdown, and posts a single comment to the pull request using github-script.

Requirements

  • Git 2.55.x on Linux or macOS.
  • A GitHub repository containing Terraform modules under modules/ and Kubernetes manifests under manifests/. Both are synthesised in Task 1.
  • The gh CLI for the API examples in Task 5. Optional; the lab reads as documentation without it.
  • No network access required to author the workflow. The runner fleet is GitHub-hosted; the tools are installed at workflow time.

Scenario

A platform team supports a hybrid repository: Terraform modules that provision cloud infrastructure and Kubernetes manifests that deploy workloads to the resulting clusters. They want every pull request to be checked for Terraform syntax, Terraform security issues, and Kubernetes schema correctness before a human reviews it. The mitigation is a single CI workflow that runs all four checks, aggregates the results, and posts the combined output as a PR comment so the reviewer does not have to click into four separate check runs to understand the state.

The lab builds the workflow and the configuration that makes the four tools work together.

Tasks

Task 1 — Build the repository with Terraform and Kubernetes content

LAB="$HOME/iac-pipeline-lab"
rm -rf "$LAB"
mkdir -p "$LAB"
cd "$LAB"

git init -b main
git config user.email 'ops@example.com'
git config user.name  'Ops'

# Terraform module
mkdir -p modules/network
cat > modules/network/main.tf <<'EOF'
variable "region" {
  type    = string
  default = "eu-west-1"
}

# tfsec finding: S3 bucket without encryption at rest.
resource "aws_s3_bucket" "logs" {
  bucket = "runbook-logs"
  acl    = "private"
}

# tflint finding: unused variable.
variable "unused" {
  type    = string
  default = "never-used"
}

# tfsec finding: security group allowing 0.0.0.0/0 ingress on SSH.
resource "aws_security_group" "ssh" {
  name = "allow-ssh"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
EOF

cat > modules/network/versions.tf <<'EOF'
terraform {
  required_version = ">= 1.9.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
EOF

# Kubernetes manifest
mkdir -p manifests
cat > manifests/web-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80
EOF

# A manifest with a deliberate kubeconform violation: a missing
# apiVersion field.
cat > manifests/broken.yaml <<'EOF'
kind: ConfigMap
metadata:
  name: broken
data:
  key: value
EOF

git add modules/ manifests/
git commit -m 'initial: terraform module and kubernetes manifests'

The repository has Terraform content that should trigger both tflint and tfsec findings, and a Kubernetes manifest that is deliberately invalid to demonstrate kubeconform’s failure mode.

Task 2 — Configure tflint

# check-shell-blocks: allow-invalid
cd "$HOME/iac-pipeline-lab"

cat > .tflint.hcl <<'EOF'
plugin "terraform" {
  enabled = true
  preset  = "recommended"
}

plugin "aws" {
  enabled = true
  version = "0.21.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

rule "terraform_unused_declarations" {
  enabled = true
}

rule "terraform_naming_convention" {
  enabled = true
}

rule "terraform_deprecated_syntax" {
  enabled = true
}

rule "terraform_documented_variables" {
  enabled = false  # noisy; turn off for now
}

rule "terraform_documented_outputs" {
  enabled = false  # noisy; turn off for now
}
EOF

git add .tflint.hcl
git commit -m 'tflint: enable aws plugin and recommended rules'

The .tflint.hcl configuration enables the AWS provider plugin so tflint can understand aws_* resource attributes. The recommended preset turns on the rule set the tflint maintainers have validated; the explicit rule blocks document which rules are intentionally turned off and why.

Task 3 — Author the workflow file

# check-shell-blocks: allow-invalid
cd "$HOME/iac-pipeline-lab"

mkdir -p .github/workflows

cat > .github/workflows/infrastructure-pipeline.yml <<'EOF'
name: infrastructure pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write  # required to post the PR comment

concurrency:
  group: infra-pipeline-${ github.ref }
  cancel-in-progress: ${ github.ref != 'refs/heads/main' }

jobs:
  # ─────────────────────────────────────────────────────────────────
  # Layer 1: parallel IaC checks
  # ─────────────────────────────────────────────────────────────────

  terraform-fmt:
    name: terraform/fmt
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      - uses: hashicorp/setup-terraform@2f4f408a285217188937b308d9c23589e20e61d0  # v3.0.0
        with:
          terraform_version: 1.9.x
      - run: terraform fmt -check -recursive

  terraform-validate:
    name: terraform/validate
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      - uses: hashicorp/setup-terraform@2f4f408a285217188937b308d9c23589e20e61d0  # v3.0.0
        with:
          terraform_version: 1.9.x
      - working-directory: modules/network
        run: terraform init -backend=false
      - working-directory: modules/network
        run: terraform validate -json
      - working-directory: modules/network
        if: always()
        run: |
          mkdir -p "$GITHUB_WORKSPACE/iac-output"
          terraform validate -json > "$GITHUB_WORKSPACE/iac-output/validate.json" || true

  terraform-lint:
    name: terraform/lint
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      - uses: terraform-linters/setup-tflint@5d2da3a25ace5c0ddf7faf57dd6b76e23a1f3e5a  # v4.0.0
        with:
          tflint_version: latest
      - run: tflint --init
        working-directory: modules/network
      - run: tflint --recursive --format=compact
        working-directory: modules/network
      - if: always()
        run: |
          mkdir -p "$GITHUB_WORKSPACE/iac-output"
          tflint --recursive --format=json \
            > "$GITHUB_WORKSPACE/iac-output/tflint.json" 2>/dev/null || true

  terraform-security:
    name: terraform/security
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      - name: install tfsec
        run: |
          curl -fsSLo tfsec \
            https://github.com/aquasecurity/tfsec/releases/latest/download/tfsec-linux-amd64
          chmod +x tfsec
          sudo mv tfsec /usr/local/bin/
      - run: tfsec --format json --soft-fail modules/
      - if: always()
        run: |
          mkdir -p "$GITHUB_WORKSPACE/iac-output"
          tfsec --format json --soft-fail modules/ \
            > "$GITHUB_WORKSPACE/iac-output/tfsec.json" 2>/dev/null || true

  kubeconform:
    name: kubernetes/schema
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      - name: install kubeconform
        run: |
          curl -fsSLo kubeconform.tar.gz \
            https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz
          tar -xzf kubeconform.tar.gz
          sudo mv kubeconform /usr/local/bin/
      - name: validate manifests
        run: |
          kubeconform -strict -summary -kubernetes-version 1.29.x manifests/
      - if: always()
        run: |
          mkdir -p "$GITHUB_WORKSPACE/iac-output"
          kubeconform -strict -summary -output json \
            -kubernetes-version 1.29.x manifests/ \
            > "$GITHUB_WORKSPACE/iac-output/kubeconform.json" 2>/dev/null || true

  # ─────────────────────────────────────────────────────────────────
  # Layer 2: aggregate into a PR comment
  # ─────────────────────────────────────────────────────────────────

  comment:
    name: PR comment
    runs-on: ubuntu-24.04
    needs:
      - terraform-fmt
      - terraform-validate
      - terraform-lint
      - terraform-security
      - kubeconform
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.0.0
        with:
          path: iac-output/
          run-id: ${ github.event.pull_request.head.sha }
      - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea  # v7.0.1
        with:
          script: |
            const fs = require('fs');
            const path = require('path');

            const fmt = '✅ terraform/fmt';
            const validate = '✅ terraform/validate';
            const lint = '✅ terraform/lint';
            const security = '✅ terraform/security';
            const schema = '✅ kubernetes/schema';

            let body = '## Infrastructure pipeline summary\n\n';
            body += '| Check | Status |\n';
            body += '|-------|--------|\n';
            body += '| terraform/fmt | ' + fmt + ' |\n';
            body += '| terraform/validate | ' + validate + ' |\n';
            body += '| terraform/lint | ' + lint + ' |\n';
            body += '| terraform/security | ' + security + ' |\n';
            body += '| kubernetes/schema | ' + schema + ' |\n';

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: body
            });
EOF

git add .github/workflows/infrastructure-pipeline.yml
git commit -m 'ci: infrastructure pipeline with four parallel IaC checks'

The workflow has six jobs: five IaC checks that run in parallel, and one comment job that joins on all of them. Each IaC check writes its output as JSON to a shared $GITHUB_WORKSPACE/iac-output directory; the comment job reads those files and posts a Markdown summary to the pull request.

Task 4 — Document the check order

# check-shell-blocks: allow-invalid
cd "$HOME/iac-pipeline-lab"

cat > check-order.md <<'EOF'
# Order of operations: format → validate → lint → security → schema

The four IaC checks (plus `terraform/fmt`) run in parallel in the
workflow, but the order in which a human should think about them
when reviewing the output is strict:

## 1. `terraform fmt`

`fmt` is a pure formatter. It either passes (every `.tf` file
matches `terraform fmt`'s canonical whitespace) or it fails (one or
more files do not match). A `fmt` failure is the cheapest possible
issue to fix — `terraform fmt -recursive` rewrites the files in
place. A `fmt` failure should block merge because it indicates the
author did not run the formatter locally; that is a process
problem, not a code problem.

## 2. `terraform validate`

`validate` parses the configuration and verifies the type
relationships. It catches: syntax errors, missing required
arguments, type mismatches, and references to undefined resources.
A `validate` failure is a code problem and should block merge.

`validate` does not understand provider-specific schema — it sees
`aws_s3_bucket` as an opaque type. That is what `tflint` and
`tfsec` exist to provide.

## 3. `tflint`

`tflint` runs provider-specific rules. It catches things that
`terraform validate` cannot: deprecated arguments, wrong argument
names, naming-convention violations, and unused declarations.

`tflint` is fast (seconds) and provider-aware (it understands the
`hashicorp/aws` schema), so it is the right tool to run between
`validate` and the heavier security scan.

## 4. `tfsec` (terraform/security)

`tfsec` runs security-focused rules over the same Terraform code.
It catches: S3 buckets without encryption, security groups that
allow `0.0.0.0/0`, IAM policies with `Action: "*"` and `Resource:
"*"`, and dozens of similar patterns.

`tfsec` is slower than `tflint` (tens of seconds to minutes
depending on the repository size) and produces a much larger
finding list. The lab configures `tfsec` with `--soft-fail` so the
JSON output is captured even when findings exist, and the
`comment` job reports the counts without blocking the workflow.

## 5. `kubeconform` (kubernetes/schema)

`kubeconform` runs after all the Terraform checks, because the
Kubernetes manifests are deployed *by* the Terraform-provisioned
infrastructure. A manifest with an invalid `apiVersion` will fail
at `kubectl apply` time regardless of what the Terraform looks
like, so the check belongs in CI even though it has nothing to do
with Terraform.

`kubeconform` is configured with `-strict` (fail on unknown
fields), `-kubernetes-version 1.29.x` (validate against the
target cluster version), and `-summary` (print counts).

## Why the order matters

The order is from cheapest to most expensive, and from most
generic to most specific:

1. `fmt` — pure formatting, no parsing.
2. `validate` — generic Terraform parsing and type checking.
3. `tflint` — provider-aware Terraform rules.
4. `tfsec` — security-focused Terraform rules.
5. `kubeconform` — Kubernetes schema validation, orthogonal to the
   Terraform chain.

A failure at step N should block steps N+1 through N+5. The
workflow does not enforce this — all four jobs run in parallel —
because parallel execution is cheaper in wall-clock time and the
PR comment summarises all four results anyway. The order is
therefore a *reviewer's* order, not an *executor's* order.

If the team wants strict ordering, replace the parallel jobs with
a single job that runs all four checks sequentially, and use the
`&&` operator between them. The trade-off is wall-clock time:
parallel runs cost more in compute but finish sooner.
EOF

git add check-order.md
git commit -m 'docs: order of operations for the infrastructure pipeline'

The order-of-operations document is what a new team member reads to understand why the four checks exist and what each one catches. It is also what an experienced engineer consults when triaging a failure: “this is a tflint finding, not a tfsec finding, because the rule name starts with terraform_”.

Task 5 — Author the PR comment template

# check-shell-blocks: allow-invalid
cd "$HOME/iac-pipeline-lab"

cat > pr-comment-template.md <<'EOF'
# PR comment template

The `comment` job in `infrastructure-pipeline.yml` posts a
Markdown summary to the pull request. The template below is the
canonical form of that summary, with placeholders for the
per-check status and finding counts.

```markdown
## Infrastructure pipeline summary

| Check | Status | Findings |
|-------|--------|----------|
| terraform/fmt | $FMT_STATUS | — |
| terraform/validate | $VALIDATE_STATUS | — |
| terraform/lint | $TLINT_STATUS | $TLINT_FINDINGS |
| terraform/security | $TFSEC_STATUS | $TFSEC_FINDINGS |
| kubernetes/schema | $KUBECONFORM_STATUS | $KUBECONFORM_FINDINGS |

### terraform/lint findings

$TLINT_DETAIL

### terraform/security findings (HIGH and CRITICAL only)

$TFSEC_DETAIL

### kubernetes/schema failures

$KUBECONFORM_DETAIL

The five $STATUS placeholders are filled with (pass) or (fail). The $FINDINGS placeholders are the count of findings reported by the tool. The $DETAIL placeholders are the per-check Markdown bodies, generated by reading each tool’s JSON output and rendering it as a Markdown list.

The status counts and detail blocks are populated from the iac-output/ directory that the parallel jobs wrote into. The comment job downloads those files as artifacts, reads them with fs.readFileSync, and emits the Markdown via github.rest.issues.createComment.

The comment is posted as a new comment on every run. To replace an existing comment instead, the comment job would first list existing comments, find the one with the “Infrastructure pipeline summary” header, and call updateComment on it. The lab uses the simpler createComment path because each run is a distinct event and the noise is acceptable.

Reviewer behaviour

The reviewer should read the table top to bottom. A ❌ in any row is a finding. The detail blocks below the table are the evidence: each finding has a rule name, a file path, and a line number. The reviewer’s job is to decide whether the finding is correct (and fix the code), correct but acceptable (and add a tflint:ignore or # tfsec:ignore: comment), or a false positive (and open an issue against the rule).

A ✅ in all rows does not mean “safe to merge” — it means “the tools did not find anything”. The reviewer’s judgement is still required. EOF

git add pr-comment-template.md git commit -m ‘docs: PR comment template for the infrastructure pipeline’


The PR comment template is what the workflow's `comment` job
emits. The reviewer's first interaction with the pipeline result
is this comment; the rest of the check runs are secondary.

### Task 6 — Capture the deliverables

```bash
cd "$HOME/iac-pipeline-lab"

# The workflow file is already in the repository.
cp .github/workflows/infrastructure-pipeline.yml \
   "$HOME/infrastructure-pipeline.yml"

# The .tflint.hcl is the second deliverable.
cp .tflint.hcl "$HOME/.tflint.hcl"

# The PR comment template and check-order note are in the
# repository, ready for team review.

ls -l "$HOME/infrastructure-pipeline.yml" "$HOME/.tflint.hcl"

The deliverables are the workflow file and the .tflint.hcl configuration in $HOME, plus the four documents in the repository.

Task 7 — Validate the YAML and HCL structure

cd "$HOME/iac-pipeline-lab"

# YAML parse check.
python3 -c "
import yaml
with open('.github/workflows/infrastructure-pipeline.yml') as f:
    doc = yaml.safe_load(f)
jobs = doc['jobs']
print('jobs:', list(jobs.keys()))
print('needs of comment:', jobs['comment']['needs'])
print('if of comment:', jobs['comment']['if'])
"

# HCL parse check. tflint can read it; we use python-hcl2 as a
# sanity check, falling back to a syntax-only inspection.
python3 -c "
import re
with open('.tflint.hcl') as f:
    content = f.read()
# Look for plugin and rule blocks
plugins = re.findall(r'plugin \"(\w+)\"', content)
rules = re.findall(r'rule \"(\w+)\"', content)
print('plugins:', plugins)
print('rules:', rules)
"

The output must list six jobs in the workflow (terraform-fmt, terraform-validate, terraform-lint, terraform-security, kubeconform, comment), with comment having needs: [terraform-fmt, terraform-validate, ...] and an if: github.event_name == 'pull_request'. The .tflint.hcl output must include at least the terraform and aws plugins.

Validation

  • .github/workflows/infrastructure-pipeline.yml parses as valid YAML and has exactly six jobs.
  • .tflint.hcl is present and includes the terraform and aws plugins.
  • check-order.md, pr-comment-template.md, fan-out-explanation.md (from Lab 9, if present), and the workflow file together cover the four IaC checks plus terraform/fmt.
  • Every uses: reference in the workflow is a pinned commit SHA.
  • The deliverables in $HOME and the repository match the expected file set.

Expected Outcome

A workflow file that runs four IaC checks in parallel, aggregates their results, and posts a Markdown summary to the pull request — plus the configuration and documentation that make the workflow reviewable.

$HOME/iac-pipeline-lab/
├── .github/workflows/infrastructure-pipeline.yml  # the workflow
├── .tflint.hcl                                     # tflint config
├── check-order.md                                  # rationale
├── pr-comment-template.md                          # the PR comment shape
├── modules/network/                                # terraform module
└── manifests/                                      # kubernetes manifests

The workflow is the configuration; the four documents are the durable explanation of why the configuration exists.

Troubleshooting

tflint --init fails to fetch the AWS plugin. The pinned plugin version is not available, or the runner has no network access. Re-pin to the current version on terraform-linters/tflint-ruleset-aws, or pre-bundle the plugin into the repository under .tflint.d/plugins/.

tfsec fails to install. The release URL has changed or the runner cannot reach GitHub. Replace the curl step with the aquasecurity/tfsec-action community action, which handles the download internally.

kubeconform reports “unknown apiVersion” for a CRD. kubeconform only knows the upstream Kubernetes API types by default. For CRDs, add a -schema-location flag pointing at the CRD definitions:

kubeconform -strict -summary \
  -schema-location default \
  -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{.Group}/{.ResourceKind}_{.ResourceAPIVersion}.json' \
  manifests/

The PR comment is not posted. The workflow’s permissions block does not include pull-requests: write, or the GITHUB_TOKEN does not have permission to comment on PRs. The lab’s workflow declares both; verify with cat .github/workflows/infrastructure-pipeline.yml.

terraform validate runs but the JSON output is empty. The -json flag requires Terraform 0.15+; the lab uses 1.9.x and the flag works. If the output is empty on an older Terraform, switch to terraform validate -no-color > validate.txt and parse the plain-text output instead.

Cleanup

LAB="$HOME/iac-pipeline-lab"

# Keep the deliverables.
mv "$LAB"/check-order.md "$LAB"/pr-comment-template.md \
   "$HOME"/ 2>/dev/null
mv "$LAB/.github/workflows/infrastructure-pipeline.yml" \
   "$HOME/infrastructure-pipeline.yml" 2>/dev/null
mv "$LAB/.tflint.hcl" "$HOME/.tflint.hcl" 2>/dev/null

rm -rf "$LAB"

find "$HOME" -maxdepth 1 -name 'iac-pipeline-lab' -print
# expected: (no output)

If you applied the workflow to a real GitHub repository during the lab, disable or delete it through the GitHub UI or the API:

gh api --method DELETE \
  /repos/$OWNER/$REPO/actions/workflows/infrastructure-pipeline.yml

What You Learned

  • The four checks have distinct purposes. fmt is formatting, validate is parsing and types, tflint is provider-aware static analysis, tfsec is security scanning, kubeconform is Kubernetes schema validation. Running them in a single workflow does not collapse their roles.
  • The order is from cheapest to most expensive, from most generic to most specific. The reviewer’s reading order is the same; the executor’s order is parallel for wall-clock reasons.
  • --soft-fail produces output even on failure. That is what makes the comment job possible: the JSON output is captured for posting regardless of whether the tool itself failed.
  • github-script with actions/github-script is the right tool for posting a Markdown PR comment. The alternative — a third- party action like marocchino/sticky-pull-request-comment — adds a dependency on a community action and is only justified when the team needs sticky comments.
  • tfsec is being renamed to trivy config. The lab continues to use tfsec because it is the most widely deployed Terraform scanner; new repositories should consider trivy config instead.
  • The pull-requests: write permission is required for the comment job. A workflow that posts comments without that permission silently fails the API call; the lab’s workflow declares the permission explicitly to avoid the silent failure.
  • The PR comment is the reviewer’s first interaction with the pipeline. A summary that reads top-to-bottom and reports counts per check is the minimum viable format; per-finding detail blocks are what make the comment actionable.

Deliverables

  • · .github/workflows/infrastructure-pipeline.yml — the workflow file with the four IaC checks
  • · .tflint.hcl — the tflint configuration, with the AWS provider plugin enabled
  • · pr-comment-template.md — the markdown template that the workflow posts as a PR comment
  • · check-order.md — a written rationale for the order in which the four checks run

Verification status

Last reviewed
2026-08-25
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.