Build a Release Readiness API

Design, test, deploy, and observe a release-readiness API with Node.js.

Introduction

30 Second Summary

A software release can feel ready until one overlooked check turns launch day into a scramble. Teams need a shared decision they can explain before a change goes live.

In this project, you will design a release-readiness API with Node.js. You will ship it through a tested GitHub Actions pipeline to Render.

What You'll Build

You will watch release evidence become a clear live decision through a tested delivery path.

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

  • A live release check that turns evidence sent to POST /assess into ready or blocked with exact blockers.
  • A design you can defend through a product specification plus an architecture decision. Each document explains the service boundary.
  • A gated delivery path where GitHub Actions tests every push before Render deploys it. You can confirm the live result through GET /health. You can trace requests through structured JSON logs. Resource graphs show how the service behaves.
  • Secret Mission: Add rollback-plan readiness to the product specification. Protect the policy with a failing test. Ship the completed change through automated checks to the live service.

Are there any prerequisites?

Comfort with small JavaScript programs is enough. No system design experience is required.

Step 1 verifies the required Windows workspace plus account access before any design work begins.

Before We Start

A clear problem statement keeps every later design decision tied to a real need. Before the hands-on work begins, commit to who needs ShipCheck, what decision it makes, and why consistency matters.

Prepare the AI-Assisted Development Workspace

Your problem statement gives ShipCheck a clear purpose. The next challenge is making every build decision traceable from a reproducible workspace.

Visual Studio Code gives the project one working surface. GitHub Copilot adds an assistant whose suggestions you can review.

Node.js fixes the runtime used throughout the project. Git creates the version-control boundary that records each decision.

In this step, get ready to:
  • Activate GitHub Copilot Free in Visual Studio Code.
  • Verify the required Node.js runtime plus your Git installation.
  • Create the ShipCheck repository structure on your Desktop.
Activate GitHub Copilot Free

GitHub Copilot can help turn your decisions into drafts. You remain responsible for checking every suggestion against the problem you defined.

This setup stays on Copilot Free. No paid additional usage is needed for ShipCheck.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.
  • Select the Copilot control in Visual Studio Code.
  • Select Sign up for Copilot Free.
  • Sign in with the GitHub account you confirmed for this project.
  • Keep the account on Copilot Free if plan choices appear.

Once the sign-in finishes, a short response proves that Copilot Chat is available inside your editor.

  • Press Ctrl+Alt+I to open the Chat view.
  • Ask Copilot for an exact readiness response by sending this prompt:
Reply with exactly: ShipCheck workspace ready

Why use an exact response?

A fixed response makes this an observable check. Seeing the requested text confirms that the assistant can receive a prompt inside Visual Studio Code.

You should see ShipCheck workspace ready in Copilot Chat. That confirms your AI-assisted workspace is connected.

Copilot did not respond?

Check that Visual Studio Code shows your GitHub account as signed in. Reopen the Copilot control if the sign-in flow did not finish.

Make sure the account remains on Copilot Free. Paid additional usage is unnecessary for this project.

Help me troubleshoot GitHub Copilot Free in Visual Studio Code.

Verify Node.js and Git

ShipCheck uses an exact Node.js runtime so local behavior matches later automation. PowerShell gives you a direct way to check which runtime Windows is using.

  • Select Terminal in the Visual Studio Code menu bar.
  • Select New Terminal to open PowerShell.
  • Check the active Node.js version by running:
node -v

What does this command check?

The command asks Node.js to print the installed runtime version. ShipCheck requires v24.20.0 so every later command uses the pinned LTS release.

✔️ I see v24.20.0

Your Node.js runtime matches the project pin. The application and later automated checks can now use the same version.

ⓧ I see an older or different version

Your current Node.js installation does not match ShipCheck's runtime. The official Windows installer updates it to the required LTS release.

  • Open the official Node.js release page.
  • Download node-v24.20.0-x64.msi for an x64 Windows computer.
  • Download node-v24.20.0-arm64.msi for an Arm64 Windows computer.
  • Run the installer that matches your computer.
  • Approve the Windows permission request if it appears.
  • Keep the installer defaults.
  • Close Visual Studio Code after the installation finishes.
  • Reopen Visual Studio Code through Windows search.
  • Confirm the new runtime from a fresh PowerShell terminal by running:
node -v

What should I see?

The fresh terminal should print v24.20.0. Reopening Visual Studio Code ensures the terminal can find the updated installation.

ⓧ Command not found

Windows cannot currently find Node.js. Installing the official LTS release adds the runtime needed by ShipCheck.

  • Open the official Node.js release page.
  • Download node-v24.20.0-x64.msi for an x64 Windows computer.
  • Download node-v24.20.0-arm64.msi for an Arm64 Windows computer.
  • Run the installer that matches your computer.
  • Approve the Windows permission request if it appears.
  • Keep the installer defaults.
  • Close Visual Studio Code after the installation finishes.
  • Reopen Visual Studio Code through Windows search.
  • Verify the installation from a fresh PowerShell terminal by running:
node -v

What should I see?

The new terminal should print v24.20.0. That output confirms Windows can now find the required runtime.

Git records how ShipCheck changes from design through deployment. Confirming the command now prevents version-control problems after files exist.

  • Check that Git is available by running:
git --version

What does this command check?

The command prints the installed Git version. Any installed version shown here confirms that PowerShell can access Git.

You should see a line containing the installed Git version. Your runtime plus version-control tools are now ready.

Git command not available?

Close the current terminal after confirming Git is installed on Windows. Open a fresh PowerShell terminal so it reloads the system command paths.

Help me troubleshoot Git in the Visual Studio Code terminal.

Create the ShipCheck repository

The repository gives ShipCheck a clear boundary on your computer. Its folders separate application code, tests, design documents, and delivery automation from the start.

  • Create shipcheck-api on your Desktop by running these commands in PowerShell:
Set-Location ~/Desktop
New-Item -Path ./shipcheck-api -ItemType Directory
Set-Location ./shipcheck-api
code .

What do these commands do?

  • The first command moves PowerShell to your Desktop.
  • The second command creates the shipcheck-api folder.
  • The third command moves PowerShell inside that folder.
  • The final command opens the folder as your Visual Studio Code workspace.
  • Confirm that Visual Studio Code shows shipcheck-api in the Explorer sidebar.

Folder did not open?

Check the PowerShell output for a path error. Make sure the Desktop path exists under your Windows user folder.

Help me open shipcheck-api in Visual Studio Code.

A version-control repository tracks files from their first meaningful state. The initial folders also preserve the component boundaries you will use in later steps.

  • Initialize Git on the main branch.
  • Create the project directories.
  • Add the pinned Node.js version file by running:
git init -b main
New-Item -Path ./src, ./test, ./docs, ./.github -ItemType Directory
New-Item -Path ./.github/workflows -ItemType Directory
Set-Content -Path ./.node-version -Value '24.20.0'
Get-Content -Path ./.node-version

What does this setup create?

  • Git initializes the repository with main as its first branch.
  • The src folder holds the API implementation.
  • The test folder holds automated contract checks.
  • The docs folder holds the product specification plus architecture decision.
  • The .github/workflows folder holds the delivery workflow added later.
  • The .node-version file pins the project to Node.js 24.20.0.

The final line should print 24.20.0. That value matches the runtime you verified.

Repository structure missing something?

Check that the terminal path ends with shipcheck-api before rerunning the setup commands. A different path creates the folders outside your repository.

Help me check my ShipCheck repository structure.

  • Display the repository contents by running:
Get-ChildItem -Path . -Force
Get-ChildItem -Path ./.github -Force

What should I see?

The first listing should include .git, .github, docs, src, test, and .node-version.

The second listing should include workflows. These names confirm the repository boundary is in place.

Hidden folders not listed?

Make sure the first command includes -Force. PowerShell uses that option to include hidden items such as .git.

Help me verify hidden ShipCheck folders in PowerShell.

Use this comparison to confirm the complete contents of the version file.

✔️ Awesome, I've got everything!

Great. Confirm that .node-version is saved inside shipcheck-api.

ⓧ I'd like to double check the full code

24.20.0

Before the final check, do you expect PowerShell to confirm one runtime, one Git installation, and one repository on main?

  • Verify the complete workspace state by running:
node -v
git --version
git status

What should I see?

  • The Node.js check prints v24.20.0.
  • The Git check prints an installed version.
  • Git status includes On branch main.
  • Git status lists .node-version as a new untracked file.
  • Press Ctrl+Alt+I to return to Copilot Chat.
  • Confirm that the chat still shows ShipCheck workspace ready.

Final check looks different?

Check that the terminal path ends with shipcheck-api. Running Git status from the Desktop checks the wrong location.

If the Node.js version is still different, close every Visual Studio Code window after installation. Reopen the workspace before checking again.

Help me debug my final ShipCheck workspace check.

Your workspace now has a pinned runtime, an initialized repository, and a working AI assistant. Next up, you will turn the release problem into a written design plus the first runnable ShipCheck API.

Turn the Problem into a Design and Working API

Your reproducible workspace is ready. ShipCheck now needs a written API contract and a clear system boundary before code starts making release decisions.

GitHub Copilot can accelerate the first draft. You remain responsible for every requirement and architecture choice that enters the project.

A first happy path can look convincing even when caller data crosses the boundary unchecked. You will test that confidence with a second request after the service works.

In this step, get ready to:
  • Define ShipCheck's product contract and acceptance criteria.
  • Record the service boundary and architecture trade-offs.
  • Run the first API and challenge its input handling.
Define the product contract

A product specification turns an ambiguous release problem into rules that can be checked. Its JSON contract defines the evidence callers provide and the decision they receive.

  • Return to the Copilot Chat from earlier.
  • Ask Copilot to turn your opening problem sentence into a one-page product specification.
  • Require the draft to cover the user and input contract.
  • Require the draft to cover decision rules and acceptance criteria.
  • Require the draft to include explicit non-goals.
  • Review every line against the release decision you described.
  • Keep only wording that you can explain in your own words.
  • In the VS Code file sidebar from earlier, create product-spec.md inside the existing docs folder.
  • Add the problem, user, and core contract by copying this first section:
# ShipCheck Product Specification

## Problem

Small delivery teams need a consistent way to decide whether a software change is ready to release. Informal decisions can miss required evidence and make release outcomes difficult to explain.

## User

A developer or release coordinator preparing a software change for deployment.

## API Contract

`POST /assess` accepts a JSON object with three Boolean fields:

- `testsPassed`
- `reviewApproved`
- `monitoringReady`

A valid request returns:

- `ready` when every field is `true`
- `blocked` with the field names that are `false`

What Does This Section Decide?

  • The problem section explains why release evidence needs one consistent decision.
  • The user section identifies who relies on that decision.
  • The contract names the three required signals.
  • The two outcomes make the decision observable to a caller.
  • Save docs/product-spec.md.
  • Read the three field names under the API contract.

You should see testsPassed, reviewApproved, and monitoringReady listed as the complete release evidence.

Missing a Contract Field?

Check that product-spec.md sits inside the existing docs folder. Compare the three field names character by character.

Ask for help checking the first section of your specification.

  • Append the remaining contract rules and project boundaries below the existing content:
A request that is not a JSON object, contains invalid JSON, or uses a non-Boolean value returns a client error and does not produce a readiness decision.

`GET /health` returns a successful response while this stateless process is ready to handle requests.

## Acceptance Criteria

1. All three checks set to `true` produce `ready` with no blockers.
2. Any check set to `false` produces `blocked` and names every failed check.
3. Missing or non-Boolean checks are rejected.
4. Invalid JSON is rejected.
5. Unknown routes return a not-found response.
6. Every handled request produces a structured log record.

## Non-Goals

- User authentication
- Persistent release history
- Team-specific policy configuration
- Notifications
- A browser user interface

Why Define Acceptance Criteria?

Acceptance criteria convert product intent into outcomes that can be tested. The non-goals protect this small service from expanding into authentication or persistent storage.

Some criteria describe later operational work. Recording them now keeps the implementation tied to one agreed destination.

  • Save docs/product-spec.md.
  • Review each acceptance criterion against the API contract above it.

You should see six observable criteria and five explicit non-goals. The specification now separates required behavior from future possibilities.

Does the Specification Feel Inconsistent?

Check that each acceptance criterion describes an outcome from the contract. Remove any Copilot suggestion that adds authentication or persistent release history.

Ask for help reviewing the product boundary.

✔️ Awesome, I've got everything!

Your product specification now defines the release decision that every later artifact must preserve.

ⓧ I'd like to double check the full code

  • Compare your complete docs/product-spec.md file with this reference:
# ShipCheck Product Specification

## Problem

Small delivery teams need a consistent way to decide whether a software change is ready to release. Informal decisions can miss required evidence and make release outcomes difficult to explain.

## User

A developer or release coordinator preparing a software change for deployment.

## API Contract

`POST /assess` accepts a JSON object with three Boolean fields:

- `testsPassed`
- `reviewApproved`
- `monitoringReady`

A valid request returns:

- `ready` when every field is `true`
- `blocked` with the field names that are `false`

A request that is not a JSON object, contains invalid JSON, or uses a non-Boolean value returns a client error and does not produce a readiness decision.

`GET /health` returns a successful response while this stateless process is ready to handle requests.

## Acceptance Criteria

1. All three checks set to `true` produce `ready` with no blockers.
2. Any check set to `false` produces `blocked` and names every failed check.
3. Missing or non-Boolean checks are rejected.
4. Invalid JSON is rejected.
5. Unknown routes return a not-found response.
6. Every handled request produces a structured log record.

## Non-Goals

- User authentication
- Persistent release history
- Team-specific policy configuration
- Notifications
- A browser user interface

What Does the Full File Contain?

This reference combines the problem, contract, acceptance criteria, and non-goals into one product specification. Every later implementation decision traces back to this file.

Record the system boundary

An architecture decision record captures what the system owns and why its structure fits the problem. ShipCheck needs a stateless service because each assessment depends only on the request in front of it.

A dependency-free Node.js service exposes the HTTP boundary directly. This keeps the exercise focused on contracts and component responsibilities.

  • Return to Copilot Chat.
  • Ask Copilot to compare a dependency-free Node.js HTTP service with a web framework.
  • Ask it to separate business rules from HTTP handling.
  • Challenge each suggested component by asking whether the product specification requires it.
  • Reject any database because the current decision needs no release history.
  • In the VS Code file sidebar, create architecture.md inside the existing docs folder.
  • Add the boundary, components, and data flow by copying this first section:
# ShipCheck Architecture Decision

## System Boundary

ShipCheck receives release evidence, evaluates a deterministic policy, and returns a decision. It does not own test execution, code review, monitoring configuration, or release history.

## Components

1. An HTTP client sends a JSON request.
2. The Node.js HTTP layer handles routing, parsing, and responses.
3. The release-readiness module evaluates the policy.

## Data Flow

`client -> HTTP boundary -> policy evaluation -> JSON response`

What Does the Boundary Protect?

  • The system boundary limits ShipCheck to evaluating evidence supplied by a caller.
  • The HTTP layer owns transport concerns such as routing and parsing.
  • The release-readiness module owns the deterministic policy.
  • The data flow shows the order in which a request crosses those components.
  • Save docs/architecture.md.
  • Trace the data flow from the client to the JSON response.

You should see policy evaluation separated from HTTP routing. This boundary lets the release rule change without rewriting request handling.

Are the Responsibilities Blurred?

Check that the release-readiness module evaluates policy only. Keep routing and request parsing in the HTTP layer.

Ask for help reviewing the component boundary.

  • Append the decision, rationale, and trade-offs below the data flow:
## Decision

Use a stateless Node.js 24.20.0 service built only with stable core modules.

## Why This Fits

- The decision is deterministic and needs no database.
- Separating policy from transport makes the rule easy to test.
- Avoiding an external web framework keeps this project focused on boundaries and contracts.

## Trade-offs

- Core HTTP needs more manual routing and body parsing than a framework.
- No database means there is no audit history of prior assessments.

Why a Core HTTP Service?

A web framework could reduce manual routing code. The dependency-free approach keeps the system mechanics visible for this small deterministic API.

The trade-off is extra request parsing code. That cost is small enough here because ShipCheck has one decision endpoint.

  • Save docs/architecture.md.
  • Review the decision against the non-goals in docs/product-spec.md.

You should see Node.js 24.20.0 recorded with a stateless core-module design. The document also names the cost of manual HTTP handling.

Did Copilot Add Extra Infrastructure?

Remove databases and external frameworks from this decision. Neither one serves the current deterministic policy.

Ask for help checking the architecture against the specification.

✔️ Awesome, I've got everything!

Your architecture now explains the service boundary and the trade-offs behind it.

ⓧ I'd like to double check the full code

  • Compare your complete docs/architecture.md file with this reference:
# ShipCheck Architecture Decision

## System Boundary

ShipCheck receives release evidence, evaluates a deterministic policy, and returns a decision. It does not own test execution, code review, monitoring configuration, or release history.

## Components

1. An HTTP client sends a JSON request.
2. The Node.js HTTP layer handles routing, parsing, and responses.
3. The release-readiness module evaluates the policy.

## Data Flow

`client -> HTTP boundary -> policy evaluation -> JSON response`

## Decision

Use a stateless Node.js 24.20.0 service built only with stable core modules.

## Why This Fits

- The decision is deterministic and needs no database.
- Separating policy from transport makes the rule easy to test.
- Avoiding an external web framework keeps this project focused on boundaries and contracts.

## Trade-offs

- Core HTTP needs more manual routing and body parsing than a framework.
- No database means there is no audit history of prior assessments.

What Does the Full File Contain?

This reference combines the boundary, component flow, decision, rationale, and trade-offs. Together they explain how ShipCheck stays small without hiding its HTTP mechanics.

Build and challenge the first API

The first implementation turns the written decision into a runnable service. Its evaluator keeps policy separate from transport while the server handles requests and responses.

  • In the VS Code file sidebar, create release-readiness.js inside the existing src folder.
  • Add the first release policy by copying this code:
const RELEASE_CHECKS = [
  'testsPassed',
  'reviewApproved',
  'monitoringReady'
];

function assessRelease(payload) {
  const blockers = RELEASE_CHECKS.filter((field) => !payload[field]);

  return {
    status: blockers.length === 0 ? 'ready' : 'blocked',
    blockers
  };
}

module.exports = {
  RELEASE_CHECKS,
  assessRelease
};

What Does This Module Do?

  • RELEASE_CHECKS keeps the policy field names in one ordered list.
  • assessRelease examines the supplied value for each field.
  • The returned object contains one status and the complete blocker list.
  • The exports let the HTTP server reuse the policy without owning it.
  • Save src/release-readiness.js.
  • Check the VS Code file sidebar for src/release-readiness.js.

You should see the policy module inside src with RELEASE_CHECKS and assessRelease defined.

Is the Policy Module Missing?

Check that the filename is exactly release-readiness.js and that it sits inside src.

Ask for help checking the module structure.

✔️ Awesome, I've got everything!

Your release policy now lives in its own reusable module.

ⓧ I'd like to double check the full code

  • Compare your complete src/release-readiness.js file with this reference:
const RELEASE_CHECKS = [
  'testsPassed',
  'reviewApproved',
  'monitoringReady'
];

function assessRelease(payload) {
  const blockers = RELEASE_CHECKS.filter((field) => !payload[field]);

  return {
    status: blockers.length === 0 ? 'ready' : 'blocked',
    blockers
  };
}

module.exports = {
  RELEASE_CHECKS,
  assessRelease
};

How Is the Module Structured?

The file defines one ordered policy and one evaluator. Its exports provide the exact boundary that the server needs.

  • In the VS Code file sidebar, create server.js inside the existing src folder.
  • Add the imports and request-body reader by copying this first section:
const http = require('node:http');
const { assessRelease } = require('./release-readiness');

function readJsonBody(req) {
  return new Promise((resolve, reject) => {
    let rawBody = '';

    req.on('data', (chunk) => {
      rawBody += chunk;
    });

    req.on('end', () => {
      try {
        resolve(JSON.parse(rawBody || '{}'));
      } catch (error) {
        reject(error);
      }
    });

    req.on('error', reject);
  });
}

What Does the Body Reader Do?

  • The HTTP import provides the built-in server implementation.
  • The policy import gives the transport layer access to assessRelease.
  • readJsonBody collects incoming chunks into one request body.
  • The promise resolves with parsed JSON or rejects when parsing fails.
  • Save src/server.js.
  • Check the VS Code file sidebar for src/server.js.

You should see both JavaScript files together inside src.

Is the Import Path Wrong?

Check that both JavaScript files sit in the same src folder. The relative import must point to ./release-readiness.

Ask for help checking the server imports.

  • Append the request handler and listener below readJsonBody by copying this code:
const server = http.createServer(async (req, res) => {
  if (req.method === 'POST' && req.url === '/assess') {
    try {
      const payload = await readJsonBody(req);
      const assessment = assessRelease(payload);

      res.writeHead(200, {
        'Content-Type': 'application/json; charset=utf-8'
      });
      res.end(JSON.stringify(assessment));
    } catch {
      res.writeHead(400, {
        'Content-Type': 'application/json; charset=utf-8'
      });
      res.end(JSON.stringify({ error: 'Request body must contain valid JSON.' }));
    }
    return;
  }

  res.writeHead(404, {
    'Content-Type': 'application/json; charset=utf-8'
  });
  res.end(JSON.stringify({ error: 'Route not found.' }));
});

server.listen(3000, () => {
  console.log('ShipCheck listening on port 3000');
});

How Does the Server Handle Requests?

  • The server sends POST requests for /assess through the body reader and policy module.
  • A valid body receives the assessment as JSON.
  • Invalid JSON receives a client error response.
  • Every other route receives a not-found response.
  • The listener exposes the service locally on port 3000.
  • Save src/server.js.

✔️ Awesome, I've got everything!

Your HTTP server now connects incoming assessment requests to the release policy.

ⓧ I'd like to double check the full code

  • Compare your complete src/server.js file with this reference:
const http = require('node:http');
const { assessRelease } = require('./release-readiness');

function readJsonBody(req) {
  return new Promise((resolve, reject) => {
    let rawBody = '';

    req.on('data', (chunk) => {
      rawBody += chunk;
    });

    req.on('end', () => {
      try {
        resolve(JSON.parse(rawBody || '{}'));
      } catch (error) {
        reject(error);
      }
    });

    req.on('error', reject);
  });
}

const server = http.createServer(async (req, res) => {
  if (req.method === 'POST' && req.url === '/assess') {
    try {
      const payload = await readJsonBody(req);
      const assessment = assessRelease(payload);

      res.writeHead(200, {
        'Content-Type': 'application/json; charset=utf-8'
      });
      res.end(JSON.stringify(assessment));
    } catch {
      res.writeHead(400, {
        'Content-Type': 'application/json; charset=utf-8'
      });
      res.end(JSON.stringify({ error: 'Request body must contain valid JSON.' }));
    }
    return;
  }

  res.writeHead(404, {
    'Content-Type': 'application/json; charset=utf-8'
  });
  res.end(JSON.stringify({ error: 'Route not found.' }));
});

server.listen(3000, () => {
  console.log('ShipCheck listening on port 3000');
});

How Does the Full Server Fit Together?

The complete file parses request bodies before handing release evidence to the policy module. It then converts the result into an HTTP response.

Before you run the service, what single terminal message would prove that the listener reached port 3000?

  • Start the local ShipCheck service from the repository by running:
node src/server.js

What Does This Command Do?

Node.js loads src/server.js and starts the HTTP listener. The process stays active so another terminal can send requests to it.

You should see ShipCheck listening on port 3000 in the terminal. Your first runnable system is now waiting for release evidence.

Does the Service Fail to Start?

Check both files for missing brackets or mismatched quotes. Confirm that the terminal is running from the shipcheck-api folder.

Ask for help diagnosing the startup problem.

  • Create a second PowerShell terminal in the VS Code terminal panel.
  • Keep the first terminal running the ShipCheck service.
  • Send a request where every release signal is a Boolean true by running:
Invoke-RestMethod -Uri 'http://localhost:3000/assess' -Method Post -ContentType 'application/json' -Body '{"testsPassed":true,"reviewApproved":true,"monitoringReady":true}'

What Does This Request Test?

The request sends all three required release signals to POST /assess. Each signal uses a Boolean value that matches the written contract.

You should see ready as the status and an empty blockers collection. The happy path now works from HTTP request to policy response.

Did the Valid Request Fail?

Confirm that the first terminal still shows the running service. Check that the request URL ends with /assess.

Ask for help checking the valid request.

Before you send the next request, do you think quotation marks around false will change the decision?

  • Challenge the API with a string-valued test result by running:
Invoke-RestMethod -Uri 'http://localhost:3000/assess' -Method Post -ContentType 'application/json' -Body '{"testsPassed":"false","reviewApproved":true,"monitoringReady":true}'

Why Does Malformed Evidence Pass?

PowerShell still shows ready with no blockers. This is the intended shortfall in the first design.

The non-empty string "false" is truthy in JavaScript. The current filter therefore treats that field as a passed check.

This result violates the specification because the caller supplied a string instead of a Boolean. The visible mismatch proves that the boundary cannot trust caller data.

Did the Request Return Blocked?

Check that quotation marks surround false inside the request body. A Boolean false correctly becomes a blocker in this first evaluator.

Ask for help reproducing the boundary failure.

The failure is now evidence instead of a hunch. Preserve this exact first version so the next step can prove the correction.

  • Stop the running service using the stop control in its terminal.
  • Use the second terminal for the repository snapshot commands below.

A Git commit gives the naive implementation a named checkpoint. The next change can then show exactly how the contract becomes stricter.

  • Stage the project files and record the initial API by running these commands:
git add .
git commit -m "feat: add initial readiness API"
git status

What Does This Snapshot Preserve?

The first command stages the specification, architecture, and implementation files. The second records them under the agreed commit message.

The final command checks whether any project changes remain outside the commit.

You should see a new commit recorded on the main branch. The final status should confirm that no project changes remain uncommitted.

Did the Commit Fail?

Check that all four new files are saved before staging them. Confirm that the terminal is still inside the shipcheck-api repository.

Ask for help completing the snapshot.

Your product contract and architecture now frame a working API with one proven boundary failure. Next, you will turn that failure into automated tests before changing the evaluator.

Make the Contract Trustworthy with Tests

ShipCheck can already return a release decision through its API. The string value "false" has exposed a gap where caller data can cross the boundary without matching the contract.

That incorrect ready result gives you a concrete failure to protect against. The Node.js test runner lets automated tests define the expected behavior before you tighten the evaluator.

In this step, get ready to:
  • Create four boundary tests for the three-signal contract.
  • Run the tests against the loose evaluator to expose the contract failure.
  • Replace truthiness with explicit input validation.
Define the boundary cases

A boundary-focused test pairs one input shape with one exact result. These cases turn the acceptance criteria in docs/product-spec.md into checks that can run after every change.

  • Ask GitHub Copilot Free to critique the string-value misclassification from the previous step.
  • Ask Copilot to propose ready, blocked, malformed-object, plus non-Boolean cases.
  • Compare the proposed cases with the acceptance criteria in docs/product-spec.md.
  • Create test/release-readiness.test.js inside the existing test folder by using the VS Code file tree.
  • Add the test imports plus the ready case by pasting this code into test/release-readiness.test.js:
const test = require('node:test');
const assert = require('node:assert/strict');
const { assessRelease } = require('../src/release-readiness');

test('returns ready when every release check passes', () => {
  const result = assessRelease({
    testsPassed: true,
    reviewApproved: true,
    monitoringReady: true
  });

  assert.deepStrictEqual(result, {
    ok: true,
    status: 'ready',
    blockers: []
  });
});

What does this test protect?

  • The node:test module registers a case for the built-in test runner.
  • The input sets every required release signal to true.
  • The strict assertion protects the entire ready response.
  • Save test/release-readiness.test.js.
  • Check the first contract example by running this command in the VS Code terminal:
node --test

What should I see?

The test runner reports one failing test. The evaluator returns the correct status but omits the required ok field.

Test file not running?

Check that the file path is test/release-readiness.test.js inside the existing ShipCheck repository.

Confirm that the terminal still points to the repository on branch main.

Help me diagnose why the Node.js test runner cannot find my ShipCheck test file.

A blocked response must preserve every failed release signal. The next case protects that complete blocker list.

  • Append the blocked case below the ready test in test/release-readiness.test.js:
test('returns every failed release check as a blocker', () => {
  const result = assessRelease({
    testsPassed: false,
    reviewApproved: true,
    monitoringReady: false
  });

  assert.deepStrictEqual(result, {
    ok: true,
    status: 'blocked',
    blockers: ['testsPassed', 'monitoringReady']
  });
});

What does this test protect?

  • The input fails testsPassed plus monitoringReady.
  • The expected blockers array includes both field names.
  • The response must identify the decision as blocked.
  • Save test/release-readiness.test.js.
  • Check both decision cases by running the suite again:
node --test

What should I see now?

The test runner reports two failing tests. Both failures show that the loose evaluator does not return the complete response contract.

Blocked case missing?

Confirm that the second test() call begins after the ready case closes.

Check that both blocker names match RELEASE_CHECKS exactly.

Help me find why the blocked ShipCheck test is missing from the test run.

The contract accepts a JSON object before it evaluates any release signal. Passing null tests that object boundary directly.

  • Append the malformed-object case below the blocked test in test/release-readiness.test.js:
test('rejects a request body that is not an object', () => {
  const result = assessRelease(null);

  assert.deepStrictEqual(result, {
    ok: false,
    error: 'Request body must be a JSON object.'
  });
});

What does this test protect?

  • The null input cannot contain the required release fields.
  • The expected result rejects the body before policy evaluation.
  • The error gives the HTTP layer a useful reason to return to the caller.
  • Save test/release-readiness.test.js.
  • Check the object boundary by running the expanded suite:
node --test

What should I see now?

The test runner reports three failing tests. The malformed-object case fails because the loose evaluator tries to read release fields from null.

This result proves that object validation must happen before policy evaluation.

Malformed case behaving differently?

Confirm that the test passes null directly to assessRelease().

Check that the expected result contains only ok plus error.

Help me compare the malformed-object test with the ShipCheck contract.

The final case captures the original string-value defect. It also proves that a missing required field belongs in the validation result.

  • Append the non-Boolean case below the malformed-object test in test/release-readiness.test.js:
test('rejects missing or non-Boolean release evidence', () => {
  const result = assessRelease({
    testsPassed: 'false',
    reviewApproved: true
  });

  assert.deepStrictEqual(result, {
    ok: false,
    error: 'Each release check must be true or false.',
    invalidFields: ['testsPassed', 'monitoringReady']
  });
});

What does this test protect?

  • The string assigned to testsPassed violates the Boolean contract.
  • The missing monitoringReady value also fails the field type check.
  • The expected invalidFields array identifies both problems.
  • Save test/release-readiness.test.js.

Before you run the complete suite, do you think a truthiness check can enforce a Boolean-only contract?

  • Expose the complete boundary failure by running all four tests:
node --test

Why is this failure useful?

The test runner reports four failing tests against the unchanged evaluator. The non-Boolean case preserves the defect that allowed a string to influence the release decision.

This is the intended checkpoint. The failing suite now defines the exact contract that the implementation must satisfy.

Suite does not report four tests?

Confirm that test/release-readiness.test.js contains four separate test() calls.

Check that every test closes before the next case begins.

Help me find why my ShipCheck suite does not report four test cases.

✔️ Awesome, I've got everything!

Your test file now contains four boundary-focused cases. Keep the failing result visible while you fix the evaluator.

ⓧ I'd like to double check the full code

const test = require('node:test');
const assert = require('node:assert/strict');
const { assessRelease } = require('../src/release-readiness');

test('returns ready when every release check passes', () => {
  const result = assessRelease({
    testsPassed: true,
    reviewApproved: true,
    monitoringReady: true
  });

  assert.deepStrictEqual(result, {
    ok: true,
    status: 'ready',
    blockers: []
  });
});

test('returns every failed release check as a blocker', () => {
  const result = assessRelease({
    testsPassed: false,
    reviewApproved: true,
    monitoringReady: false
  });

  assert.deepStrictEqual(result, {
    ok: true,
    status: 'blocked',
    blockers: ['testsPassed', 'monitoringReady']
  });
});

test('rejects a request body that is not an object', () => {
  const result = assessRelease(null);

  assert.deepStrictEqual(result, {
    ok: false,
    error: 'Request body must be a JSON object.'
  });
});

test('rejects missing or non-Boolean release evidence', () => {
  const result = assessRelease({
    testsPassed: 'false',
    reviewApproved: true
  });

  assert.deepStrictEqual(result, {
    ok: false,
    error: 'Each release check must be true or false.',
    invalidFields: ['testsPassed', 'monitoringReady']
  });
});
Enforce the contract

The evaluator needs two validation gates before it calculates blockers. The first gate checks the body shape. The second gate checks every required field type.

  • Return to src/release-readiness.js in VS Code.
  • Find the function that begins with function assessRelease(payload) {.
  • Select the complete function through its closing brace before module.exports.
  • Replace the selected function with this implementation:
function assessRelease(payload) {
  if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
    return {
      ok: false,
      error: 'Request body must be a JSON object.'
    };
  }

  const invalidFields = RELEASE_CHECKS.filter(
    (field) => typeof payload[field] !== 'boolean'
  );

  if (invalidFields.length > 0) {
    return {
      ok: false,
      error: 'Each release check must be true or false.',
      invalidFields
    };
  }

  const blockers = RELEASE_CHECKS.filter(
    (field) => payload[field] === false
  );

  return {
    ok: true,
    status: blockers.length === 0 ? 'ready' : 'blocked',
    blockers
  };
}

How does validation protect the boundary?

  • The first condition rejects null values, arrays, plus non-object bodies.
  • The invalidFields filter finds every required value that is not Boolean.
  • The blocker filter runs only after the complete input passes validation.
  • The ok field distinguishes validation failures from release decisions.
  • Save src/release-readiness.js.

Before you rerun the suite, which cases do you expect to change from failing to passing?

  • Verify the strict evaluator by running the complete suite:
node --test

What should I see?

The test runner reports four passing tests with zero failures. ShipCheck now rejects malformed evidence before calculating ready or blocked.

You have closed the original boundary gap. The string-value defect now has a permanent regression test.

Still seeing a failing test?

Check that the object validation appears before both calls to RELEASE_CHECKS.filter().

Confirm that the type comparison uses 'boolean'. Confirm that the blocker comparison uses false.

Help me compare the evaluator with the four ShipCheck contract cases.

✔️ Awesome, I've got everything!

Your evaluator now validates the request shape before making a release decision. Save the file before reviewing the changes.

ⓧ I'd like to double check the full code

const RELEASE_CHECKS = [
  'testsPassed',
  'reviewApproved',
  'monitoringReady'
];

function assessRelease(payload) {
  if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
    return {
      ok: false,
      error: 'Request body must be a JSON object.'
    };
  }

  const invalidFields = RELEASE_CHECKS.filter(
    (field) => typeof payload[field] !== 'boolean'
  );

  if (invalidFields.length > 0) {
    return {
      ok: false,
      error: 'Each release check must be true or false.',
      invalidFields
    };
  }

  const blockers = RELEASE_CHECKS.filter(
    (field) => payload[field] === false
  );

  return {
    ok: true,
    status: blockers.length === 0 ? 'ready' : 'blocked',
    blockers
  };
}

module.exports = {
  RELEASE_CHECKS,
  assessRelease
};
Review and commit the fix

A passing suite proves the behavior. The diff shows whether the implementation stayed inside the contract boundary.

  • Review the unstaged changes by running this Git command:
git diff

What does this command show?

The diff shows the new test file plus the validation changes in src/release-readiness.js. It gives you one final chance to spot contract drift before creating the commit.

  • Ask GitHub Copilot Free to review the displayed diff for mismatches with the four tests.
  • Stage the reviewed changes by running this command:
git add .

What does this command do?

This stages the evaluator update plus the new test file for the next commit. The staged snapshot now contains the complete contract fix.

  • Record the contract enforcement by running this command:
git commit -m "test: enforce the release contract"

What does this commit capture?

The commit records the tests plus the implementation that satisfies them. Its message identifies contract enforcement as the purpose of the change.

Commit not created?

Check that the test file plus evaluator changes were staged before running the commit command.

Confirm that Git has an author identity configured for this repository.

Help me diagnose why Git cannot create the ShipCheck contract-enforcement commit.

Before the final check, do you expect the committed version to preserve all four passing tests?

  • Verify the committed contract by running the test suite once more:
node --test

What should I see?

The test runner reports four passing tests with zero failures. The committed evaluator now handles ready, blocked, malformed-object, plus non-Boolean inputs deterministically.

  • Confirm the repository state by running this command:
git status

What should I see?

You should see branch main with no uncommitted test or evaluator changes. Your contract fix is safely recorded.

ShipCheck now protects its release decision with executable contract tests. Next, you will add the health, logging, port binding, plus shutdown behavior needed by a hosted service.

Add the Operational Design

Your ShipCheck API now enforces its three-check contract. Four passing tests prove that its release decisions are deterministic.

A hosted Node.js service also needs an operational contract. Render needs a stable port plus a health signal.

Structured logs expose each request outcome. Graceful shutdown lets the platform stop the process safely.

In this step, get ready to:
  • Extend the architecture decision with runtime concerns.
  • Add health checks, request logs, error responses, and shutdown handling.
  • Exercise the operational behavior before committing it.
Extend the architecture decision

The architecture decision currently explains the business boundary. An operational review adds the path from source control to live traffic.

Use GitHub Copilot as a reviewer. Keep its suggestions limited to port binding, health, logs, and shutdown.

  • Switch back to docs/architecture.md in Visual Studio Code.
  • Ask GitHub Copilot to review the architecture for the four operational concerns from above.
  • Reject any suggestion outside those four concerns.
  • Replace the content from the first line through the existing Data Flow section with this updated design:
# ShipCheck Architecture Decision

## System Boundary

ShipCheck receives release evidence, validates the API contract, evaluates a deterministic policy, and returns a decision. It does not own test execution, code review, monitoring configuration, or release history.

## Components

1. PowerShell or another HTTP client sends a JSON request.
2. The Node.js HTTP layer handles routing, parsing, responses, health, and logs.
3. The release-readiness module validates evidence and evaluates the policy.
4. GitHub stores source history and GitHub Actions runs the test suite.
5. Render starts the service only after CI passes, probes `/health`, and exposes deploy logs and metrics.

## Data Flow

`client -> HTTP boundary -> validation -> policy evaluation -> JSON response`

Operational signals flow separately:

`Render health probe -> /health`

`request -> structured stdout log -> Render log stream`

`push -> GitHub Actions -> Render deployment -> health check -> live traffic`

What does this design capture?

  • The system boundary now names validation as a separate responsibility.
  • The component list connects the local service to GitHub Actions plus Render.
  • The data flows separate request handling from operational evidence.
  • Save docs/architecture.md.
  • Confirm that the document now shows one request flow plus three operational flows.

You should see validation between the HTTP boundary and policy evaluation. The operational flows should cover health, logs, and deployment.

  • Replace the content from the existing Decision heading through the end of docs/architecture.md with this section:
## Decision

Use a stateless Node.js 24.20.0 service built only with stable core modules.

## Why This Fits

- The decision is deterministic and needs no database.
- Separating policy from transport makes the rule easy to test.
- Avoiding an external web framework keeps this project focused on boundaries, contracts, and delivery.
- A Render web service demonstrates a long-running process, health checks, logs, and graceful shutdown.
- `After CI Checks Pass` makes automated tests a deployment gate instead of an optional report.

## Trade-offs

- Core HTTP exposes useful mechanics but needs more manual routing and body parsing than a framework.
- In-memory request sequencing resets when the service restarts.
- A Render Free service is a learning environment, not production infrastructure.
- No database means there is no audit history of prior assessments.

Why record these trade-offs?

The decision keeps ShipCheck stateless on Node.js 24.20.0. The trade-offs document what this small design gains plus what it gives up.

  • Save docs/architecture.md.
  • Confirm that the final sections cover delivery, request sequencing, Render Free, and audit history.

Architecture sections out of order?

Check that System Boundary comes before Components. Confirm that Trade-offs remains the final heading.

Compare your file with the reference below. Help me find a missing or duplicated section in my ShipCheck architecture decision.

✔️ Awesome, I've got everything!

Your architecture decision now connects the API boundary to its runtime and delivery evidence.

ⓧ I'd like to double check the full code

# ShipCheck Architecture Decision

## System Boundary

ShipCheck receives release evidence, validates the API contract, evaluates a deterministic policy, and returns a decision. It does not own test execution, code review, monitoring configuration, or release history.

## Components

1. PowerShell or another HTTP client sends a JSON request.
2. The Node.js HTTP layer handles routing, parsing, responses, health, and logs.
3. The release-readiness module validates evidence and evaluates the policy.
4. GitHub stores source history and GitHub Actions runs the test suite.
5. Render starts the service only after CI passes, probes `/health`, and exposes deploy logs and metrics.

## Data Flow

`client -> HTTP boundary -> validation -> policy evaluation -> JSON response`

Operational signals flow separately:

`Render health probe -> /health`

`request -> structured stdout log -> Render log stream`

`push -> GitHub Actions -> Render deployment -> health check -> live traffic`

## Decision

Use a stateless Node.js 24.20.0 service built only with stable core modules.

## Why This Fits

- The decision is deterministic and needs no database.
- Separating policy from transport makes the rule easy to test.
- Avoiding an external web framework keeps this project focused on boundaries, contracts, and delivery.
- A Render web service demonstrates a long-running process, health checks, logs, and graceful shutdown.
- `After CI Checks Pass` makes automated tests a deployment gate instead of an optional report.

## Trade-offs

- Core HTTP exposes useful mechanics but needs more manual routing and body parsing than a framework.
- In-memory request sequencing resets when the service restarts.
- A Render Free service is a learning environment, not production infrastructure.
- No database means there is no audit history of prior assessments.

How should I use this file?

Compare each section with your saved file. The order plus wording should match before you continue.

Build the operational HTTP boundary

The HTTP boundary needs one response helper so every route returns JSON consistently. That helper also records one structured completion event per request.

  • Switch back to src/server.js.
  • Replace the current import lines with this opening section:
const http = require('node:http');
const process = require('node:process');
const { assessRelease } = require('./release-readiness');

const port = Number(process.env.PORT) || 3000;
let requestSequence = 0;

function sendJson(req, res, statusCode, payload, requestId, startedAt) {
  const body = JSON.stringify(payload);

  res.writeHead(statusCode, {
    'Content-Type': 'application/json; charset=utf-8'
  });
  res.end(body);

  console.log(JSON.stringify({
    event: 'request_completed',
    requestId,
    method: req.method,
    path: req.url,
    statusCode,
    durationMs: Date.now() - startedAt
  }));
}

What does this code do?

  • The port value uses the platform setting when one exists.
  • The requestSequence value gives each request a local identifier.
  • The sendJson() helper sends the response before recording its outcome.
  • Find the existing server.listen block at the bottom of src/server.js.
  • Replace that block with this platform-compatible listener:
server.listen(port, '0.0.0.0', () => {
  console.log(JSON.stringify({
    event: 'server_started',
    port
  }));
});

Why change the listener?

The server now reads its port from the environment. Binding to 0.0.0.0 makes the process reachable through Render's network.

  • Save src/server.js.

Before you run the service, consider whether its startup message will still be plain text or become structured JSON.

  • Start ShipCheck by running this command:
node src/server.js

What should I see?

The terminal prints a one-line JSON record for server_started. Its port value is 3000 on your local machine.

  • Press Ctrl+C in the terminal to stop the service.

Server fails to start?

Check the new process import for matching quotes. Confirm that the old fixed-port listener was fully replaced.

Help me debug the startup changes in my ShipCheck server.

A health endpoint answers one operational question: can this process handle traffic now. Each request also needs timing data so the logs reveal how the boundary behaved.

  • Find the opening line of the existing asynchronous server callback.
  • Add this request metadata plus health route directly below that opening line:
  const requestId = `req-${++requestSequence}`;
  const startedAt = Date.now();

  if (req.method === 'GET' && req.url === '/health') {
    sendJson(req, res, 200, { status: 'ok' }, requestId, startedAt);
    return;
  }

How does the health route work?

  • The request identifier increments within the running process.
  • The start time supports the final durationMs log field.
  • The health route returns status with the value ok.
  • Save src/server.js.
  • Restart ShipCheck by running this command:
node src/server.js

What does this command prove?

The startup record confirms that the edited file still runs. The health request provides the next visible check.

  • Press Cmd+Space on macOS or the Windows key on Windows to open system search.
  • Type the name of your browser into system search.
  • Press Enter to open the browser.
  • Enter http://localhost:3000/health in the address bar.

You should see a JSON response whose status value is ok. The terminal should show a one-line request record for /health.

  • Return to the terminal running ShipCheck.
  • Press Ctrl+C to stop the service.

Health response missing?

Confirm that the health route appears before the assessment route. Check that the browser URL ends with /health.

Help me debug why the ShipCheck health route is not returning its JSON response.

The existing assessment route sends every parsed request with a success code. The validated evaluator already distinguishes client errors, so the HTTP layer can now return that distinction.

  • Replace the existing POST /assess block with this version:
  if (req.method === 'POST' && req.url === '/assess') {
    let payload;

    try {
      payload = await readJsonBody(req);
    } catch {
      sendJson(
        req,
        res,
        400,
        { error: 'Request body must contain valid JSON.' },
        requestId,
        startedAt
      );
      return;
    }

    const assessment = assessRelease(payload);

    if (!assessment.ok) {
      sendJson(req, res, 400, assessment, requestId, startedAt);
      return;
    }

    sendJson(req, res, 200, assessment, requestId, startedAt);
    return;
  }

How does the route classify requests?

  • Invalid JSON returns a client error before policy evaluation.
  • Rejected evidence returns the evaluator's invalid field names.
  • Valid evidence returns a release decision with a success code.
  • Replace the old fallback response with this unknown-route response:
  sendJson(
    req,
    res,
    404,
    { error: 'Route not found.' },
    requestId,
    startedAt
  );

What does the fallback protect?

Any request that misses the health route plus assessment route reaches this response. The log records the same 404 status sent to the caller.

  • Save src/server.js.

Before you test the routes, predict which earlier string-valued request now produces a client error.

  • Restart ShipCheck by running this command:
node src/server.js

What is running now?

ShipCheck is ready for local requests. Leave this terminal running while you use a second PowerShell terminal for the checks.

  • Switch to the PowerShell terminal you used for request tests earlier.
  • Resend the valid assessment request from earlier.
  • Resend the blocked assessment request from earlier.
  • Resend the string-valued assessment request from earlier.
  • Request http://localhost:3000/missing with Invoke-RestMethod.

What should the requests prove?

  • The valid request returns ready.
  • The blocked request returns its failed checks.
  • The string-valued request returns a 400 client error.
  • The unknown route returns a 404 response.
  • The server terminal prints one-line JSON records with matching status codes.
  • Return to the terminal running ShipCheck.
  • Press Ctrl+C to stop the service.

Assessment returns the wrong status?

Confirm that assessment.ok is checked before the success response. Check that each sendJson() call receives the request metadata.

Help me trace an incorrect status code through my ShipCheck assessment route.

The route behavior now works through one callback. A named handler gives the server wrapper one place to catch unexpected rejected requests.

  • Ask GitHub Copilot to extract the existing asynchronous callback into a function named handleRequest.
  • Tell GitHub Copilot to preserve the callback body exactly.
  • Replace Copilot's server wrapper with this exact block:
const server = http.createServer((req, res) => {
  handleRequest(req, res).catch((error) => {
    console.error(JSON.stringify({
      event: 'request_failed',
      message: error.message
    }));

    res.writeHead(500, {
      'Content-Type': 'application/json; charset=utf-8'
    });
    res.end(JSON.stringify({ error: 'Internal server error.' }));
  });
});

Why add the wrapper?

The named handleRequest() function owns normal route behavior. The wrapper catches unexpected rejected work plus returns a server error.

  • Save src/server.js.
  • Restart ShipCheck by running this command:
node src/server.js

What does this check confirm?

The structured startup record confirms that the extraction preserved runnable JavaScript. The health route should remain available.

  • Refresh http://localhost:3000/health in your browser.

You should still see the health response. The server terminal should record the request.

  • Return to the terminal running ShipCheck.
  • Press Ctrl+C to stop the service.

Seeing a syntax error after extraction?

Confirm that handleRequest() closes before the server declaration. Check that only one server constant remains.

Help me repair the handleRequest extraction without changing the route behavior.

Render sends SIGTERM when it replaces a running instance. The handler closes the HTTP server so active work can finish before the process exits.

  • Add this shutdown handler after the server.listen block:
process.on('SIGTERM', () => {
  console.log(JSON.stringify({ event: 'shutdown_started' }));

  server.close(() => {
    console.log(JSON.stringify({ event: 'shutdown_complete' }));
  });
});

How does shutdown work?

The signal handler records when shutdown starts. The server records completion after it stops accepting traffic.

  • Save src/server.js.

✔️ Awesome, I've got everything!

Your server now has the complete operational boundary.

ⓧ I'd like to double check the full code

const http = require('node:http');
const process = require('node:process');
const { assessRelease } = require('./release-readiness');

const port = Number(process.env.PORT) || 3000;
let requestSequence = 0;

function sendJson(req, res, statusCode, payload, requestId, startedAt) {
  const body = JSON.stringify(payload);

  res.writeHead(statusCode, {
    'Content-Type': 'application/json; charset=utf-8'
  });
  res.end(body);

  console.log(JSON.stringify({
    event: 'request_completed',
    requestId,
    method: req.method,
    path: req.url,
    statusCode,
    durationMs: Date.now() - startedAt
  }));
}

function readJsonBody(req) {
  return new Promise((resolve, reject) => {
    let rawBody = '';

    req.on('data', (chunk) => {
      rawBody += chunk;
    });

    req.on('end', () => {
      try {
        resolve(JSON.parse(rawBody || '{}'));
      } catch (error) {
        reject(error);
      }
    });

    req.on('error', reject);
  });
}

async function handleRequest(req, res) {
  const requestId = `req-${++requestSequence}`;
  const startedAt = Date.now();

  if (req.method === 'GET' && req.url === '/health') {
    sendJson(req, res, 200, { status: 'ok' }, requestId, startedAt);
    return;
  }

  if (req.method === 'POST' && req.url === '/assess') {
    let payload;

    try {
      payload = await readJsonBody(req);
    } catch {
      sendJson(
        req,
        res,
        400,
        { error: 'Request body must contain valid JSON.' },
        requestId,
        startedAt
      );
      return;
    }

    const assessment = assessRelease(payload);

    if (!assessment.ok) {
      sendJson(req, res, 400, assessment, requestId, startedAt);
      return;
    }

    sendJson(req, res, 200, assessment, requestId, startedAt);
    return;
  }

  sendJson(
    req,
    res,
    404,
    { error: 'Route not found.' },
    requestId,
    startedAt
  );
}

const server = http.createServer((req, res) => {
  handleRequest(req, res).catch((error) => {
    console.error(JSON.stringify({
      event: 'request_failed',
      message: error.message
    }));

    res.writeHead(500, {
      'Content-Type': 'application/json; charset=utf-8'
    });
    res.end(JSON.stringify({ error: 'Internal server error.' }));
  });
});

server.listen(port, '0.0.0.0', () => {
  console.log(JSON.stringify({
    event: 'server_started',
    port
  }));
});

process.on('SIGTERM', () => {
  console.log(JSON.stringify({ event: 'shutdown_started' }));

  server.close(() => {
    console.log(JSON.stringify({ event: 'shutdown_complete' }));
  });
});

How should I use this file?

Compare the imports, helper functions, request handler, server wrapper, listener, and shutdown handler. Your saved file should match each section.

Verify and commit the operational design

The implementation now covers both business decisions and runtime evidence. A final local exercise proves that those responsibilities still work together.

Before the final run, predict which requests produce success responses and which requests produce client errors.

  • Start the completed service by running this command:
node src/server.js

What starts with the service?

The terminal prints the structured startup record. Keep this process running during the endpoint checks.

  • Refresh http://localhost:3000/health in your browser.
  • Resend the valid assessment request from your PowerShell terminal.
  • Resend the blocked assessment request from your PowerShell terminal.
  • Resend the string-valued assessment request from your PowerShell terminal.
  • Resend the unknown-route request from your PowerShell terminal.

What should the final exercise show?

  • The browser shows status with the value ok.
  • Valid release evidence returns ready.
  • Blocked release evidence names its failed checks.
  • Malformed evidence returns a 400 response.
  • The unknown route returns a 404 response.
  • Each handled request produces one JSON log record.
  • Return to the terminal running ShipCheck.
  • Press Ctrl+C to stop the service.

The process closes and returns control to your terminal. Your local shutdown check is complete.

  • Confirm that the validated release contract still passes by running:
node --test

What should the tests report?

The terminal should report four passing tests. The operational changes leave the three-check evaluator unchanged.

  • Inspect the changed files by running:
git status

What should Git show?

The working tree should list changes to docs/architecture.md plus src/server.js. The release-readiness module plus its tests should remain unchanged.

  • Stage the operational design by running:
git add .

What does staging capture?

Git prepares the architecture update plus server update for one coherent commit.

  • Record the completed operational design by running:
git commit -m "feat: add operational readiness"

What does this commit represent?

The commit records the health endpoint, platform binding, structured request logs, JSON errors, and shutdown behavior as one operational change.

Your service now has a documented runtime boundary plus local evidence that it works. That is the hard part done: ShipCheck is ready for a delivery pipeline.

  • Confirm the commit left a clean working tree by running:
git status

What should the final status show?

Git should report that the working tree is clean on main. Your operational design is now safely recorded.

Tests pass but Git shows extra changes?

Review each listed path before committing anything else. Keep generated files plus unrelated edits out of this operational commit.

Help me understand the extra files shown by Git after the ShipCheck operational changes.

ShipCheck now behaves like a hosted service before it ever leaves your machine. Next, you will gate it with automated checks and deploy it to Render.

Gate, Deploy, and Observe the Service

ShipCheck now behaves like a hosted service on your machine. You have already proved its operational paths locally.

A delivered service needs a feedback loop from source control to runtime evidence. GitHub Actions will run your tests as a continuous integration gate.

Render will deploy the commit only after that gate passes. Its health page, logs, and metrics will show what happened.

In this step, get ready to:
  • Add an automated test gate to the repository.
  • Deploy ShipCheck through the passing gate.
  • Prove the live delivery feedback loop with operational evidence.
Add the CI gate and publish the repository

A workflow turns your four local tests into a shared release gate. Every push or pull request must pass the same contract checks.

  • In VS Code's Explorer from earlier, expand .github/workflows.
  • Select the new-file control beside .github/workflows.
  • Name the file ci.yml.
  • Populate ci.yml by pasting the workflow below:
name: CI

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v7
        with:
          node-version: '24.20.0'
      - run: node --test

What does this workflow do?

  • The push trigger checks every published commit.
  • The pull_request trigger checks proposed changes before they merge.
  • The test job runs on a GitHub-hosted ubuntu-latest runner.
  • The actions/checkout@v6 step loads your repository onto the runner.
  • The actions/setup-node@v7 step selects Node.js 24.20.0.
  • The node --test step runs the four contract tests.
  • Save .github/workflows/ci.yml.
  • Confirm ci.yml is listed inside .github/workflows in the Explorer.

Workflow file missing?

Check that ci.yml sits inside .github/workflows. A workflow stored elsewhere will not run on GitHub.

Ask for help checking the workflow path and YAML indentation.

✔️ Awesome, I've got everything!

Your workflow file is saved in the directory GitHub Actions monitors.

ⓧ I'd like to double check the full code

name: CI

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v7
        with:
          node-version: '24.20.0'
      - run: node --test
  • Select Source Control in VS Code's Activity Bar.
  • Stage .github/workflows/ci.yml from the Changes list.
  • Enter ci: add test workflow in the commit message box.
  • Select Commit to record the workflow in Git history.
  • Confirm the workflow commit is clean by running this in the terminal from earlier:
git status

What should I see?

You should see On branch main. The workflow file should no longer appear as an uncommitted change.

Workflow still uncommitted?

Return to the Source Control view. Confirm ci.yml was staged before you selected Commit.

Help me diagnose why the workflow commit is not clean.

The local history now contains the service plus its test gate. Publishing that history gives GitHub enough information to run the gate.

  • Open GitHub in your browser.
  • Select New repository.
  • Enter shipcheck-api as the repository name.
  • Set the repository visibility to public.
  • Leave the optional initialization files unselected.
  • Select Create repository.
  • Copy the repository URL shown on Quick Setup.
  • Store the copied URL here: https://github.com/your-name/shipcheck-api.git.
  • Switch back to the VS Code terminal from earlier.
  • Prepare the command below by replacing REMOTE-URL with https://github.com/your-name/shipcheck-api.git.
  • Add the GitHub repository as origin by running the edited command:
git remote add origin REMOTE-URL

What does this command do?

The command names your GitHub repository origin. That name gives later Git commands a stable destination.

Git may open a browser sign-in if it needs to confirm your GitHub identity. Complete that prompt with the GitHub account from earlier.

  • Publish the local main branch by running:
git push -u origin main

What does this command do?

The command publishes your committed history to GitHub. It also configures local main to track the remote branch.

Push rejected?

Confirm the remote repository is empty. A pre-created README or license can give the remote a separate history.

Help me troubleshoot why my ShipCheck main branch will not push to the empty GitHub repository.

Before you open the workflow run, predict whether all four tests will pass.

  • Return to the shipcheck-api repository on GitHub.
  • Select the Actions tab.
  • Open the CI workflow run to check its result.

You should see a green CI run with four passing tests. Your repository now rejects contract-breaking commits before deployment.

Create the Render web service and gate deployment

The repository now has a trustworthy automated check. Render can use that result as permission to deploy the service.

  • Open Render in your browser.
  • Sign in with your Render account.
  • Select New.
  • Select Web Service.
  • Choose Git Provider as the source.

Render may ask GitHub for permission to read your public repository. Approving that request lets Render detect commits and CI results.

  • Approve the GitHub authorization request if it appears.
  • Select the public shipcheck-api repository.
  • Set Branch to main.
  • Set Language to Node.
  • Enter node --test in Build Command.

The plan selector includes paid choices. This project stays at no cost on Render's Free compute plan.

  • Enter node src/server.js in Start Command.
  • Select the Free compute plan.
  • Select Create Web Service.

The first deployment has to run the tests before starting ShipCheck. A quiet deployment screen is normal while those stages complete.

Why use separate build and start commands?

The build command proves the API contract before deployment. The start command launches the long-running HTTP process only after that proof succeeds.

This makes the test result part of delivery instead of an optional report.

  • Select the service's Settings page.
  • Enter /health in the path field under Health Checks.
  • Use the adjacent save control to apply the health-check path.
  • Set Auto-Deploy to After CI Checks Pass.

Your service settings now use the test suite as a deployment gate. Render also probes /health before treating the service as healthy.

Deployment not starting?

Confirm the connected branch is main. Check that the build command and start command match the values above.

Help me diagnose why my Render service is not deploying the ShipCheck repository.

Verify the live feedback loop

A live status alone proves that a process started. The endpoint responses, request logs, deploy details, and resource graphs prove that the whole delivery path works.

  • Wait until the Render service status shows Live.
  • Open the current live deploy on the Deploys page to confirm the CI-gated commit reached Render.
  • Record the public service URL here: https://shipcheck-api.onrender.com.

Before you test the live endpoints, predict whether their responses will match your local results.

  • Enter https://shipcheck-api.onrender.com followed by /health in your browser's address bar.

You should see a response with status set to ok. That response proves Render can reach the running process.

  • Switch back to the PowerShell terminal from earlier.
  • Repeat the valid assessment request from Step 4 using https://shipcheck-api.onrender.com in place of the local base URL.

You should see ready with an empty blockers list. The deployed API matches the validated local contract.

  • Return to the Render service.
  • Open its log stream.
  • Find the one-line JSON records for /health and /assess.

Each matching record should include request_completed plus its method, path, status code, and duration. Those records connect your requests to runtime evidence.

  • Select the service's Metrics page.
  • Identify the CPU evidence from the live service.
  • Identify the memory evidence from the live service.
  • Identify the request-volume evidence from your endpoint calls.
  • Identify the outbound-bandwidth evidence from the responses.

You should see resource graphs plus request evidence from the service. The exact values can stay small because this is a learning workload.

Where is response latency?

Response-latency metrics require a paid Render plan. The Free service still gives you CPU, memory, request volume, and outbound bandwidth.

Live request failing?

Confirm the service still shows Live. Check that the URL uses the current onrender.com address and the exact /health or /assess path.

Help me trace a failed live ShipCheck request through Render deploy status, health checks, and structured logs.

That completes the delivery loop. ShipCheck now moves from a tested commit to a gated deployment with visible operational evidence.

Secret mission

Evolve the Release Policy Safely

Add rollback-plan readiness across the ShipCheck delivery path. Prove the new rule with a failing test. Carry the fix through CI to the live service.

Clean Up Your Resources

Clean Up Your Resources

ShipCheck can remain available after the project. The path you used has no ongoing cost.

Your next choice is whether these resources stay available, pause, or disappear.

Resources you used:

  • A local shipcheck-api folder containing a Git repository.
  • A public shipcheck-api repository on GitHub.
  • A live Render Free web service.

Render Free Limits

Render positions Free instances as learning resources. Production applications need a different plan.

An idle Free web service spins down after 15 minutes. Its next request can take about one minute while it wakes.

The filesystem is ephemeral. Your workspace receives 750 Free instance hours per calendar month.

Without a payment method, excess bandwidth suspends Free services. Excess pipeline minutes stop new builds from starting.

Keep everything running

No action is needed while you are still building. The service stays configured even when its Free instance spins down.

  • Leave your public shipcheck-api repository available on GitHub.
  • Leave the Render web service on the Free compute plan.
  • Keep credentials out of the public repository.

Your code remains public. Render keeps the service configured for its next request.

Pause - I'll come back to this later

This option preserves your source code. It also preserves the service's deploy history.

  • Return to the Render service from earlier.
  • Open the Settings page.
  • Set Auto-Deploy to Off.
  • Stop sending requests to the live onrender.com URL.

After 15 minutes without inbound traffic, the Free service spins down. Your code remains available when you return.

Delete - I don't want to use this again

This option removes the project resources for a fresh start. Deletion is permanent, so each confirmation gives you one last chance to check the shipcheck-api name.

  • Return to the Render service from earlier.
  • Open the Settings page.
  • Locate the service deletion control shown in the current settings page.
  • Read the live confirmation text.
  • Approve the deletion after confirming the ShipCheck service name.
  • Reload the former onrender.com URL.

You should no longer receive a ShipCheck response. The service's deploys, logs, health checks, and metrics are removed with it.

  • Return to your public shipcheck-api repository on GitHub.
  • Open the repository settings from the top navigation.
  • Locate the repository's permanent deletion control.
  • Read GitHub's live confirmation.
  • Confirm the deletion after checking the shipcheck-api repository name.
  • Reload the former repository URL.

You should no longer see the ShipCheck source or its workflow history.

  • Return to the folder that contains shipcheck-api in File Explorer.
  • Select the shipcheck-api folder.
  • Use File Explorer's permanent deletion option.
  • Search that location for shipcheck-api.

You should no longer see the local project folder or its Git history.

Nice Work!

Nice Work!

You shipped it! ShipCheck is now a live release-readiness API that turns four delivery signals into an explainable decision.

You've learned how to:

  • Turned an ambiguous release problem into a product specification with an explicit API contract. Recorded the system boundary in an architecture decision record. Explained why a stateless service built with Node.js fits that boundary. Used GitHub Copilot to critique decisions while you kept ownership of the final design.
  • Watched malformed release evidence fool the first implementation. Protected POST /assess with automated tests plus strict validation. Confirmed deterministic ready or blocked decisions for valid evidence. Invalid values now receive a clear rejection.
  • Published ShipCheck through GitHub with GitHub Actions as the CI gate. Deployed the passing build to Render. Verified the live service through GET /health. Read structured logs plus resource metrics as operational evidence. Added graceful shutdown for platform redeploys.
  • Secret Mission: Evolved the live policy to require rollbackPlanReady. Updated the specification first. Added a failing test before changing RELEASE_CHECKS. Proved GitHub Actions passed before Render redeployed the new behavior.

Ready to quiz yourself?