Skip to content

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.

  1. Point the GitHub App at the org with the org runner permission. On the App's settings page (the existing workwingman-ci-runner App, 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-level Administration: Read and write used by per-repo mode - upgrading the install scope does NOT grant it implicitly.
  2. Install the App on the andrewjonesdev-tools org (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.
  3. 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's config.psd1 as InstallationId (it is a DIFFERENT id from the old per-repo installation).
  4. (Recommended) Create the home-ci org runner group: org Settings -> Actions -> Runner groups -> New runner group, name home-ci, scope it to the tool-fleet repos (per the ship-tools-to-private-repos policy). If you skip this, runners land in GitHub's Default group, which also works - the home-ci LABEL is added automatically in org mode either way.
  5. Per PC (all 4): edit config.psd1 (see config.example.psd1 for the commented template):
  6. Org = 'andrewjonesdev-tools'
  7. InstallationId = '<org installation id from step 3>'
  8. if you did step 4: RunnerGroup = 'home-ci'
  9. 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.
  10. 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:
    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 pool
    
    (Run Runner-Stop.ps1 BEFORE editing config.psd1, or pass it the old config, so it deregisters from the old per-repo scope it registered in.)
  11. Flip fleet repos' workflows to the shared labels: runs-on: [self-hosted, home-ci, windows] (or linux), 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 in docs/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.psd1 without Org behaves 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.