Skip to content

Making of - workwingman-ci-runner

A living journal of how this tool came to be and how it keeps changing. Newest entry at the top of each chapter's log; chapters themselves are chronological.

Chapter 1 - Ship-gate migration (2026-07-08)

Where this repo started: already built to the council-decided architecture - GitHub App auth (no PAT), a leaderless warm pool across 4 home Windows PCs, ephemeral runners, and a full watchdog + skill + docs set (README.md, docs/SETUP.md, docs/WORKFLOW-CHANGES.md, docs/REVOCATION-RUNBOOK.md, docs/DR-OPS.md). One commit in, clean working tree, no CI wired up yet.

What this pass added, bringing the repo to the house ship-gate standard used across the tool fleet (pilot: house-council-client), without touching any runtime behavior:

  • TESTING.md - the 4-pillar matrix (unit/coverage, mutation-N/A, churn, E2E) with the same honesty-about-gaps standard as the pilot.
  • tests/GitHubApp.Tests.ps1 + tests/Repo.Tests.ps1 - Pester 5+ unit tests. The interesting design problem here: watchdog/GitHubApp.psm1 mints real GitHub App JWTs and exchanges them for tokens over live HTTP - none of that can touch api.github.com in a test. Solved with two techniques:
  • A throwaway, in-memory-only RSA test key (never registered with GitHub) for everything cryptographic - JWT signing, PKCS#1 DER decode, DPAPI protect/unprotect, Credential Manager round trip.
  • Pester's Mock -ModuleName to intercept the module's internal Invoke-WWGitHub HTTP caller, so the full Get-WWRunnerRegistration four-hop flow (key -> JWT -> install token -> registration token) is tested end-to-end against canned responses with zero live calls. Landed at ~81% line coverage on the module (documented gaps: the network-caller body itself, which is intentionally mocked rather than executed; the CredentialManager multi-line-PEM path, which self-skips on hosts where cmdkey can't hold a multi-line secret).
  • .github/workflows/ci.yml - test, churn (5x), security (semgrep via Docker), secret-gate (grep hard gate), and a visible e2e-note placeholder rather than a silently-skipped E2E job (see TESTING.md for why real E2E is blocked on a live App installation).
  • Doc-set gaps filled: docs/technical/ARCHITECTURE.md (Mermaid component + sequence diagrams), docs/technical/SECURITY.md, docs/plain/README.md (this repo's plain-language sibling), this journal, docs/BOARD.md.
  • docs/ORG-MIGRATION.md - a design note (no code) capturing the diff the council decided on 2026-07-08 for moving this watchdog from per-repo to org-level runner registration, so the next pass has a spec instead of a blank page.

What did NOT change: any watchdog/skill/module runtime logic, the per-PC config.example.psd1 shape, the Docker image, or the existing docs (README.md, docs/SETUP.md, docs/WORKFLOW-CHANGES.md, docs/REVOCATION-RUNBOOK.md, docs/DR-OPS.md) beyond what's noted above. This was a "bring to ship-gate standard" pass, not a feature or architecture change.

Gate results at time of shipping: see the commit this file was added in for secret-grep / Codex review / test-count verbatim results.

Chapter 2 - Org migration implemented (2026-07-08, branch org-migration)

Same day, next pass: the council's docs/ORG-MIGRATION.md design note went from spec to code, on a branch (master was concurrently being worked by the CI-fix session, so this pass never touched it).

The shape of the change: everything interesting went into watchdog/GitHubApp.psm1 - the one coverage-instrumented file - as pure functions with injectable seams, and the watchdog scripts stayed thin callers. That kept the module's honest coverage story intact (~81% -> ~88%) instead of eroding it with untestable script logic:

  • Org scope is a single URI-prefix decision (Get-WWRunnerApiBase: repos/{owner}/{repo} vs orgs/{org}, repo-XOR-org validated) that every runner-scoped function shares. The auth flow (key -> JWT -> installation token) is byte-identical in both modes, which is what made this a low-risk, mechanical extension - exactly as the design note predicted.
  • Backoff + jitter got one pure formula (Get-WWBackoffDelay, injectable [Random]) reused by both the API retry wrapper (Invoke-WWGitHubWithRetry, injectable -SleepAction so tests never sleep) and the watchdog loops' consecutive-failure backoff. Retry policy deliberately refuses non-403/408/429 4xx - a 404/401 is a config bug, and retrying it just delays the operator noticing.
  • Stale-runner cleanup hit a real API limitation: GitHub's runner listing has no "offline since" field. Solution: track first-seen-offline times in process memory (Select-WWStaleRunners mutates a caller-owned table), which is honest about its own weakness - a watchdog restart restarts the clock. One sweeper per PC (the Linux watchdog), because both OS runners share the ww-<pc>-* name prefix and two sweepers would double-DELETE.
  • Bounded count is a hard cap of 4 in code (Get-WWRunnerTarget), not just a doc convention - a config.psd1 typo must not be able to melt a home PC. The Windows host runner stays at exactly one (a single actions/runner install dir serves one listener; N dirs was scoped out).

Backward compatibility as a test invariant: the entire pre-existing suite passes unmodified, and the new suite re-asserts the per-repo URI shape - so "per-repo mode unchanged" is enforced, not promised. New tests live in tests/OrgMigration.Tests.ps1 (mocked GitHub, seeded randomness, recorded no-op sleeps; 105 tests total across the suite).

What ships vs what waits: the code path is complete, but org mode stays dormant until the user does the GitHub-side steps (App org permission "Self-hosted runners: R/W", org install, per-PC Org + org InstallationId config) - the exact checklist replaced the design note in docs/ORG-MIGRATION.md. Rollback is config-only in either direction.

Gate results (this pass): 105 Pester tests passed / 0 failed / 2 pre-existing self-skips; module coverage 87.7% (floor 70%, was ~81%); churn 3x locally identical (P=105 F=0 S=2 each run); secret grep empty; Codex security review run pre-push (verdict recorded in the session report).

A concurrency war story, preserved for the record: this branch was built while a second agent session was concurrently committing stopgap-watchdog and runner-image work in the SAME checkout. That session's commits (3023823, c602a14) swept this pass's then-uncommitted files into its own commits, so the org-migration implementation is CONTENT-complete on this branch but its commit boundaries and messages belong to the image/stopgap work - re-learning, the hard way, the house workwingman-shared-repo-concurrent-commits lesson: prefer a separate worktree, stage exact files, commit fast. Do not be surprised that git log attributes the org endpoints to "feat(image): bake pwsh + xvfb"; git log --follow tests/OrgMigration.Tests.ps1 and this journal are the honest record. The stray wd-err.txt / wd-out.txt runtime logs committed in c602a14 belong to that session's stopgap watchdog debugging, not to org migration.