Skip to content
4 changes: 2 additions & 2 deletions GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,13 @@ python3 /path/to/ProjectTemplate/scripts/docker_lint.py --root "$PWD"

The wrapper discovers tracked and unignored targets before it pulls applicable images. It reports a zero-target skip without pulling or mounting the repository. It pulls each applicable image in a distinct pull phase, then resolves the pulled repository digest. A digest prevents the tag from changing between the pull and execution. It does not make third-party code trusted.

After all pulls, the wrapper reports that repository mounts are about to begin. Each execution uses the resolved digest, disabled networking, and a read-only checkout mount. PSScriptAnalyzer installs its pinned module in a separate container without the checkout mount. Its lint container receives each tracked PowerShell path as a distinct argument.
After all pulls, the wrapper reports that repository mounts are about to begin. Each execution uses the resolved digest, disabled networking, and a read-only checkout mount. PSScriptAnalyzer installs its pinned module in a separate container without the checkout mount. Linters receive each tracked path as a distinct argument, split across bounded batches before host command-line limits become relevant.

Every Docker command has a five-minute timeout by default. Use `--timeout` to select another positive bound. The wrapper emits a start and completion line for each command. It reports the checked-file count for every linter, including tools that produce no success output. Timeout, container failure, zero-target execution, and successful quiet completion have distinct result lines. The wrapper names each lint container and removes it after a timeout.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Use repeated `--linter` options for a subset. The supported names are `editorconfig-checker`, `actionlint`, `markdownlint`, `cspell`, `shellcheck`, and `PSScriptAnalyzer`. editorconfig-checker reads the mounted tree. actionlint reads eligible workflows and includes shellcheck for `run:` blocks. markdownlint reads tracked and unignored Markdown files. CSpell reads `README.md` and `HISTORY.md` only. shellcheck and PSScriptAnalyzer run only when matching scripts are tracked or unignored.

In a configured editor the davidanson extension is enough for Markdown. Use the wrapper for a headless run or before pushing.
In a configured editor the `DavidAnson.vscode-markdownlint` extension is enough for Markdown. Use the wrapper for a headless run or before pushing.

When pulling a public image fails on a Docker-Desktop/WSL credential-helper error (`docker-credential-desktop.exe: exec format error`), retry with an empty Docker config: `DOCKER_CONFIG=$(mktemp -d) docker run ...` after writing `{}` to `$DOCKER_CONFIG/config.json`.

Expand Down
4 changes: 2 additions & 2 deletions OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The cache directory is unique to this verification run and remains outside the c

The `test_install.py` line behaves differently here than in CI, stated so its failure reads as the verdict it is. Its report cases install from this checkout and assert the machine then reads as current. An install from a checkout carrying uncommitted changes records a dirty stamp that reads as stale. So on a working tree mid-change those cases fail by design where CI's clean checkout passes. The remedy is to run them again once the change is committed, not to read the failure as a regression.

The Docker runner discovers tracked and unignored targets with `git ls-files`. It passes each path as one argument, so a path that contains whitespace remains one target.
The Docker runner discovers tracked and unignored targets with `git ls-files`. It passes each path as one argument, so a path that contains whitespace remains one target. It splits long target lists into bounded batches before host command-line limits become relevant.

Two gaps in that list are CI's rather than this runbook's, reproduced here so a local run matches CI rather than quietly exceeding it. The `jq` glob covers `repo-config/*.json` and does not reach `repo-config/operational/develop.json`, so a malformed operational payload passes. The second is that `sentence-split` and `sentence-length` are implemented and tested but named by no invocation, so nothing runs them.

Expand Down Expand Up @@ -112,7 +112,7 @@ The Docker linters pull `:latest` deliberately, so a local run matches whatever
python3 scripts/docker_lint.py
```

The runner prints target counts before pulling images. It then prints a phase boundary before any repository mount. Every Docker command has a five-minute default timeout, which `--timeout` can change. Each linter emits start and completion markers even when the tool succeeds quietly. A timeout, container failure, zero-target skip, and successful completion each have distinct output.
The runner prints target counts before pulling images. It then prints a phase boundary before any repository mount. Every Docker command has a five-minute default timeout, which `--timeout` can change. Each linter emits start and completion markers even when the tool succeeds quietly. Long target lists run in bounded batches. A timeout, container failure, zero-target skip, and successful completion each have distinct output.

Use repeated `--linter` options to run a subset. markdownlint reads every tracked or unignored Markdown file. CSpell reads only `README.md` and `HISTORY.md`, matching CI's deliberate scope.

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ Within this repo, day-to-day development follows the same branching, CI, review,

- **Branching.** Persistent `main` and `develop`, each with its own ruleset. This repo uses the default `release` workflow model: commit on feature branches only, feature branch to `develop` is squash-merged, `develop` to `main` is a merge commit, and `develop` is forward-only (no `main -> develop` back-merges). Live-service config repos instead use the `operational` model (registry `workflowModel`), with direct signed commits to `develop`, promoted to `main` by an occasional PR. See [GOVERNANCE.md "Branching Model"][governance-branching-model].
- **CI is lint-only.** There is no build or unit test. The PR gate runs markdownlint, cspell, JSON validation (`jq` parses `registry/`, `spec/`, and `repo-config/`, plus the `spec/validate.py` cross-reference and shape checks), and actionlint, and exposes the ruleset-bound `Check pull request workflow status job` aggregator. The same lint configs (`.markdownlint-cli2.jsonc`, `cspell.json`) drive the editor extensions, the CLI, and CI.
- **Review loop.** Every PR is reviewed by GitHub Copilot, and the agent drives the review loop to green and merges only with explicit maintainer permission. See [GOVERNANCE.md "PR Review Etiquette"][governance-pr-review-etiquette].
- **Review loop.** Every PR is reviewed by GitHub Copilot, and the agent drives the review loop to green and merges only with explicit maintainer permission. CodeRabbit and Qodo remain advisory candidates under the measured [pull request reviewer evaluation][pr-reviewer-evaluation]. See [GOVERNANCE.md "PR Review Etiquette"][governance-pr-review-etiquette].
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- **Release.** A `develop -> main` merge is promoted through a GitHub release (tag plus a source zip, README, and LICENSE). Versioning is NBGV-driven from [version.json][version]. See [WORKFLOW.md][workflow].

## Using This Repo
Expand Down Expand Up @@ -369,6 +369,7 @@ Licensed under the [MIT License][license]\
[license]: ./LICENSE
[matrix]: ./reports/conformance-matrix.md
[project-types]: ./spec/project-types.json
[pr-reviewer-evaluation]: ./docs/pr-reviewer-evaluation.md
[readme-structure]: ./spec/readme-structure.md
[repo-config]: ./repo-config/
[repo-config-doc]: ./docs/repo-config.md
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"pyright",
"pyrightconfig",
"pytest",
"Qodo",
"quoteoftheday",
"regen",
"resharper",
Expand Down
64 changes: 45 additions & 19 deletions docs/homelab-runner-selection-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This document tracks the design and rollout for [issue #889][issue-889]. It is a

- [Status](#status)
- [Decision Outcome](#decision-outcome)
- [Requirements Before Resuming](#requirements-before-resuming)
- [Objective](#objective)
- [Non-Goals](#non-goals)
- [Primary Constraint: Copilot Code Review](#primary-constraint-copilot-code-review)
Expand Down Expand Up @@ -39,13 +40,21 @@ Actions minutes savings do not justify the added runner, security, and operating

Ordinary CI savings alone do not justify separating CI onto a persistent runner while Copilot remains GitHub-hosted. This document preserves the design for reconsideration.

## Requirements Before Resuming

Implementation remains blocked until the maintainer approves the consultation decisions in this document.

Before implementation resumes, verify the current GitHub runner controls against GitHub's documentation and organization settings. The design depends on organization-level Copilot controls and runner-group workflow restrictions that can change independently of this repository.

The implementation must establish infrastructure authorization before it enables homelab selection. A selector, labels, actor checks, and static workflow tests are routing controls. They do not prevent another workflow from requesting the same labels directly.

## Objective

One workflow definition selects either GitHub-hosted Ubuntu or a trusted homelab runner. Missing or invalid configuration selects GitHub-hosted Ubuntu.

The design does not attempt an automatic availability fallback. GitHub chooses one target when it queues a job.

The first canary proves ordinary hub CI on a repository-scoped homelab runner. Copilot code review remains on GitHub-hosted infrastructure.
The first canary proves ordinary hub CI on a homelab runner in a restricted organization runner group. Copilot code review remains on GitHub-hosted infrastructure.

## Non-Goals

Expand Down Expand Up @@ -77,9 +86,11 @@ Ordinary fleet CI and Copilot code review therefore have separate runner policie

The fleet runner variable never reaches Copilot configuration. Homelab labels never appear in the Copilot workflow.

The proposed implementation adds a dedicated `.github/workflows/copilot-code-review.yml`. It pins `copilot-setup-steps` to `ubuntu-24.04`.
The organization-level Copilot runner type selects a standard GitHub-hosted runner. Repository customization of the Copilot runner type is disabled.

That file prevents a future `copilot-setup-steps.yml` from unintentionally changing the code-review runner. GitHub gives the dedicated code-review file precedence.
The proposed implementation adds a dedicated `.github/workflows/copilot-code-review.yml`. It pins `copilot-setup-steps` to `ubuntu-24.04` and begins with a step that fails unless `runner.environment` is `github-hosted`.

The organization policy is the authorization boundary. The workflow pin and runtime assertion detect configuration drift. The assertion does not make a job safe after GitHub has assigned it to the wrong runner.

## Configuration Contract

Expand Down Expand Up @@ -117,7 +128,7 @@ Homelab output:

Dependent jobs pass the output through `fromJSON` in `runs-on`. No caller copies the authorization expression.

The selector job adds a small GitHub-hosted job to each validation run. This cost buys one auditable security decision for the fleet.
The selector job adds a small GitHub-hosted job to each validation run. This cost buys one auditable routing decision for the fleet.

[GitHub treats runner labels as cumulative requirements][github-runner-selection]. The homelab array therefore selects only a runner carrying every listed label.

Expand All @@ -128,9 +139,10 @@ The selector permits homelab execution only when every applicable condition pass
Allowed actors are exactly:

- `ptr727`
- `dependabot[bot]`
- `ptr727-codegen[bot]`

Dependabot-triggered work always selects GitHub-hosted Ubuntu. A dependency update can introduce untrusted executable code even when its pull request originates in this repository.

Allowed events are exactly:

- `push`
Expand All @@ -146,17 +158,20 @@ Every rejected homelab request emits a visible warning. The warning names the fa

Runner labels provide routing, not authorization. Existing workflow trigger, permission, environment, and actor checks remain in force.

The homelab runner belongs to a dedicated organization runner group. The group allows this repository and the approved validation workflow only. Its selected-workflow restriction names the protected workflow ref. Direct label requests from any other workflow cannot reach the runner.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## Public Repository Boundary

The first runner is scoped specifically to `ptr727/ProjectTemplate`. It is not an organization-wide runner.
The first runner is registered at the organization scope so runner-group restrictions can enforce repository and workflow access. It is not available organization-wide.

The CloudInit implementation retains its private-repository default. Public-repository support requires a separate, explicit opt-in.

The proposed provisioning contract requires:

- An explicit public-repository opt-in with a false default.
- An exact repository owner and name.
- Repository-scoped runner registration.
- A dedicated organization runner group restricted to `ptr727/ProjectTemplate`.
- A selected-workflow restriction for the approved validation workflow at its protected ref.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- The dedicated `homelab` label.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- A visible provisioning warning for a public repository.
- Documentation that workflow authorization remains mandatory.
Expand All @@ -173,17 +188,19 @@ The rollout proceeds in these stages:

1. Add the selector while `FLEET_RUNNER_TARGET` is absent.
2. Confirm every selectable job remains GitHub-hosted.
3. Provision the repository-scoped homelab runner.
3. Provision the homelab runner in its restricted organization runner group.
4. Confirm the runner carries every required label.
5. Set `FLEET_RUNNER_TARGET` to `homelab`.
6. Open a same-repository pull request as `ptr727`.
7. Confirm the canary job runs on the homelab runner.
8. Confirm Copilot's agentic job runs on GitHub-hosted Ubuntu.
9. Exercise the external pull request path.
10. Confirm the external path selects GitHub-hosted Ubuntu.
11. Take the homelab runner offline for a bounded observation.
11. Take the homelab runner offline for a maximum of ten minutes.
12. Confirm the trusted job remains visibly queued without fallback.
13. Restore the runner and finish the canary.
13. Cancel the queued workflow when the observation bound expires.
14. Confirm the cancellation reaches a terminal state.
15. Restore the runner and finish the canary with a new workflow run.

The rollout stops after any unexpected target selection. It also stops if Copilot delivers only a reduced review.

Expand Down Expand Up @@ -211,6 +228,7 @@ Automated tests cover:
- Explicit GitHub-hosted target.
- Invalid target.
- Every trusted actor.
- A Dependabot pull request selects GitHub-hosted Ubuntu.
- An untrusted actor.
- Internal pull request.
- Fork pull request.
Expand All @@ -220,6 +238,8 @@ Automated tests cover:
- Unsupported event.
- Exact GitHub-hosted label output.
- Exact homelab label output.
- A direct-label workflow cannot use the homelab runner group.
- An unapproved workflow ref cannot use the homelab runner group.
- A fixed GitHub-hosted Copilot runner.
- Absence of the fleet variable from Copilot configuration.
- Absence of homelab labels from Copilot configuration.
Expand Down Expand Up @@ -247,21 +267,25 @@ The CloudInit repository carries its public opt-in, provisioning tests, and oper

| Risk | Control |
| --- | --- |
| Fork code reaches a persistent runner | Fork identity check forces GitHub-hosted execution |
| Fork code reaches a persistent runner | Fork identity check forces GitHub-hosted execution, and runner-group restrictions reject unapproved workflows |
| An unexpected actor reaches homelab | Exact actor allowlist fails closed |
| A dependency update executes on a persistent runner | Dependabot always selects GitHub-hosted Ubuntu |
| A workflow bypasses the selector with direct labels | Runner-group selected-workflow restrictions reject it |
| A new event bypasses assumptions | Exact event allowlist fails closed |
| Invalid configuration widens access | Unknown values select GitHub-hosted Ubuntu |
| An offline runner stalls CI | Queued state is documented and observed during canary |
| Copilot is routed to a persistent VM | Dedicated Copilot workflow pins GitHub-hosted Ubuntu |
| Copilot is routed to a persistent VM | Organization policy requires standard GitHub-hosted runners and disables repository overrides |
| Copilot review silently loses agentic context | Acceptance requires review-session evidence |
| A public runner becomes fleet-wide | Repository-scoped registration is mandatory |
| A public runner becomes fleet-wide | Its organization runner group allows only the named repository and workflow |
| Provisioning weakens the private default | Public support requires an explicit false-by-default opt-in |

## Rollback

Unset `FLEET_RUNNER_TARGET` to return selectable jobs to GitHub-hosted Ubuntu. No workflow source rollback is required for this operational response.
Set `FLEET_RUNNER_TARGET` to `github-hosted` at its authoritative repository scope. Do not unset it, because an inherited value could become effective. No workflow source rollback is required for this operational response.

Cancel every queued or in-progress homelab workflow. Confirm each cancellation reaches a terminal state before restoring service.

Disable or remove the repository-scoped runner registration if routing remains unexpected. Preserve its diagnostic logs before removal.
Disable or remove the runner from its restricted group if routing remains unexpected. Preserve its diagnostic logs before removal.

The Copilot workflow remains GitHub-hosted throughout rollback.

Expand All @@ -271,15 +295,17 @@ Implementation waits for an explicit decision on each item.

- [ ] Use `FLEET_RUNNER_TARGET` as the configuration variable.
- [ ] Accept only `github-hosted` and `homelab` in phase 1.
- [ ] Use a GitHub-hosted selector job as the single authorization point.
- [ ] Use a GitHub-hosted selector job as the single routing decision point.
- [ ] Use a restricted organization runner group as the infrastructure authorization point.
- [ ] Route the reusable validation lint job as the first canary.
- [ ] Decide whether unit-test and repository-validation join the first canary.
- [ ] Add a dedicated GitHub-hosted `copilot-code-review.yml`.
- [ ] Select standard GitHub-hosted Copilot runners at the organization level and disable repository overrides.
- [ ] Keep standalone self-hosted Copilot review out of phase 1.
- [ ] Reserve ARC evaluation for phase 2.
- [ ] Require repository-scoped registration for the public hub.
- [ ] Restrict the organization runner group to the public hub and approved workflow ref.
- [ ] Add a false-by-default public-repository opt-in to CloudInit.
- [ ] Approve the trusted actor and event allowlists.
- [ ] Approve the trusted actor and event allowlists, with Dependabot excluded.
- [ ] Approve the offline-runner observation test.

## Implementation Checkpoints
Expand All @@ -290,7 +316,7 @@ Implementation waits for an explicit decision on each item.
- [ ] Structural tests pass.
- [ ] GitHub-hosted default branch exercised.
- [ ] CloudInit public opt-in reviewed in its owning repository.
- [ ] Repository-scoped runner registered.
- [ ] Restricted organization runner group and runner registered.
- [ ] Trusted homelab canary succeeds.
- [ ] Fork or external path stays GitHub-hosted.
- [ ] Copilot agentic review succeeds on GitHub-hosted infrastructure.
Expand Down
Loading
Loading