Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/command-inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -1900,6 +1900,41 @@
"--verbose"
]
},
{
"name": "runner",
"path": "nself runner",
"short": "Provision and audit self-hosted GitHub Actions CI runner hosts",
"hidden": false,
"group_id": "advanced",
"subcommands": [
{
"name": "provision",
"path": "nself runner provision",
"short": "Install runner dependencies, user, sudoers, and N runner instances",
"hidden": false,
"flags": [
"--github-url",
"--host",
"--install-root",
"--instances",
"--labels",
"--ssh-key",
"--token"
]
},
{
"name": "verify",
"path": "nself runner verify",
"short": "Check host(s) against the dependency manifest and report drift",
"hidden": false,
"flags": [
"--host",
"--json",
"--ssh-key"
]
}
]
},
{
"name": "secrets",
"path": "nself secrets",
Expand Down
10 changes: 9 additions & 1 deletion .github/command-surface-budget.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,12 @@
# `hcloud server delete`, with none of the backup/IP-protection safety this
# command enforces. `nself server provision/list/resize/destroy` is the
# surface G-011 names directly.
51
#
# 2026-09-11: +1 for `runner` (gap G-012). Deliberate exception, not
# creep: onboarding a self-hosted CI runner host had no CLI path — the
# dependency manifest (gh/zip/unzip/etc.) was installed and verified by
# hand, which is exactly how one host silently drifted from another and
# hid a job failure behind a misleading browser error for hours on
# 2026-09-11. `nself runner provision/verify` is the surface G-012 names
# directly.
52
9 changes: 9 additions & 0 deletions .github/surface-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,15 @@
"env_vars": "n/a",
"openapi": "n/a (see below)"
},
{
"name": "runner",
"path": "nself runner",
"group_id": "advanced",
"wiki_page": true,
"mcp_tool": false,
"env_vars": "undocumented: GITHUB_RUNNER_TOKEN",
"openapi": "n/a (see below)"
},
{
"name": "secrets",
"path": "nself secrets",
Expand Down
3 changes: 2 additions & 1 deletion .github/surface-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ One row per top-level command (CLI-R17), scored against the four surfaces a comm
| `nself remove` | extend | yes | no | n/a | n/a (see below) |
| `nself reset` | core | yes | no | n/a | n/a (see below) |
| `nself restart` | core | yes | yes | n/a | n/a (see below) |
| `nself runner` | advanced | yes | no | undocumented: GITHUB_RUNNER_TOKEN | n/a (see below) |
| `nself secrets` | config | yes | no | undocumented: EDITOR | n/a (see below) |
| `nself security` | advanced | yes | no | n/a | n/a (see below) |
| `nself self-heal` | observe | yes | no | n/a | n/a (see below) |
Expand All @@ -64,4 +65,4 @@ One row per top-level command (CLI-R17), scored against the four surfaces a comm
| `nself verify-sbom` | advanced | yes | no | n/a | n/a (see below) |
| `nself version` | account | yes | no | undocumented: BENCH_RESULTS_FILE | n/a (see below) |

Total: 51 commands. Missing wiki page: 0. No MCP tool: 34. Env vars found but undocumented: 16.
Total: 52 commands. Missing wiki page: 0. No MCP tool: 35. Env vars found but undocumented: 17.
3 changes: 2 additions & 1 deletion .github/wiki/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ tree in `cmd/commands/`. Run `make cmd-inventory` to refresh.
## Complete index

Generated from the cobra registration tree in `cmd/commands/`.
Run `make cmd-inventory` to refresh. **Total top-level commands: 51**
Run `make cmd-inventory` to refresh. **Total top-level commands: 52**

| Command | Short Description | Group | Subcommands |
|---|---|---|---|
Expand Down Expand Up @@ -134,6 +134,7 @@ Run `make cmd-inventory` to refresh. **Total top-level commands: 51**
| `nself remove` | Remove an installed plugin or bundle | extend | — |
| `nself reset` | Stop containers, remove all data volumes, and clean generated files | core | — |
| `nself restart` | Smart restart with config change detection | core | — |
| `nself runner` | Provision and audit self-hosted GitHub Actions CI runner hosts | advanced | provision, verify |
| `nself secrets` | Manage encrypted project secrets (age encryption) | config | audit, decrypt-on-deploy, edit, get, init, lint, list, list-schedules, rekey, retire, rotate, rotation-log, schedule, set, verify |
| `nself security` | Server security: audit, setup, and status | advanced | audit, setup, status |
| `nself self-heal` | Run targeted self-healing routines for nSelf components | observe | — |
Expand Down
4 changes: 2 additions & 2 deletions .github/wiki/_Sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@

<!-- BEGIN GENERATED:command-list -->

**All commands (51)**
**All commands (52)**

- _A:_ [[cmd-access]] · [[cmd-account]] · [[cmd-admin]]
- _B:_ [[cmd-backup]] · [[cmd-build]] · [[cmd-bundle]]
Expand All @@ -294,7 +294,7 @@
- _M:_ [[cmd-man]] · [[cmd-mcp]] · [[cmd-migrate]]
- _O:_ [[cmd-oauth]] · [[cmd-ops]]
- _P:_ [[cmd-plugin]] · [[cmd-promote]]
- _R:_ [[cmd-remove]] · [[cmd-reset]] · [[cmd-restart]]
- _R:_ [[cmd-remove]] · [[cmd-reset]] · [[cmd-restart]] · [[cmd-runner]]
- _S:_ [[cmd-secrets]] · [[cmd-security]] · [[cmd-self-heal]] · [[cmd-server]] · [[cmd-service]] · [[cmd-start]] · [[cmd-status]] · [[cmd-stop]]
- _T:_ [[cmd-telemetry]] · [[cmd-template]] · [[cmd-trust]]
- _U:_ [[cmd-update]] · [[cmd-urls]]
Expand Down
84 changes: 84 additions & 0 deletions .github/wiki/cmd-runner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# nself runner

<!-- BEGIN PROSE:summary -->
> Provision and audit self-hosted GitHub Actions CI runner hosts.
<!-- END PROSE:summary -->

## Synopsis

```
nself runner <subcommand> [flags]
```

## Description

<!-- BEGIN PROSE:description -->
Provision and audit self-hosted GitHub Actions CI runner hosts.

Runner hosts were previously hand-built: required system dependencies (gh,
zip, unzip, Playwright/Chromium's shared libraries, ...) were discovered
only when a job failed mid-run, and two hosts advertising the identical
GitHub Actions labels (self-hosted,Linux,X64) could silently drift apart —
the same commit would pass or fail depending on which host claimed the job.

The dependency set is declarative (internal/runner/manifest.yaml, compiled
into this binary) so provision and verify always check the same list.

Subcommands:
provision Install dependencies, create the runner user, register N
runner instances as systemd services
verify Check one or more hosts against the manifest and print a
parity matrix — the important half: this is how "same
labels, different tools" gets caught before it causes a
mystery failure.
<!-- END PROSE:description -->

## Flags

<!-- BEGIN GENERATED:flags -->
| Flag | Default | Description |
|------|---------|-------------|
| `--help`, `-h` | — | Show help |
<!-- END GENERATED:flags -->

## Subcommands

<!-- BEGIN GENERATED:subcommands -->
| Name | Description |
|------|-------------|
| `provision` | Install runner dependencies, user, sudoers, and N runner instances |
| `verify` | Check host(s) against the dependency manifest and report drift |
<!-- END GENERATED:subcommands -->

## Examples

<!-- BEGIN PROSE:examples -->
```bash
# Check this machine against the manifest
nself runner verify

# Check two remote runner hosts in one pass and print a parity matrix —
# this is how "same GitHub Actions labels, different tools installed"
# gets caught before it causes a mystery job failure
nself runner verify --host ci@runner-a.example.com --host ci@runner-b.example.com

# Machine-readable output for a CI gate
nself runner verify --host ci@runner-a.example.com --json

# Provision a fresh host with 2 runner instances
GITHUB_RUNNER_TOKEN=... nself runner provision \
--host ci@runner-a.example.com \
--github-url https://github.com/nself-org/cli \
--instances 2 \
--labels nself-ci
```
<!-- END PROSE:examples -->

## See Also

<!-- BEGIN PROSE:see-also -->
- [[Commands]] — full command index
- [[Core-Services]] — what a stack is made of
<!-- END PROSE:see-also -->

← [[Commands]] | [[Home]] →
17 changes: 16 additions & 1 deletion .github/wiki/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ nself build # generate docker-compose + nginx
nself start # boot the stack
```

## Commands (51)
## Commands (52)

### nself access

Expand Down Expand Up @@ -794,6 +794,21 @@ Flags:

Full page: [[cmd-restart]]

### nself runner

Provision and audit self-hosted GitHub Actions CI runner hosts

```
nself runner <subcommand> [flags]
```

Subcommands:

- `provision` — Install runner dependencies, user, sudoers, and N runner instances
- `verify` — Check host(s) against the dependency manifest and report drift

Full page: [[cmd-runner]]

### nself secrets

Manage encrypted project secrets (age encryption)
Expand Down
6 changes: 6 additions & 0 deletions cmd/commands/error_harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@ var errorHarnessCases = []errorHarnessCase{
{"verify-sbom", []string{"verify-sbom"}, "(a) no project dir"},
{"verify-sbom", []string{"verify-sbom", "--no-such-flag-xyz"}, "(b) invalid flag"},
{"verify-sbom", []string{"verify-sbom", "unknownsub_xyz"}, "(c) unknown sub"},

// ── runner (G-012) ─────────────────────────────────────────────────────
// runner root returns cmd.Help() (nil) — soft case (no project required).
{"runner", []string{"runner"}, "(a) shows help (no project required)"},
{"runner", []string{"runner", "--no-such-flag-xyz"}, "(b) invalid flag"},
{"runner", []string{"runner", "unknownsub_xyz"}, "(c) unknown sub"},
}

// runErrorHarnessCmd executes the given args against a fresh RootCmd clone
Expand Down
1 change: 1 addition & 0 deletions cmd/commands/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ var commandGroupAssignments = map[string]string{
"security": groupAdvanced,
"server": groupAdvanced,
"verify-sbom": groupAdvanced,
"runner": groupAdvanced,
}

// ApplyCommandGroups registers the groups and assigns each command to one.
Expand Down
70 changes: 70 additions & 0 deletions cmd/commands/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package commands

// Purpose: `nself runner` — build and audit self-hosted GitHub Actions CI
// runner hosts (G-012). Closes the gap where runner hosts were hand-built
// so required system dependencies were discovered only when a job failed,
// and two hosts advertising identical GitHub Actions labels could
// silently drift apart.
// Inputs: none directly — see runner_provision.go and runner_verify.go for
// each subcommand's flags.
// Outputs: registers `runner` (with `provision` and `verify` subcommands)
// on RootCmd.
// Constraints: all host-affecting logic lives in internal/runner; this file
// is wiring only, matching every other command group in cmd/commands.
// SPORT: CLI-CMD-RUNNER-001

import (
"github.com/nself-org/cli/internal/runner"
"github.com/spf13/cobra"
)

var runnerCmd = &cobra.Command{
Use: "runner",
Short: "Provision and audit self-hosted GitHub Actions CI runner hosts",
Long: `Provision and audit self-hosted GitHub Actions CI runner hosts.

Runner hosts were previously hand-built: required system dependencies (gh,
zip, unzip, Playwright/Chromium's shared libraries, ...) were discovered
only when a job failed mid-run, and two hosts advertising the identical
GitHub Actions labels (self-hosted,Linux,X64) could silently drift apart —
the same commit would pass or fail depending on which host claimed the job.

The dependency set is declarative (internal/runner/manifest.yaml, compiled
into this binary) so provision and verify always check the same list.

Subcommands:
provision Install dependencies, create the runner user, register N
runner instances as systemd services
verify Check one or more hosts against the manifest and print a
parity matrix — the important half: this is how "same
labels, different tools" gets caught before it causes a
mystery failure.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}

func init() {
runnerCmd.AddCommand(runnerProvisionCmd)
runnerCmd.AddCommand(runnerVerifyCmd)
RootCmd.AddCommand(runnerCmd)
}

// runnerExecutorsFromFlags builds one runner.Executor per --host flag
// value, or a single runner.LocalExecutor when no --host is given. Shared
// by both subcommands so "no --host means check/act on this machine" and
// "user@host means SSH" behave identically for provision and verify.
func runnerExecutorsFromFlags(hosts []string, sshKey string) []runner.Executor {
if len(hosts) == 0 {
return []runner.Executor{runner.LocalExecutor{}}
}
executors := make([]runner.Executor, len(hosts))
for i, h := range hosts {
if h == "local" {
executors[i] = runner.LocalExecutor{}
continue
}
executors[i] = runner.NewSSHExecutor(h, sshKey)
}
return executors
}
Loading
Loading