Org migration - IMPLEMENTED on branch org-migration - deploy checklist¶
Status: implemented on branch org-migration (2026-07-08), awaiting merge +
the user-side GitHub setup below. This document was the council's design
note (decision 2026-07-08); the code described here now exists on the
org-migration branch. Per-repo mode remains the default and is unchanged -
org mode activates only when a PC's config.psd1 sets Org.
What is implemented (branch org-migration)¶
| Design point | Where it landed |
|---|---|
Org token endpoint (POST /orgs/{org}/actions/runners/registration-token) |
watchdog/GitHubApp.psm1 - every runner-scoped function (Get-WWRegistrationToken, Get-WWRemoveToken, Get-WWRepoRunners, Remove-WWRunner, Get-WWRunnerRegistration, Invoke-WWStaleRunnerSweep) takes -Repo (owner/name) or -Org (org login); same App JWT -> installation-token auth flow in both modes, only the URI prefix changes. Exactly one of the two is required (both/neither throws). |
| Config schema | config.psd1 gains optional keys (all documented, commented-out, in config.example.psd1): Org (activates org mode; Repo then ignored), RunnerGroup (register into a named org runner group), MaxRunners (Linux warm-pool size, default 1, hard cap 4), StaleRunnerMinutes / StaleSweepMinutes (cleanup tuning). A config with none of these behaves exactly as before. |
home-ci label |
Get-WWEffectiveLabels: in org mode both watchdogs append home-ci (once) to the configured label sets, so fleet repos can use runs-on: [self-hosted, home-ci, <os>]. Repo mode labels are untouched. |
| Backoff + jitter | Get-WWBackoffDelay (pure, injectable [Random]) + Invoke-WWGitHubWithRetry (injectable sleep seam): ALL GitHub API calls made by the module retry transient failures (network, 403/408/429, 5xx - never other 4xx) with exponential backoff + jitter. Both watchdog loops additionally back off exponentially (capped 5 min) on consecutive pass failures instead of retrying every PollSeconds, and a failed docker run/config.cmd now throws so the backoff engages. |
| Stale-runner cleanup | Select-WWStaleRunners (pure) + Invoke-WWStaleRunnerSweep: the LINUX watchdog (one sweeper per PC - Linux and Windows runners share the ww-<pc>-* name prefix) sweeps every StaleSweepMinutes (default 15) and force-removes runners it has observed offline+idle for >= StaleRunnerMinutes (default 60). GitHub's listing has no offline-since timestamp, so first-seen-offline times are tracked in process memory; a watchdog restart just restarts the clock. |
| Bounded count | Get-WWRunnerTarget clamps MaxRunners to [1, 4] (hard cap in code); Get-WWReconcilePlan computes launch/kill deltas. The Windows host runner stays at exactly ONE per PC (a single actions/runner install dir serves one Runner.Listener) - multi-instance Windows runners would need N install dirs and were left out of scope. |
| Container registration URL | docker/entrypoint.sh accepts RUNNER_URL (org mode: https://github.com/<org>) or the original REPO env (repo mode, backward compatible). The Windows runner's config.cmd --url switches the same way, and --runnergroup is passed when RunnerGroup is set. |
| Skill scripts | Runner-Status.ps1 and Runner-Stop.ps1 read the same Org config key and list/deregister against the org pool when it is set. |
| Tests | tests/OrgMigration.Tests.ps1 - mocked org-endpoint routing, scope validation, backoff/jitter determinism (seeded [Random], no real sleeps), retry/no-retry decisions, stale-selection matrix, mocked end-to-end sweep, bounded-count clamping, label injection, and config-example hygiene. Zero live GitHub/Docker calls anywhere in the suite. |
Deploy checklist (remaining USER steps)¶
Nothing below can be done by code in this repo - it is GitHub-side setup plus per-PC config edits.
- Point the GitHub App at the org with the org runner permission. On the
App's settings page (the existing
workwingman-ci-runnerApp, or a new one owned by the org): Permissions & events -> Organization permissions -> "Self-hosted runners" -> Read and write, then Save. This is a DISTINCT permission from the repo-levelAdministration: Read and writeused by per-repo mode - upgrading the install scope does NOT grant it implicitly. - Install the App on the
andrewjonesdev-toolsorg (org Settings -> GitHub Apps -> install, or the App page -> Install App). Choose All repositories (or an explicit list covering the whole tool fleet). If the App was already installed somewhere, GitHub will also prompt an admin to approve the new permission on the installation - accept it, the token mint 403s until then. - Record the ORG installation id. After installing, the browser URL is
https://github.com/organizations/andrewjonesdev-tools/settings/installations/<INSTALLATION_ID>. That number goes into each PC'sconfig.psd1asInstallationId(it is a DIFFERENT id from the old per-repo installation). - (Recommended) Create the
home-ciorg runner group: org Settings -> Actions -> Runner groups -> New runner group, namehome-ci, scope it to the tool-fleet repos (per theship-tools-to-private-repospolicy). If you skip this, runners land in GitHub's Default group, which also works - thehome-ciLABEL is added automatically in org mode either way. - Per PC (all 4): edit
config.psd1(seeconfig.example.psd1for the commented template): Org = 'andrewjonesdev-tools'InstallationId = '<org installation id from step 3>'- if you did step 4:
RunnerGroup = 'home-ci' - optional tuning:
MaxRunners(Linux warm-pool size, 1-4),StaleRunnerMinutes/StaleSweepMinutes. No key re-provisioning is needed - the App private key and its DPAPI/KeePass custody chain are untouched by org mode. - Re-run watchdog registration on each PC: stop the old per-repo runners
and restart the watchdogs so fresh runners register at the org level:
(Run
pwsh ./skill/scripts/Runner-Stop.ps1 # deregisters ww-<pc>-* from the OLD scope Enable-ScheduledTask -TaskName WorkWingmanRunner-Linux-<pc> Start-ScheduledTask -TaskName WorkWingmanRunner-Linux-<pc> Enable-ScheduledTask -TaskName WorkWingmanRunner-Windows-<pc> Start-ScheduledTask -TaskName WorkWingmanRunner-Windows-<pc> pwsh ./skill/scripts/Runner-Status.ps1 # should now list the ORG poolRunner-Stop.ps1BEFORE editingconfig.psd1, or pass it the old config, so it deregisters from the old per-repo scope it registered in.) - Flip fleet repos' workflows to the shared labels:
runs-on: [self-hosted, home-ci, windows](orlinux), matching the convention already used as the placeholder comment in this repo's own.github/workflows/ci.yml. WorkWingman's own workflow changes are documented indocs/WORKFLOW-CHANGES.md.
Rollback¶
Org mode is config-gated: comment out Org (and restore the per-repo
InstallationId) in a PC's config.psd1 and restart its watchdogs to return
that PC to per-repo registration. No code change needed in either direction.
Explicitly unchanged¶
- The key-custody chain (KeePass -> DPAPI/CredMan) - org migration only changes which token endpoint the already-minted installation token calls, not how the App private key is stored or loaded.
- Per-repo mode: a
config.psd1withoutOrgbehaves exactly as shipped before this branch (verified by the pre-existing test suite, which still passes unmodified). - Duplicate suppression stays workflow-
concurrency-based (docs/WORKFLOW-CHANGES.md), not runner coordination.