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.psm1mints real GitHub App JWTs and exchanges them for tokens over live HTTP - none of that can touchapi.github.comin 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 -ModuleNameto intercept the module's internalInvoke-WWGitHubHTTP caller, so the fullGet-WWRunnerRegistrationfour-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 wherecmdkeycan't hold a multi-line secret). .github/workflows/ci.yml-test,churn(5x),security(semgrep via Docker),secret-gate(grep hard gate), and a visiblee2e-noteplaceholder 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}vsorgs/{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-SleepActionso 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-WWStaleRunnersmutates 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 theww-<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 - aconfig.psd1typo 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.