Build a Deployment Doctor

Build a CLI that diagnoses deployment bundles and creates safe reports.

Introduction

30 Second Summary

A failed customer deployment can arrive as a wall of technical details. The clue that matters can be buried beside sensitive customer information.

In this project, you will build a local Deployment Doctor that turns a customer diagnostic bundle into evidence-backed findings. The finished tool produces a redacted Markdown escalation report for customer support.

What You'll Build

Picture one command turning a failed customer bundle into a report that a support responder can safely share.

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

  • An evidence-backed diagnosis that surfaces the project's three failure types. The healthy bundle stays free of false alarms.
  • Runbook guidance that labels every recommendation with its human approval status.
  • Redacted escalation reports that hide secret-like values before the findings are shared.
  • Secret Mission: Onboard a second customer through customer-specific configuration while leaving the Python engine unchanged.

Are there any prerequisites?

No prior setup is required. The first step prepares the complete local toolchain on your Linux device.

Before We Start

Before the hands-on work begins, this checkpoint locks in the customer problem you want to solve. You are building a customer-facing Deployment Doctor that turns failed deployment evidence into a clearer support workflow with a measurable business outcome.

Set Up the Local Toolchain

Customer deployment investigations become harder when responders use different local environments. A repeatable toolchain lets you demonstrate the first support workflow on your Linux device.

You will prepare Visual Studio Code as your workspace editor. The uv tool at version 0.12.9 will provide the script with Python 3.12 or later.

In this step, get ready to:
  • Prepare Visual Studio Code with a working code launcher.
  • Install uv version 0.12.9.
  • Create a runnable Deployment Doctor workspace.
Install Visual Studio Code

Visual Studio Code keeps the diagnostic engine beside its customer samples. Its terminal launcher opens the correct workspace from a single command.

  • Check whether the Visual Studio Code launcher is available by running:
code --help

What Does This Command Confirm?

The command asks Visual Studio Code to print its command-line help. Seeing that text confirms Linux can find the code launcher.

  • Select the tab that matches your terminal result.

✔️ I see the help output

Your terminal has found the editor launcher. Visual Studio Code is ready for the Deployment Doctor workspace.

ⓧ The command is unavailable

The package commands use sudo. Linux may ask for your administrator password to authorize the local installation.

  • Select the installation route for your Linux distribution.

Debian or Ubuntu

  • Download the .deb package from the official Visual Studio Code Linux setup page.
  • Open the folder containing the downloaded package in your Linux file manager.
  • Start a terminal in that folder using your file manager's terminal option.
  • Use the downloaded package filename in place of <file> when you run:
sudo apt install ./<file>.deb
What Does This Command Do?

The command installs the local .deb package through the Debian package manager.

Fedora

  • Download the .rpm package from the official Visual Studio Code Linux setup page.
  • Open the folder containing the downloaded package in your Linux file manager.
  • Start a terminal in that folder using your file manager's terminal option.
  • Use the downloaded package filename in place of <file> when you run:
sudo dnf install <file>.rpm
What Does This Command Do?

The command installs the downloaded .rpm package through Fedora's package manager.

Snap Already Available

This route uses the existing Snap installation on your Linux device.

  • Install the stable Visual Studio Code package by running:
sudo snap install --classic code
What Does This Command Do?

The command installs the stable Visual Studio Code package through Snap. The package includes the code launcher.

  • Close the current terminal window.
  • Start a new terminal from your Linux application menu.
  • Confirm the launcher is available by running:
code --help

What Does This Command Confirm?

The help output confirms the installation added the code launcher to your terminal path.

You should see the Visual Studio Code command-line help text.

Still Unable to Run the Launcher?

  • Confirm the package installation completed before you closed the terminal.
  • Start another terminal so Linux reloads its command path.
  • Repeat the installation route for the package format you downloaded.

Help me troubleshoot the Visual Studio Code launcher on Linux. You can also share the terminal output in the NextWork community.

Install uv and establish Python

The starter script declares its minimum Python version inside the file. uv reads that requirement before execution.

Why uv for This Project?

A manual Python setup can vary across responder devices. uv reads the script requirement before selecting a compatible interpreter.

The same project file can therefore request Python 3.12 or later on every device.

  • Check whether uv is already installed by running:
uv self version

What Does This Command Confirm?

The command prints the installed uv version. This project uses version 0.12.9.

  • Select the tab that matches your version check.

✔️ I see version 0.12.9

Your uv installation matches the project version. It is ready to manage the script's Python requirement.

ⓧ I see an older version

  • Install the pinned uv version by running:
curl -LsSf https://astral.sh/uv/0.12.9/install.sh | sh

What Does This Command Do?

The command downloads the official installer for uv version 0.12.9. It updates the standalone uv installation.

  • Close the current terminal window.
  • Start a new terminal from your Linux application menu.
  • Verify the pinned installation by running:
uv self version

What Does This Command Confirm?

The command reads the uv installation available to the new terminal.

You should see version 0.12.9 in the output.

Still Seeing the Older Version?

  • Close every terminal window that was open before the installation.
  • Start a new terminal from your Linux application menu.
  • Run the version check again.

Help me identify which uv installation Linux is using. You can also share the version output in the NextWork community.

ⓧ Command not found

  • Install uv version 0.12.9 by running:
curl -LsSf https://astral.sh/uv/0.12.9/install.sh | sh

What Does This Command Do?

The command downloads the official installer for uv version 0.12.9. It installs the standalone uv tool on your device.

  • Close the current terminal window.
  • Start a new terminal from your Linux application menu.
  • Verify the installation by running:
uv self version

What Does This Command Confirm?

The command confirms that your new terminal can find uv.

You should see version 0.12.9 in the output.

Still Unable to Find uv?

  • Confirm the installer completed without a network failure.
  • Close every terminal window that was open during the installation.
  • Run the version check from a new terminal.

Help me troubleshoot an uv installation that Linux cannot find. You can also share the installer output in the NextWork community.

Create the workspace and run it

A named workspace keeps sample bundles separate from generated reports. The starter file records the Python requirement beside the code that uses it.

  • Move to your Desktop by running this command:
cd ~/Desktop

What Does This Command Do?

The command moves your terminal into the Desktop directory. Your new deployment-doctor workspace will be easy to find there.

  • Create the workspace structure by running:
mkdir -p deployment-doctor/samples deployment-doctor/reports && cd deployment-doctor

What Does This Command Create?

The command creates the deployment-doctor workspace. It adds separate samples/ and reports/ directories.

The terminal finishes inside the new workspace.

  • Open the current workspace in Visual Studio Code by running:
code .

What Does This Command Do?

The dot represents the terminal's current directory. Visual Studio Code opens deployment-doctor as the active workspace.

You should see samples/ and reports/ in the Explorer sidebar.

Workspace Folders Missing?

  • Return to the terminal that created the workspace.
  • Confirm the prompt ends inside deployment-doctor.
  • Run the workspace command again from that directory.

Help me locate the Deployment Doctor workspace on my Linux Desktop.

The inline metadata asks uv for Python 3.12 or later. The empty dependency list keeps this first run limited to Python's built-in features.

  • Select the file creation icon in the Explorer sidebar.
  • Type deployment_doctor.py as the filename.
  • Press Enter to create the file.

You should see deployment_doctor.py beside the two workspace directories.

  • Add the workspace check by pasting this code into deployment_doctor.py:
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///

print("Deployment Doctor workspace ready")

What Does This Code Do?

  • The script metadata requires Python >=3.12 for every run.
  • The empty dependency list confirms that the script uses no third-party packages.
  • The print statement provides a visible toolchain check.
  • Save deployment_doctor.py.

The first run can take a few minutes while uv obtains a compatible Python interpreter. A longer wait here means the runtime requirement is being prepared.

Before you run the script, what message do you expect the workspace check to print?

  • Run the workspace check from the deployment-doctor terminal by running:
uv run deployment_doctor.py

What Does This Command Do?

uv reads the inline metadata before executing the script. It uses a compatible Python interpreter or downloads one when required.

You should see Deployment Doctor workspace ready in the terminal. That is your first working slice of the Deployment Doctor.

Workspace Check Not Running?

  • Confirm the terminal prompt is inside the deployment-doctor directory.
  • Confirm the Explorer sidebar shows deployment_doctor.py beside samples/ and reports/.
  • Check that the four metadata lines match the saved script exactly.

Help me troubleshoot the Deployment Doctor workspace check. You can also share the terminal output in the NextWork community.

✔️ Awesome, I've got everything!

Your workspace is saved. The terminal check proves uv can run the script with a compatible Python interpreter.

ⓧ I'd like to double check the full code

  • Compare your saved deployment_doctor.py with this complete file:
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///

print("Deployment Doctor workspace ready")

What Should Match?

Your file should contain the same four metadata lines. The workspace-ready print statement should follow them.

The samples/ and reports/ directories should sit beside the file.

Your local toolchain can now run the starter script from a structured workspace. Next up, you will feed it raw customer diagnostics to expose the noise that responders face.

Inspect Raw Customer Diagnostics

Your local Deployment Doctor workspace is ready to run. Customer diagnostics now need a consistent shape before the tool can inspect them.

In this step, you will model failed and healthy customer evidence as JSON. You will extend the Python script with a raw inspection path.

In this step, get ready to:
  • Capture a failed customer deployment in a JSON bundle.
  • Create a healthy bundle for comparison.
  • Print the failed bundle without interpreting its contents.
Create the failed customer bundle

A diagnostic bundle captures the customer context that a responder receives during a deployment failure. This bundle combines configuration evidence with runtime symptoms.

  • In Visual Studio Code's file sidebar, select the samples/ directory.
  • Create failed_bundle.json inside samples/ using the new-file control beside the directory.
  • Populate samples/failed_bundle.json by pasting this bundle:
{
  "customer": "Northstar Retail",
  "environment": "on-prem-linux",
  "service": "checkout-api",
  "business_impact": "Customers cannot complete checkout.",
  "deployment": {
    "version": "2026.09.04-rc1",
    "desired_port": 8080,
    "required_env": ["DATABASE_URL", "PAYMENT_SERVICE_TOKEN"],
    "present_env": {"PAYMENT_SERVICE_TOKEN": "example-token-not-a-real-secret"}
  },
  "runtime": {
    "status": "failed",
    "restart_count": 7,
    "listening_ports": [{"port": 8080, "process": "legacy-gateway"}]
  },
  "dependencies": [
    {"name": "orders-db", "status": "unreachable"},
    {"name": "fraud-api", "status": "healthy"}
  ]
}

What Does This Bundle Capture?

  • The deployment object records the required environment names plus the requested service port.
  • The runtime object shows that legacy-gateway already owns port 8080.
  • The dependencies array records that orders-db is unreachable.
  • The fictional token demonstrates how support bundles can contain values that should not be forwarded.
  • Save samples/failed_bundle.json in Visual Studio Code.
  • Confirm the file is listed beneath samples/ in the file sidebar.

You should see failed_bundle.json inside samples/ with the Northstar Retail bundle open in the editor.

Bundle Showing JSON Problems?

Compare every brace plus bracket with the bundle above. A missing comma between fields can also prevent the file from being valid JSON.

Ask for help if the editor still marks the bundle as invalid. Help me find the JSON syntax problem in my failed diagnostic bundle.

Create the healthy comparison bundle

A healthy bundle uses the same customer contract as the failed example. Its values provide a clean comparison for later diagnostic checks.

  • Select the samples/ directory in Visual Studio Code's file sidebar.
  • Create healthy_bundle.json inside samples/ using the new-file control beside the directory.
  • Populate samples/healthy_bundle.json by pasting this bundle:
{
  "customer": "Northstar Retail",
  "environment": "on-prem-linux",
  "service": "checkout-api",
  "business_impact": "No active customer impact.",
  "deployment": {
    "version": "2026.09.04-rc1",
    "desired_port": 8080,
    "required_env": ["DATABASE_URL", "PAYMENT_SERVICE_TOKEN"],
    "present_env": {
      "DATABASE_URL": "postgresql://database.internal/orders",
      "PAYMENT_SERVICE_TOKEN": "example-token-not-a-real-secret"
    }
  },
  "runtime": {
    "status": "healthy",
    "restart_count": 0,
    "listening_ports": [{"port": 9090, "process": "metrics-agent"}]
  },
  "dependencies": [
    {"name": "orders-db", "status": "healthy"},
    {"name": "fraud-api", "status": "healthy"}
  ]
}

What Makes This Bundle Healthy?

  • The present_env object contains both required environment names.
  • The only listening process uses port 9090.
  • Each required dependency reports a healthy status.
  • The shared schema lets one diagnostic engine inspect both customer outcomes.
  • Save samples/healthy_bundle.json in Visual Studio Code.
  • Confirm both bundle files are listed beneath samples/ in the file sidebar.

You should see failed_bundle.json plus healthy_bundle.json inside samples/.

Healthy Bundle Still Marked Invalid?

Check the comma after the DATABASE_URL entry. Confirm each dependency object has matching braces.

Ask for a focused syntax review if the marker remains. Help me validate the structure of my healthy diagnostic bundle.

Build the raw bundle inspector

The script needs a loader before it can inspect a bundle from disk. Standard-library modules handle the command-line input plus JSON parsing.

  • Switch back to deployment_doctor.py in the Visual Studio Code workspace from earlier.
  • Find the blank line below the inline script metadata.
  • Add the imports plus load_json() above the existing print("Deployment Doctor workspace ready") line by pasting:
import argparse
import json
from pathlib import Path


def load_json(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8"))

What Does This Code Do?

  • The argparse import prepares the script to accept terminal arguments.
  • The json import converts the bundle text into Python data.
  • The Path class represents the bundle's file path.
  • The load_json() function reads the selected file with UTF-8 encoding.
  • Save deployment_doctor.py.
  • Confirm the existing workspace path still runs by using this command:
uv run deployment_doctor.py

You should still see Deployment Doctor workspace ready in the terminal.

Workspace Message Missing?

Confirm the original print line remains below load_json(). Check that all three imports appear after the script metadata.

Use this prompt for a focused review. Help me find why my Deployment Doctor workspace message stopped printing.

A parser turns terminal input into named values that the script can use. The bundle path remains optional so the original workspace check still works.

  • Find the blank line after load_json().
  • Add build_parser() before the existing print line by pasting:
def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Turn a customer deployment bundle into a safe diagnostic report."
    )
    parser.add_argument("bundle", type=Path, nargs="?", help="Path to a JSON bundle")
    parser.add_argument("--raw", action="store_true", help="Print the unprocessed bundle")
    return parser

How Does the Parser Work?

  • The optional bundle argument accepts a path to a customer JSON file.
  • The --raw flag records whether the complete input should be printed.
  • The returned parser gives main() one place to read both values.
  • Save deployment_doctor.py.
  • Confirm the optional bundle path preserves the workspace check by running:
uv run deployment_doctor.py

You should see Deployment Doctor workspace ready again.

Parser Stopping the Script?

Compare the parser's parentheses with the snippet above. Confirm return parser remains inside build_parser().

Ask for help if the workspace check no longer runs. Help me troubleshoot my Deployment Doctor argument parser.

The main() function now chooses between the workspace check and raw bundle output. Formatted output preserves the complete customer payload for inspection.

  • Find the existing print("Deployment Doctor workspace ready") line.
  • Replace that line with the complete entry point below:
def main() -> None:
    args = build_parser().parse_args()
    if args.bundle is None:
        print("Deployment Doctor workspace ready")
        return

    bundle = load_json(args.bundle)
    if args.raw:
        print(json.dumps(bundle, indent=2, sort_keys=True))


if __name__ == "__main__":
    main()

What Does the Entry Point Do?

  • The parsed arguments tell main() whether a bundle path was supplied.
  • The no-bundle branch preserves the original workspace message.
  • The bundle branch loads the selected JSON file.
  • The raw branch prints every field with indentation plus sorted keys.
  • Save deployment_doctor.py.

Before you run the inspector, do you think this raw bundle gives a responder a clear diagnosis?

  • Inspect the failed customer bundle by running:
uv run deployment_doctor.py samples/failed_bundle.json --raw

You will see the complete formatted bundle, including the fictional PAYMENT_SERVICE_TOKEN value example-token-not-a-real-secret.

The output provides no diagnosis or priority. That noisy result is the intended shortfall.

Raw Bundle Not Printing?

Confirm the terminal is still inside the deployment-doctor workspace. Check that samples/failed_bundle.json appears in the file sidebar.

Use this prompt if the command still fails. Help me troubleshoot why my raw Deployment Doctor bundle is not printing.

✔️ Awesome, I've got everything!

Great work. Your failed bundle now prints as a complete raw payload.

  • Double-check that deployment_doctor.py is saved.
  • Confirm both JSON bundles remain inside samples/.

ⓧ I'd like to double check the full code

# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///

import argparse
import json
from pathlib import Path


def load_json(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8"))


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Turn a customer deployment bundle into a safe diagnostic report."
    )
    parser.add_argument("bundle", type=Path, nargs="?", help="Path to a JSON bundle")
    parser.add_argument("--raw", action="store_true", help="Print the unprocessed bundle")
    return parser


def main() -> None:
    args = build_parser().parse_args()
    if args.bundle is None:
        print("Deployment Doctor workspace ready")
        return

    bundle = load_json(args.bundle)
    if args.raw:
        print(json.dumps(bundle, indent=2, sort_keys=True))


if __name__ == "__main__":
    main()

You have reproduced the noise that responders face when a customer sends an unprocessed diagnostic bundle. Next up, you will turn these scattered symptoms into prioritized findings with traceable evidence.

Build the Diagnostic Engine

The raw diagnostic bundle proved how quickly customer evidence becomes a wall of JSON. Finding the failure still depends on a responder hunting through every field.

This step turns that bundle into prioritized findings through evidence-based rules. Each finding points to the exact symptom that triggered it.

In this step, get ready to:
  • Detect missing environment configuration.
  • Check runtime ports plus dependency health.
  • Rank each finding for evidence-rich terminal output.
Detect missing configuration

A deployment contract lists the environment names that a service requires. Comparing that list with the names actually present reveals missing configuration without exposing any values.

  • In deployment_doctor.py from earlier, find this import:
from pathlib import Path

Why start here?

This import marks the end of the existing import group. The severity map belongs directly below it as module-level configuration.

  • Place the severity map below that import so the top of the file matches this:
from pathlib import Path

SEVERITY_RANK = {"P1": 3, "P2": 2, "P3": 1, "OK": 0}

How severity ranking works

The numeric values give each priority a sortable weight. A larger weight moves a more urgent finding toward the top of the output.

  • Save deployment_doctor.py.
  • Confirm the script still starts by running:
uv run deployment_doctor.py

What should you see?

You should still see Deployment Doctor workspace ready. This confirms the new module-level value has not disrupted the existing startup path.

Script no longer starting?

  • Check that SEVERITY_RANK starts at the far left of the file.
  • Check that every priority key uses matching quotation marks.
  • Ask for help with the syntax error through this troubleshooting prompt.

The first rule compares the required environment names with the keys available in present_env. A set difference isolates any required names that are absent.

  • Add the initial analyze_bundle() function below load_json() by copying this code:
def analyze_bundle(bundle: dict) -> list[dict]:
    findings = []
    deployment = bundle.get("deployment", {})
    runtime = bundle.get("runtime", {})

    required_env = set(deployment.get("required_env", []))
    present_env = set(deployment.get("present_env", {}).keys())
    missing_env = sorted(required_env - present_env)
    if missing_env:
        findings.append(
            {
                "id": "missing-environment",
                "severity": "P1",
                "title": "Required environment configuration is missing",
                "evidence": f"Missing names: {', '.join(missing_env)}",
                "impact": "The service cannot initialize its required connections.",
                "runbook_id": "missing_environment",
            }
        )

    return sorted(
        findings,
        key=lambda finding: SEVERITY_RANK[finding["severity"]],
        reverse=True,
    )

What does this rule do?

  • The set stored in required_env represents the deployment contract.
  • The set stored in present_env contains the configured names without reading their values.
  • The missing-environment finding records a priority plus traceable evidence when the difference contains any names.
  • The final sort uses SEVERITY_RANK to place urgent findings first.

The existing main() function loads the bundle for raw inspection. It now needs a second path that sends the same bundle through the analyzer.

  • In main(), find this code:
    bundle = load_json(args.bundle)
    if args.raw:
        print(json.dumps(bundle, indent=2, sort_keys=True))

What does the current path do?

This path loads the selected bundle. The --raw condition prints the original payload without interpreting it.

  • Replace that code with the analyzer path below:
    bundle = load_json(args.bundle)
    if args.raw:
        print(json.dumps(bundle, indent=2, sort_keys=True))
        return

    findings = analyze_bundle(bundle)
    for finding in findings:
        print(
            f"- {finding['severity']} {finding['title']}: "
            f"{finding['evidence']}"
        )

How the two paths differ

  • The return keeps raw inspection separate from diagnosis.
  • The standard path passes the loaded bundle to analyze_bundle().
  • The output loop prints the priority plus the title plus the triggering evidence for every finding.
  • Save deployment_doctor.py.
  • Before running the check, form a prediction about the customer symptom that the first rule surfaces.
  • Test the missing-configuration rule by running:
uv run deployment_doctor.py samples/failed_bundle.json

What should you see?

You should see one P1 finding titled Required environment configuration is missing. Its evidence names DATABASE_URL as missing.

That first diagnostic rule is working. The responder now receives a specific finding instead of searching the entire bundle.

Missing the first finding?

  • Check that analyze_bundle() sits above build_parser().
  • Check that present_env uses .keys() before the set comparison.
  • Ask for help with the missing result through this diagnostic prompt.
Check ports and dependencies

Configuration is only one part of a failed deployment. The bundle also records which processes own local ports plus whether required dependencies are healthy.

  • Insert the port-conflict rule before the final return sorted( line in analyze_bundle() by copying this code:
    desired_port = deployment.get("desired_port")
    for listener in runtime.get("listening_ports", []):
        if desired_port is not None and listener.get("port") == desired_port:
            findings.append(
                {
                    "id": "port-conflict",
                    "severity": "P1",
                    "title": "The requested service port is already occupied",
                    "evidence": (
                        f"Port {desired_port} is owned by "
                        f"{listener.get('process', 'an unknown process')}"
                    ),
                    "impact": "The new service cannot bind to its configured port.",
                    "runbook_id": "port_conflict",
                }
            )

What does the port rule prove?

  • The rule reads the service's requested port from desired_port.
  • Each entry in listening_ports represents an observed listener.
  • A matching port produces evidence that names the owning process.
  • Save deployment_doctor.py.
  • Check the new runtime rule by running:
uv run deployment_doctor.py samples/failed_bundle.json

What should you see now?

You should see two P1 findings. The second finding reports that port 8080 is owned by legacy-gateway.

Port conflict not appearing?

  • Check that the new loop is indented inside analyze_bundle().
  • Check that the port comparison uses listener.get("port").
  • Ask for help with the comparison through this troubleshooting prompt.

A service can start while an external system remains unavailable. The dependency rule catches that separate failure mode by checking every recorded status.

  • Insert the dependency rule below the port loop by copying this code:
    for dependency in bundle.get("dependencies", []):
        if dependency.get("status") != "healthy":
            findings.append(
                {
                    "id": f"dependency-{dependency.get('name', 'unknown')}",
                    "severity": "P2",
                    "title": "A required dependency is unreachable",
                    "evidence": (
                        f"{dependency.get('name', 'Unknown dependency')} reported "
                        f"status {dependency.get('status', 'unknown')}"
                    ),
                    "impact": "The service may start but cannot complete its business workflow.",
                    "runbook_id": "dependency_unreachable",
                }
            )

What does the dependency rule do?

  • The loop checks each object inside dependencies.
  • Any status other than healthy creates a finding.
  • The evidence records both the dependency name plus the reported status.
  • The rule assigns P2 because the local configuration failures remain the highest priority.
  • Save deployment_doctor.py.
  • Check all three rules by running:
uv run deployment_doctor.py samples/failed_bundle.json

What should you see now?

You should see three findings. The new P2 finding says that orders-db reported status unreachable.

The healthy fraud-api dependency produces no finding. The engine only reports evidence that violates a rule.

Dependency finding missing?

  • Check that the dependency loop sits before the function's final return.
  • Check that the comparison uses the literal value healthy.
  • Ask for help with the dependency rule through this troubleshooting prompt.
Rank the findings

The findings already return in severity order. A small helper now summarizes the highest priority so a responder can judge urgency before reading every detail.

  • Add highest_priority() below analyze_bundle() by copying this code:
def highest_priority(findings: list[dict]) -> str:
    if not findings:
        return "OK"
    return max(
        (finding["severity"] for finding in findings),
        key=lambda severity: SEVERITY_RANK[severity],
    )

How the summary works

An empty findings list returns OK. Otherwise the helper uses the same severity map to select the most urgent priority.

  • In main(), find the current findings output:
    findings = analyze_bundle(bundle)
    for finding in findings:
        print(
            f"- {finding['severity']} {finding['title']}: "
            f"{finding['evidence']}"
        )

What does the current output include?

The current loop prints every finding's priority plus title plus evidence. It does not yet summarize the customer context or the total finding count.

  • Replace that output with the summarized version below:
    findings = analyze_bundle(bundle)
    print(f"Analyzed: {bundle.get('customer')} / {bundle.get('service')}")
    print(f"Findings: {len(findings)}")
    print(f"Highest priority: {highest_priority(findings)}")
    for finding in findings:
        print(
            f"- {finding['severity']} {finding['title']}: "
            f"{finding['evidence']}"
        )

What changes in the terminal?

  • The first line identifies the customer plus the affected service.
  • The second line shows how many rules produced findings.
  • The third line shows the highest priority across those findings.
  • The loop preserves the evidence-rich detail underneath the summary.
  • Save deployment_doctor.py.
  • Before running the final check, predict the finding order from the three assigned priorities.
  • Verify the diagnostic engine by running:
uv run deployment_doctor.py samples/failed_bundle.json

What should you see?

You should see Findings: 3 plus Highest priority: P1. The missing configuration finding appears first.

The port conflict follows at P1. The unreachable dependency appears last at P2.

Seeing the wrong count or order?

  • Check that return sorted( remains at the end of analyze_bundle().
  • Check that both P1 findings use the same severity value.
  • Ask for help with the result through this troubleshooting prompt.

✔️ Awesome, I've got everything!

Great work. Save deployment_doctor.py one more time before moving on.

ⓧ I'd like to double check the full code

Compare your complete deployment_doctor.py file with this cumulative version.

# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///

import argparse
import json
from pathlib import Path

SEVERITY_RANK = {"P1": 3, "P2": 2, "P3": 1, "OK": 0}


def load_json(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8"))


def analyze_bundle(bundle: dict) -> list[dict]:
    findings = []
    deployment = bundle.get("deployment", {})
    runtime = bundle.get("runtime", {})

    required_env = set(deployment.get("required_env", []))
    present_env = set(deployment.get("present_env", {}).keys())
    missing_env = sorted(required_env - present_env)
    if missing_env:
        findings.append(
            {
                "id": "missing-environment",
                "severity": "P1",
                "title": "Required environment configuration is missing",
                "evidence": f"Missing names: {', '.join(missing_env)}",
                "impact": "The service cannot initialize its required connections.",
                "runbook_id": "missing_environment",
            }
        )

    desired_port = deployment.get("desired_port")
    for listener in runtime.get("listening_ports", []):
        if desired_port is not None and listener.get("port") == desired_port:
            findings.append(
                {
                    "id": "port-conflict",
                    "severity": "P1",
                    "title": "The requested service port is already occupied",
                    "evidence": (
                        f"Port {desired_port} is owned by "
                        f"{listener.get('process', 'an unknown process')}"
                    ),
                    "impact": "The new service cannot bind to its configured port.",
                    "runbook_id": "port_conflict",
                }
            )

    for dependency in bundle.get("dependencies", []):
        if dependency.get("status") != "healthy":
            findings.append(
                {
                    "id": f"dependency-{dependency.get('name', 'unknown')}",
                    "severity": "P2",
                    "title": "A required dependency is unreachable",
                    "evidence": (
                        f"{dependency.get('name', 'Unknown dependency')} reported "
                        f"status {dependency.get('status', 'unknown')}"
                    ),
                    "impact": "The service may start but cannot complete its business workflow.",
                    "runbook_id": "dependency_unreachable",
                }
            )

    return sorted(
        findings,
        key=lambda finding: SEVERITY_RANK[finding["severity"]],
        reverse=True,
    )


def highest_priority(findings: list[dict]) -> str:
    if not findings:
        return "OK"
    return max(
        (finding["severity"] for finding in findings),
        key=lambda severity: SEVERITY_RANK[severity],
    )


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Turn a customer deployment bundle into a safe diagnostic report."
    )
    parser.add_argument("bundle", type=Path, nargs="?", help="Path to a JSON bundle")
    parser.add_argument("--raw", action="store_true", help="Print the unprocessed bundle")
    return parser


def main() -> None:
    args = build_parser().parse_args()
    if args.bundle is None:
        print("Deployment Doctor workspace ready")
        return

    bundle = load_json(args.bundle)
    if args.raw:
        print(json.dumps(bundle, indent=2, sort_keys=True))
        return

    findings = analyze_bundle(bundle)
    print(f"Analyzed: {bundle.get('customer')} / {bundle.get('service')}")
    print(f"Findings: {len(findings)}")
    print(f"Highest priority: {highest_priority(findings)}")
    for finding in findings:
        print(
            f"- {finding['severity']} {finding['title']}: "
            f"{finding['evidence']}"
        )


if __name__ == "__main__":
    main()

How to compare the file

Check the order of the module-level constant plus each function. Pay close attention to indentation inside analyze_bundle() and main().

Your Deployment Doctor now converts scattered customer symptoms into three prioritized findings. Next up, you will attach reusable runbooks plus human approval states.

Attach Runbooks and Approval Gates

Your diagnostic engine now turns Northstar Retail's bundle into three traceable findings. Responders still have to invent the next action from scratch.

Reusable runbooks give each finding a reviewed response path. Human approval gates keep risky actions advisory until a named responder approves them.

The tool records the decision. It never executes remediation.

In this step, get ready to:
  • Create reusable remediation runbooks for the three diagnostic findings.
  • Add approval-aware enrichment to each finding.
  • Verify how approval states change when a responder is supplied.
Create the runbook library

Each finding already carries a runbook_id. The runbook library turns that identifier into a safe action plus a sequence of remediation checks.

  • In the file sidebar in Visual Studio Code, click the new-file icon beside deployment-doctor.
  • Enter runbooks.json as the file name.
  • Add the first two remediation runbooks by pasting this code:
{
  "missing_environment": {
    "title": "Restore required runtime configuration",
    "safe_action": "Confirm the missing name and retrieve its value through the approved secret-management process.",
    "requires_approval": true,
    "steps": [
      "Confirm the required name against the deployment contract.",
      "Request the value through the customer's approved secret owner.",
      "Restart only during the approved change window and verify service health."
    ]
  },
  "port_conflict": {
    "title": "Resolve the service port conflict",
    "safe_action": "Identify the listening process and validate ownership before stopping or reconfiguring anything.",
    "requires_approval": true,
    "steps": [
      "Confirm which process owns the requested port.",
      "Check whether that process serves active traffic.",
      "Approve either a port change or a controlled process stop."
    ]
  }
}

Why Use Reusable Runbooks?

  • Each runbook gives responders the same reviewed guidance whenever its finding appears.
  • The safe_action describes the next evidence-gathering move without changing the customer environment.
  • The requires_approval value marks configuration restoration plus process changes as gated work.
  • Save runbooks.json.
  • Confirm the file sidebar lists runbooks.json inside deployment-doctor.

You now have a valid runbook library covering the two findings that require approval.

Runbook File Showing Problems?

Check that every property name uses double quotes. Confirm that the comma after the first runbook remains in place.

Compare each opening brace with its closing brace. Help me find the JSON syntax problem in my runbook library.

  • At the bottom of runbooks.json, find these closing lines:
  }
}

What Are These Lines?

The first brace closes the port-conflict runbook. The second brace closes the complete runbook library.

  • Replace those closing lines with the dependency runbook below:
  },
  "dependency_unreachable": {
    "title": "Validate dependency connectivity",
    "safe_action": "Collect DNS, route, and dependency-status evidence before escalating to the dependency owner.",
    "requires_approval": false,
    "steps": [
      "Verify the dependency name and expected endpoint.",
      "Check whether the dependency is in maintenance or degraded.",
      "Attach the evidence and escalate to the dependency owner."
    ]
  }
}

Why Is Approval Not Required?

This runbook gathers connectivity evidence before escalation. Its steps avoid changing configuration or stopping a process.

  • Save runbooks.json.
  • Count the top-level identifiers in the file.

You'll see missing_environment, port_conflict, plus dependency_unreachable.

Only See Two Runbooks?

Confirm that you replaced both final braces. The dependency entry must sit inside the outer object.

Check for the comma immediately before dependency_unreachable. Help me place the dependency runbook correctly.

Add approval-aware enrichment

The diagnostic rules explain what failed. The enrichment function joins each finding to its matching runbook plus an explicit approval state.

  • Switch back to deployment_doctor.py from earlier.
  • Find the highest_priority() function.
  • Add the enrichment function directly above highest_priority() by pasting this code:
def enrich_findings(
    findings: list[dict], runbooks: dict, approved_by: str | None
) -> list[dict]:
    enriched = []
    for finding in findings:
        runbook = runbooks.get(
            finding["runbook_id"],
            {
                "title": "Escalate to the service owner",
                "safe_action": "Collect evidence and stop before changing the environment.",
                "requires_approval": True,
                "steps": ["Assign an owner", "Review the evidence", "Record the decision"],
            },
        )
        requires_approval = runbook.get("requires_approval", True)
        if requires_approval and approved_by:
            approval_status = f"APPROVED BY {approved_by}"
        elif requires_approval:
            approval_status = "PENDING HUMAN APPROVAL"
        else:
            approval_status = "NOT REQUIRED"

        enriched.append(
            {
                **finding,
                "runbook": runbook,
                "approval_status": approval_status,
            }
        )
    return enriched

What Does This Function Do?

  • The runbook_id selects the matching entry from the runbook library.
  • The fallback runbook keeps an unknown identifier on a safe escalation path.
  • The requires_approval value determines whether the responder name must be recorded.
  • Each enriched finding keeps its original evidence plus the selected runbook plus its approval state.
  • Save deployment_doctor.py.
  • Switch back to the Linux terminal from earlier.
  • Confirm the existing diagnostic path still produces findings by running:
uv run deployment_doctor.py samples/failed_bundle.json

What Does This Check Prove?

This run uses the existing diagnostic path before the enrichment function is connected to main(). A successful run proves the new function did not break the current analyzer.

You'll still see three findings for the missing environment configuration, the occupied port, plus the unreachable dependency.

Seeing a Python Syntax Problem?

Confirm that enrich_findings() starts at the left edge of the file. Check that return enriched remains inside the function.

Compare the indentation inside the loop. Help me debug the new enrichment function.

Connect and verify the approval gates

The command-line parser needs paths for reusable runbooks plus a responder name. The main workflow can then enrich findings before displaying them.

  • Return to deployment_doctor.py.
  • Find the complete build_parser() function.
  • Replace that function with this updated version:
def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Turn a customer deployment bundle into a safe diagnostic report."
    )
    parser.add_argument("bundle", type=Path, nargs="?", help="Path to a JSON bundle")
    parser.add_argument(
        "--runbooks",
        type=Path,
        default=Path("runbooks.json"),
        help="Path to the runbook library",
    )
    parser.add_argument("--approved-by", help="Responder approving gated actions")
    parser.add_argument("--raw", action="store_true", help="Print the unprocessed bundle")
    return parser

What Changed in the Parser?

  • The --runbooks option accepts a reusable runbook library.
  • The default path points to runbooks.json in the current workspace.
  • The --approved-by option captures the responder responsible for gated actions.
  • The existing --raw path remains available for diagnostic inspection.
  • Save deployment_doctor.py.
  • Confirm the parser accepts a responder name by running:
uv run deployment_doctor.py samples/failed_bundle.json --approved-by "Demo Responder"

What Does This Parser Check Prove?

The command now accepts the responder option plus its value. The existing three findings still render because main() has not been replaced yet.

You'll see the three diagnostic findings without an option-parsing failure.

Responder Option Rejected?

Confirm that you replaced the full build_parser() function. Save deployment_doctor.py before running the command again.

Check the spelling of --approved-by in the function plus the terminal command. Help me debug the responder option.

  • Find the complete main() function in deployment_doctor.py.
  • Replace that function with this approval-aware version:
def main() -> None:
    args = build_parser().parse_args()
    if args.bundle is None:
        print("Deployment Doctor workspace ready")
        return

    bundle = load_json(args.bundle)
    if args.raw:
        print(json.dumps(bundle, indent=2, sort_keys=True))
        return

    runbooks = load_json(args.runbooks)
    findings = enrich_findings(
        analyze_bundle(bundle), runbooks, approved_by=args.approved_by
    )

    print(f"Analyzed: {bundle.get('customer')} / {bundle.get('service')}")
    print(f"Findings: {len(findings)}")
    print(f"Highest priority: {highest_priority(findings)}")
    for finding in findings:
        print(
            f"- {finding['severity']} {finding['title']} "
            f"[{finding['approval_status']}]"
        )

How Does the Workflow Change?

  • The workflow loads the selected runbook library after the raw-inspection path.
  • The analyzer still produces the same evidence-backed findings.
  • The enrichment function attaches guidance plus an approval state before terminal display.
  • The final loop displays the state without performing any runbook step.
  • Save deployment_doctor.py.

Before you run this check, which findings do you expect to wait for a person?

  • Run the failed bundle without a responder name by using:
uv run deployment_doctor.py samples/failed_bundle.json

What Does the Ungated Run Prove?

The command uses the default runbooks.json library. No responder name is available for the approval checks.

You'll see two P1 findings tagged PENDING HUMAN APPROVAL. The P2 dependency finding is tagged NOT REQUIRED.

Now predict which statuses can change when the responder supplies their name.

  • Run the failed bundle with the demo responder by using:
uv run deployment_doctor.py samples/failed_bundle.json --approved-by "Demo Responder"

What Does the Approved Run Prove?

The responder name is passed into enrich_findings(). Only runbooks marked as requiring approval record that name.

You'll see both P1 findings tagged APPROVED BY Demo Responder. The P2 finding remains NOT REQUIRED.

That is the safety boundary working. Your Deployment Doctor records human authorization while keeping every remediation step advisory.

Approval Status Missing?

Confirm that main() calls enrich_findings() before printing findings. Check that the print statement uses approval_status.

Confirm that the terminal is inside deployment-doctor so the default runbook path resolves. Help me debug the approval status output.

✔️ Awesome, I've got everything!

Great. Double-check that you saved runbooks.json plus deployment_doctor.py.

ⓧ I'd like to double check the full code

Your completed runbook library should match this file:

{
  "missing_environment": {
    "title": "Restore required runtime configuration",
    "safe_action": "Confirm the missing name and retrieve its value through the approved secret-management process.",
    "requires_approval": true,
    "steps": [
      "Confirm the required name against the deployment contract.",
      "Request the value through the customer's approved secret owner.",
      "Restart only during the approved change window and verify service health."
    ]
  },
  "port_conflict": {
    "title": "Resolve the service port conflict",
    "safe_action": "Identify the listening process and validate ownership before stopping or reconfiguring anything.",
    "requires_approval": true,
    "steps": [
      "Confirm which process owns the requested port.",
      "Check whether that process serves active traffic.",
      "Approve either a port change or a controlled process stop."
    ]
  },
  "dependency_unreachable": {
    "title": "Validate dependency connectivity",
    "safe_action": "Collect DNS, route, and dependency-status evidence before escalating to the dependency owner.",
    "requires_approval": false,
    "steps": [
      "Verify the dependency name and expected endpoint.",
      "Check whether the dependency is in maintenance or degraded.",
      "Attach the evidence and escalate to the dependency owner."
    ]
  }
}

What Should Match?

The library contains three identifiers. The first two require approval while the dependency runbook permits evidence collection without approval.

Your completed Python script should match this cumulative version:

# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///

import argparse
import json
from pathlib import Path

SEVERITY_RANK = {"P1": 3, "P2": 2, "P3": 1, "OK": 0}


def load_json(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8"))


def analyze_bundle(bundle: dict) -> list[dict]:
    findings = []
    deployment = bundle.get("deployment", {})
    runtime = bundle.get("runtime", {})

    required_env = set(deployment.get("required_env", []))
    present_env = set(deployment.get("present_env", {}).keys())
    missing_env = sorted(required_env - present_env)
    if missing_env:
        findings.append(
            {
                "id": "missing-environment",
                "severity": "P1",
                "title": "Required environment configuration is missing",
                "evidence": f"Missing names: {', '.join(missing_env)}",
                "impact": "The service cannot initialize its required connections.",
                "runbook_id": "missing_environment",
            }
        )

    desired_port = deployment.get("desired_port")
    for listener in runtime.get("listening_ports", []):
        if desired_port is not None and listener.get("port") == desired_port:
            findings.append(
                {
                    "id": "port-conflict",
                    "severity": "P1",
                    "title": "The requested service port is already occupied",
                    "evidence": (
                        f"Port {desired_port} is owned by "
                        f"{listener.get('process', 'an unknown process')}"
                    ),
                    "impact": "The new service cannot bind to its configured port.",
                    "runbook_id": "port_conflict",
                }
            )

    for dependency in bundle.get("dependencies", []):
        if dependency.get("status") != "healthy":
            findings.append(
                {
                    "id": f"dependency-{dependency.get('name', 'unknown')}",
                    "severity": "P2",
                    "title": "A required dependency is unreachable",
                    "evidence": (
                        f"{dependency.get('name', 'Unknown dependency')} reported "
                        f"status {dependency.get('status', 'unknown')}"
                    ),
                    "impact": "The service may start but cannot complete its business workflow.",
                    "runbook_id": "dependency_unreachable",
                }
            )

    return sorted(
        findings,
        key=lambda finding: SEVERITY_RANK[finding["severity"]],
        reverse=True,
    )


def enrich_findings(
    findings: list[dict], runbooks: dict, approved_by: str | None
) -> list[dict]:
    enriched = []
    for finding in findings:
        runbook = runbooks.get(
            finding["runbook_id"],
            {
                "title": "Escalate to the service owner",
                "safe_action": "Collect evidence and stop before changing the environment.",
                "requires_approval": True,
                "steps": ["Assign an owner", "Review the evidence", "Record the decision"],
            },
        )
        requires_approval = runbook.get("requires_approval", True)
        if requires_approval and approved_by:
            approval_status = f"APPROVED BY {approved_by}"
        elif requires_approval:
            approval_status = "PENDING HUMAN APPROVAL"
        else:
            approval_status = "NOT REQUIRED"

        enriched.append(
            {
                **finding,
                "runbook": runbook,
                "approval_status": approval_status,
            }
        )
    return enriched


def highest_priority(findings: list[dict]) -> str:
    if not findings:
        return "OK"
    return max(
        (finding["severity"] for finding in findings),
        key=lambda severity: SEVERITY_RANK[severity],
    )


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Turn a customer deployment bundle into a safe diagnostic report."
    )
    parser.add_argument("bundle", type=Path, nargs="?", help="Path to a JSON bundle")
    parser.add_argument(
        "--runbooks",
        type=Path,
        default=Path("runbooks.json"),
        help="Path to the runbook library",
    )
    parser.add_argument("--approved-by", help="Responder approving gated actions")
    parser.add_argument("--raw", action="store_true", help="Print the unprocessed bundle")
    return parser


def main() -> None:
    args = build_parser().parse_args()
    if args.bundle is None:
        print("Deployment Doctor workspace ready")
        return

    bundle = load_json(args.bundle)
    if args.raw:
        print(json.dumps(bundle, indent=2, sort_keys=True))
        return

    runbooks = load_json(args.runbooks)
    findings = enrich_findings(
        analyze_bundle(bundle), runbooks, approved_by=args.approved_by
    )

    print(f"Analyzed: {bundle.get('customer')} / {bundle.get('service')}")
    print(f"Findings: {len(findings)}")
    print(f"Highest priority: {highest_priority(findings)}")
    for finding in findings:
        print(
            f"- {finding['severity']} {finding['title']} "
            f"[{finding['approval_status']}]"
        )


if __name__ == "__main__":
    main()

What Should the Script Contain?

The cumulative script contains the original analyzer plus enrich_findings(). Its parser accepts a runbook path plus an optional responder name.

Your findings now come with reusable guidance plus a visible human decision boundary. Next, you'll turn that enriched evidence into a redacted escalation report.

Generate Safe Escalation Reports

Your diagnostic engine now turns customer evidence into prioritized findings with reusable runbooks. Its human approval gates keep risky recommendations under a responder's control.

Support teams still need a shared artifact that they can forward safely. This step adds secret redaction and generates structured Markdown reports without executing remediation.

In this step, get ready to:
  • Recursively replace secret-like values with a safe redaction marker.
  • Build reports that combine business impact, evidence, runbooks, and approval status.
  • Generate failed and healthy reports to verify both diagnostic outcomes.
Add recursive secret redaction

Customer bundles can hide credentials inside nested objects or lists. A recursive redaction function checks every level before the bundle reaches a report.

  • Switch back to deployment_doctor.py in Visual Studio Code.
  • Find the existing SEVERITY_RANK declaration.
  • Insert the sensitive-key markers immediately above that declaration by adding:
SENSITIVE_MARKERS = ("token", "password", "secret", "api_key", "credential")

What do these markers identify?

The tuple lists fragments commonly found in keys that hold sensitive values. Matching the key keeps the report safe even when the credential value has an unfamiliar format.

  • Find the end of enrich_findings().
  • Place the recursive redaction function below it by adding:
def redact(value):
    if isinstance(value, dict):
        return {key: "[REDACTED]" if any(marker in key.lower() for marker in SENSITIVE_MARKERS) else redact(item) for key, item in value.items()}
    if isinstance(value, list):
        return [redact(item) for item in value]
    return value

What does this code do?

  • The dictionary branch checks each key against SENSITIVE_MARKERS.
  • A matching key receives the literal value [REDACTED].
  • The list branch applies the same check to every nested item.
  • Ordinary values pass through unchanged so the report keeps useful evidence.
  • Save deployment_doctor.py.
  • Confirm that the existing diagnostic path still runs by running this command:
uv run deployment_doctor.py samples/failed_bundle.json

What should you see?

You should still see three findings for the failed Northstar bundle. This confirms that the new redaction logic has not disrupted the diagnostic path.

Seeing a Python error?

  • Check that SENSITIVE_MARKERS appears above SEVERITY_RANK.
  • Check that redact() sits outside enrich_findings() with no leading indentation.
  • Compare every bracket in the dictionary comprehension with the code above.
  • Help me debug the recursive redaction function in deployment_doctor.py.
Build the Markdown report

An escalation report needs one overall priority before it presents individual findings. It also needs enough context for support and engineering to make the same decision from the same evidence.

  • Add the priority helper immediately below redact() by using this code:
def highest_priority(findings: list[dict]) -> str:
    if not findings:
        return "OK"
    return max((finding["severity"] for finding in findings), key=lambda severity: SEVERITY_RANK[severity])

How is the overall priority chosen?

The helper returns OK when no findings exist. Otherwise, it uses SEVERITY_RANK to select the most urgent severity.

  • Place the report builder immediately below highest_priority() by adding:
def build_report(bundle: dict, findings: list[dict]) -> str:
    lines = ["# Deployment Doctor Report", "", f"- Customer: {bundle.get('customer', 'Unknown')}", f"- Environment: {bundle.get('environment', 'Unknown')}", f"- Service: {bundle.get('service', 'Unknown')}", f"- Business impact: {bundle.get('business_impact', 'Not supplied')}", f"- Overall priority: {highest_priority(findings)}", "", "## Findings"]
    if not findings:
        lines.extend(["", "No blocking issues were detected by the configured checks."])
    for index, finding in enumerate(findings, start=1):
        runbook = finding["runbook"]
        lines.extend(["", f"### {index}. {finding['title']}", "", f"- Severity: {finding['severity']}", f"- Evidence: {finding['evidence']}", f"- Why it matters: {finding['impact']}", f"- Runbook: {runbook['title']}", f"- Safe next action: {runbook['safe_action']}", f"- Approval: {finding['approval_status']}", "- Runbook steps:"])
        for step in runbook.get("steps", []):
            lines.append(f"  - {step}")
    lines.extend(["", "## Redacted diagnostic snapshot", "", "```json", json.dumps(redact(bundle), indent=2, sort_keys=True), "```", ""])
    return "\n".join(lines)

What does this code do?

  • The report header captures the customer, environment, service, business impact, and overall priority.
  • Each finding includes its evidence, impact, runbook, safe action, approval status, and remediation steps.
  • A healthy bundle receives a clear message stating that no blocking issues were detected.
  • The diagnostic snapshot passes through redact() before it becomes Markdown.
  • Save deployment_doctor.py.
  • Check that the diagnostic engine still produces its three enriched findings by running:
uv run deployment_doctor.py samples/failed_bundle.json

What should you see?

You should see three findings with their existing approval statuses. The next change connects the new report builder to this working diagnostic flow.

Report functions causing an error?

  • Check that highest_priority() appears before build_report().
  • Check that the final line of build_report() remains indented inside the function.
  • Help me debug the report-building functions in deployment_doctor.py.
Wire report output and verify both bundles

The report functions now know how to format safe evidence. The command-line flow needs an output path so it can save that evidence as a reusable customer artifact.

  • Find the --runbooks argument inside build_parser().
  • Add the report output argument immediately below it by using this line:
    parser.add_argument("--output", type=Path, default=Path("reports/deployment-report.md"), help="Path for the Markdown report")

What does this argument control?

The --output option lets each run choose its report path. The default keeps generated files inside the existing reports/ directory.

  • Find the existing main() function near the bottom of deployment_doctor.py.
  • Replace that function and its entry point with this finished version:
def main() -> None:
    args = build_parser().parse_args()
    if args.bundle is None:
        print("Deployment Doctor workspace ready")
        return
    bundle = load_json(args.bundle)
    if args.raw:
        print(json.dumps(bundle, indent=2, sort_keys=True))
        return
    runbooks = load_json(args.runbooks)
    findings = enrich_findings(analyze_bundle(bundle), runbooks, approved_by=args.approved_by)
    report = build_report(bundle, findings)
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(report, encoding="utf-8")
    print(f"Analyzed: {bundle.get('customer')} / {bundle.get('service')}")
    print(f"Findings: {len(findings)}")
    print(f"Highest priority: {highest_priority(findings)}")
    for finding in findings:
        print(f"- {finding['severity']} {finding['title']} [{finding['approval_status']}]")
    print(f"Report: {args.output}")


if __name__ == "__main__":
    main()

How does the finished flow work?

  • The engine loads the selected bundle and preserves the existing raw-inspection path.
  • The standard path analyzes the bundle before attaching runbooks and approval states.
  • The engine passes those findings into build_report().
  • The final lines create the output directory, save the report, and print a concise terminal summary.
  • Save deployment_doctor.py.

✔️ Awesome, I've got everything!

Your finished Python engine is saved. Continue to the report checks below.

ⓧ I'd like to double check the full code

# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///

import argparse
import json
from pathlib import Path

SENSITIVE_MARKERS = ("token", "password", "secret", "api_key", "credential")
SEVERITY_RANK = {"P1": 3, "P2": 2, "P3": 1, "OK": 0}


def load_json(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8"))


def analyze_bundle(bundle: dict) -> list[dict]:
    findings = []
    deployment = bundle.get("deployment", {})
    runtime = bundle.get("runtime", {})
    required_env = set(deployment.get("required_env", []))
    present_env = set(deployment.get("present_env", {}).keys())
    missing_env = sorted(required_env - present_env)
    if missing_env:
        findings.append({"id": "missing-environment", "severity": "P1", "title": "Required environment configuration is missing", "evidence": f"Missing names: {', '.join(missing_env)}", "impact": "The service cannot initialize its required connections.", "runbook_id": "missing_environment"})
    desired_port = deployment.get("desired_port")
    for listener in runtime.get("listening_ports", []):
        if desired_port is not None and listener.get("port") == desired_port:
            findings.append({"id": "port-conflict", "severity": "P1", "title": "The requested service port is already occupied", "evidence": f"Port {desired_port} is owned by {listener.get('process', 'an unknown process')}", "impact": "The new service cannot bind to its configured port.", "runbook_id": "port_conflict"})
    for dependency in bundle.get("dependencies", []):
        if dependency.get("status") != "healthy":
            findings.append({"id": f"dependency-{dependency.get('name', 'unknown')}", "severity": "P2", "title": "A required dependency is unreachable", "evidence": f"{dependency.get('name', 'Unknown dependency')} reported status {dependency.get('status', 'unknown')}", "impact": "The service may start but cannot complete its business workflow.", "runbook_id": "dependency_unreachable"})
    return sorted(findings, key=lambda finding: SEVERITY_RANK[finding["severity"]], reverse=True)


def enrich_findings(findings: list[dict], runbooks: dict, approved_by: str | None) -> list[dict]:
    enriched = []
    for finding in findings:
        runbook = runbooks.get(finding["runbook_id"], {"title": "Escalate to the service owner", "safe_action": "Collect evidence and stop before changing the environment.", "requires_approval": True, "steps": ["Assign an owner", "Review the evidence", "Record the decision"]})
        requires_approval = runbook.get("requires_approval", True)
        approval_status = f"APPROVED BY {approved_by}" if requires_approval and approved_by else "PENDING HUMAN APPROVAL" if requires_approval else "NOT REQUIRED"
        enriched.append({**finding, "runbook": runbook, "approval_status": approval_status})
    return enriched


def redact(value):
    if isinstance(value, dict):
        return {key: "[REDACTED]" if any(marker in key.lower() for marker in SENSITIVE_MARKERS) else redact(item) for key, item in value.items()}
    if isinstance(value, list):
        return [redact(item) for item in value]
    return value


def highest_priority(findings: list[dict]) -> str:
    if not findings:
        return "OK"
    return max((finding["severity"] for finding in findings), key=lambda severity: SEVERITY_RANK[severity])


def build_report(bundle: dict, findings: list[dict]) -> str:
    lines = ["# Deployment Doctor Report", "", f"- Customer: {bundle.get('customer', 'Unknown')}", f"- Environment: {bundle.get('environment', 'Unknown')}", f"- Service: {bundle.get('service', 'Unknown')}", f"- Business impact: {bundle.get('business_impact', 'Not supplied')}", f"- Overall priority: {highest_priority(findings)}", "", "## Findings"]
    if not findings:
        lines.extend(["", "No blocking issues were detected by the configured checks."])
    for index, finding in enumerate(findings, start=1):
        runbook = finding["runbook"]
        lines.extend(["", f"### {index}. {finding['title']}", "", f"- Severity: {finding['severity']}", f"- Evidence: {finding['evidence']}", f"- Why it matters: {finding['impact']}", f"- Runbook: {runbook['title']}", f"- Safe next action: {runbook['safe_action']}", f"- Approval: {finding['approval_status']}", "- Runbook steps:"])
        for step in runbook.get("steps", []):
            lines.append(f"  - {step}")
    lines.extend(["", "## Redacted diagnostic snapshot", "", "```json", json.dumps(redact(bundle), indent=2, sort_keys=True), "```", ""])
    return "\n".join(lines)


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Turn a customer deployment bundle into a safe diagnostic report.")
    parser.add_argument("bundle", type=Path, nargs="?", help="Path to a JSON bundle")
    parser.add_argument("--runbooks", type=Path, default=Path("runbooks.json"), help="Path to the runbook library")
    parser.add_argument("--output", type=Path, default=Path("reports/deployment-report.md"), help="Path for the Markdown report")
    parser.add_argument("--approved-by", help="Responder approving gated actions")
    parser.add_argument("--raw", action="store_true", help="Print the unprocessed bundle")
    return parser


def main() -> None:
    args = build_parser().parse_args()
    if args.bundle is None:
        print("Deployment Doctor workspace ready")
        return
    bundle = load_json(args.bundle)
    if args.raw:
        print(json.dumps(bundle, indent=2, sort_keys=True))
        return
    runbooks = load_json(args.runbooks)
    findings = enrich_findings(analyze_bundle(bundle), runbooks, approved_by=args.approved_by)
    report = build_report(bundle, findings)
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(report, encoding="utf-8")
    print(f"Analyzed: {bundle.get('customer')} / {bundle.get('service')}")
    print(f"Findings: {len(findings)}")
    print(f"Highest priority: {highest_priority(findings)}")
    for finding in findings:
        print(f"- {finding['severity']} {finding['title']} [{finding['approval_status']}]")
    print(f"Report: {args.output}")


if __name__ == "__main__":
    main()

Before you run the failed bundle, predict whether the fictional payment token will survive in the generated report.

  • Generate the failed customer report by running this command:
uv run deployment_doctor.py samples/failed_bundle.json --output reports/failed-report.md

What should you see?

The terminal should report Findings: 3 and identify reports/failed-report.md as the saved report. Required changes should remain PENDING HUMAN APPROVAL.

  • Select reports/failed-report.md from the workspace file tree.
  • Confirm that the report contains three numbered findings.
  • Locate PAYMENT_SERVICE_TOKEN in the redacted diagnostic snapshot.
  • Confirm that its value is [REDACTED].

That is the safety boundary working. Your report keeps the evidence that responders need while removing the fictional token value.

Report missing or unredacted?

  • Check that --output is defined inside build_parser().
  • Check that build_report() calls redact(bundle) before serializing the diagnostic snapshot.
  • Check that PAYMENT_SERVICE_TOKEN still exists in the source bundle with its original fictional value.
  • Help me debug a missing or unredacted failed report.

Approval changes the recorded decision state without changing the evidence or executing a remediation. You can prove that separation by regenerating the same report with a responder name.

Before you rerun the failed bundle, predict whether the finding count or only the approval labels will change.

  • Regenerate the failed report with responder approval by running:
uv run deployment_doctor.py samples/failed_bundle.json --approved-by "Demo Responder" --output reports/failed-report.md

What changes with approval?

The terminal should still report three findings. The two gated findings should display APPROVED BY Demo Responder while the dependency finding remains NOT REQUIRED.

The tool records an approval state only. It does not stop a process, change configuration, or restart a service.

  • Restore the pending-approval version of the failed report by running:
uv run deployment_doctor.py samples/failed_bundle.json --output reports/failed-report.md

What does the restored report prove?

The required actions return to PENDING HUMAN APPROVAL while all three findings remain unchanged. Approval is metadata attached to a recommendation.

Before you run the healthy bundle, predict how many blocking findings the same rules should produce.

  • Generate the healthy customer report by running this command:
uv run deployment_doctor.py samples/healthy_bundle.json --output reports/healthy-report.md

What should you see?

The terminal should show Findings: 0 and Highest priority: OK. The report path should be reports/healthy-report.md.

  • Select reports/healthy-report.md from the workspace file tree.
  • Confirm that the report says no blocking issues were detected.
  • Confirm that the sanitized diagnostic snapshot still describes the healthy Northstar deployment.

Healthy report showing findings?

  • Check that the command uses samples/healthy_bundle.json.
  • Check that the healthy bundle contains all required environment names.
  • Check that its desired port does not match the listed listener.
  • Help me debug unexpected findings in the healthy report.

Your Deployment Doctor now produces a safe shared artifact for failed deployments and a clean result for healthy ones. The same engine preserves evidence, redacts secret-like values, and records human approval without changing the customer environment.

Secret mission

Onboard a Second Customer

Onboard Fabrikam Warehousing with its own diagnostic bundle and runbook library. Generate a customer-specific escalation report without changing the Python engine.

Clean Up Your Resources

Clean Up Your Resources

Everything you created lives inside the local deployment-doctor workspace at no ongoing cost. Choose whether to keep the files, pause your work, or delete the workspace.

Resources you used:

  • The deployment_doctor.py Python engine.
  • The samples directory containing all three customer JSON diagnostic bundles.
  • The runbooks.json and runbooks_customer_b.json runbook libraries.
  • The reports directory containing the generated Markdown escalation reports.

Keep everything running

No action needed. Choose this if you want to keep demonstrating the Deployment Doctor or revisit its customer workflows later.

  • Keep the deployment-doctor folder on your Linux device.
  • Use the reports in reports to demonstrate the Northstar Retail and Fabrikam Warehousing workflows.

Pause - I'll come back to this later

Pausing leaves every project file in place while ending your current work session.

  • Save each open file in Visual Studio Code.
  • Close Visual Studio Code.
  • Close the Linux terminal.

The Deployment Doctor runs only when invoked, so no background process or cloud resource remains active.

Delete - I don't want to use this again

Deleting the workspace is permanent. The command below targets only the local deployment-doctor folder.

  • Review the deployment-doctor workspace in the editor from earlier for any files you still need.
  • Copy any files you want to keep outside the deployment-doctor folder.
  • Close Visual Studio Code.
  • Return to the parent directory that contains deployment-doctor in the Linux terminal from earlier.
  • Remove the local workspace by running this command:
rm -rf deployment-doctor

What Does This Command Do?

The rm -rf deployment-doctor command removes the complete workspace. This deletes the Python engine, diagnostic bundles, runbook libraries, and generated reports while leaving Visual Studio Code and uv installed.

  • Confirm in the parent directory that the deployment-doctor folder is no longer present.

Workspace Still Present?

  • Confirm that your terminal is in the parent directory containing deployment-doctor.
  • Check that the folder name uses the exact spelling deployment-doctor.

Help me safely remove my local Deployment Doctor workspace.

Nice Work!

Nice Work!

You did it! You built a customer-facing Deployment Doctor that turns customer diagnostic bundles into safe escalation reports.

You've learned how to:

  • Define a clear JSON input contract that lets the same Python engine process structured deployment evidence from different customers.
  • Turn missing configuration, an occupied port, or an unreachable dependency into prioritized findings backed by customer evidence. Match each finding to a reusable runbook with an explicit human approval state.
  • Generate redacted Markdown escalation reports for failed deployments. Confirm healthy bundles produce no blocking issues.
  • Secret Mission: Onboard Fabrikam Warehousing with samples/warehouse_bundle.json plus runbooks_customer_b.json. Keep deployment_doctor.py unchanged.

Ready to quiz yourself?