Documentation
¶
Overview ¶
Package runner implements `nself runner provision` and `nself runner verify` (G-012): building and auditing self-hosted GitHub Actions CI runner hosts.
Purpose: CI runner hosts were hand-built, so required system dependencies
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 — one had `gh`/`zip`/`unzip`, the other didn't, and the same commit passed or failed depending on which host claimed the job.
Inputs: a declarative Manifest (manifest.yaml, embedded below) plus an
Executor bound to one host (local or SSH).
Outputs: for provision, packages/user/sudoers/systemd units installed on
that host; for verify, a per-host CheckResult set that composeable callers turn into a cross-host parity matrix (report.go).
Constraints: this package never talks to a host directly — every side
effect goes through the Executor interface (exec.go), so unit tests exercise the exact command strings without touching real hardware.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RenderMatrix ¶
func RenderMatrix(reports []HostReport) string
RenderMatrix formats reports as a plain-text parity matrix: one row per check, one column per host, plus a trailing drift summary. This is the primary human-facing output of `nself runner verify` when checking more than one host.
Types ¶
type CheckResult ¶
type CheckResult struct {
Name string `json:"name"`
Status CheckStatus `json:"status"`
Detail string `json:"detail,omitempty"`
}
CheckResult is one named check's outcome on one host.
func VerifyHost ¶
func VerifyHost(ctx context.Context, ex Executor, m *Manifest) []CheckResult
VerifyHost runs every manifest-declared check against one host and returns the full result set. It never returns an error itself — a host that is entirely unreachable should be detected by the caller with a cheap reachability probe first (see VerifyHosts in report.go); if it isn't, every check below simply reports StatusFail with the underlying exec error as Detail, which is still an honest (if noisier) answer.
type CheckStatus ¶
type CheckStatus string
CheckStatus is the outcome of a single verify check.
const ( // StatusPass means the check found what it expected. StatusPass CheckStatus = "pass" // StatusFail means the check found the dependency/condition missing or // broken — this is what should block a job from being trusted on this // host. StatusFail CheckStatus = "fail" // StatusWarn is informational: the check couldn't run to completion in // a meaningful way (e.g. no cached Chromium yet) but that's not itself // evidence of drift. StatusWarn CheckStatus = "warn" )
type Dependency ¶
type Dependency struct {
Name string `yaml:"name"`
AptPackage string `yaml:"apt_package"`
Binary string `yaml:"binary"`
Reason string `yaml:"reason"`
}
Dependency is one required system package, checked either by looking for a binary on PATH (Binary != "") or by asking the package manager whether AptPackage is installed (Binary == "", e.g. a runtime library with no CLI entry point).
func (Dependency) HasBinary ¶
func (d Dependency) HasBinary() bool
HasBinary reports whether this dependency is checked via `command -v` rather than the package manager.
type DriftFinding ¶
type DriftFinding struct {
CheckName string `json:"check"`
ByHost map[string]CheckStatus `json:"by_host"`
}
DriftFinding names one check that disagrees across two or more reachable hosts — the thing `nself runner verify` exists to surface.
func DetectDrift ¶
func DetectDrift(reports []HostReport) []DriftFinding
DetectDrift compares every check name present on any reachable host and reports the ones whose status differs across hosts. Manifest checks are identical across hosts by construction, so any disagreement here is real drift, not a different question being asked.
type Executor ¶
type Executor interface {
Run(ctx context.Context, command string) (output string, err error)
Label() string
}
Executor runs one shell command against a bound host and reports its label (used in verify's parity matrix and provision's log lines).
type HostReport ¶
type HostReport struct {
Host string `json:"host"`
Checks []CheckResult `json:"checks,omitempty"`
// Err is set when the host itself could not be reached at all (SSH
// connection failure), as opposed to an individual check failing.
Err string `json:"error,omitempty"`
}
HostReport is one host's full verify result.
func VerifyHosts ¶
func VerifyHosts(ctx context.Context, executors []Executor, m *Manifest) []HostReport
VerifyHosts runs a cheap reachability probe against each executor, then the full VerifyHost check set against every host that answered. Hosts that don't answer get a single HostReport with Err set and no Checks, rather than N near-identical "connection refused" failures.
type LocalExecutor ¶
type LocalExecutor struct{}
LocalExecutor runs commands on the current machine via `bash -c`. Bash (not POSIX sh) is required because the chromium-cache check uses `shopt -s nullglob`; every provisioning target (Ubuntu runner hosts) and every dev machine this ships on has bash available.
type Manifest ¶
type Manifest struct {
SchemaVersion int `yaml:"schema_version"`
RunnerUser string `yaml:"runner_user"`
WorkDir string `yaml:"work_dir"`
Dependencies []Dependency `yaml:"dependencies"`
ChromiumCacheGlobs []string `yaml:"chromium_cache_globs"`
}
Manifest is the parsed form of manifest.yaml: the full declarative dependency set plus the fixed layout conventions (runner user, work dir, where to look for a cached Chromium) that provision and verify both need to agree on.
func LoadEmbeddedManifest ¶
LoadEmbeddedManifest parses the manifest compiled into the binary. Returns an error if the embedded YAML is malformed, which would indicate a build-time bug (a hand edit that broke the schema), not a runtime condition — callers should treat a non-nil error as fatal.
func ParseManifest ¶
ParseManifest decodes manifest YAML bytes. Exported so tests (and any future NSELF_RUNNER_MANIFEST override, mirroring the deprecation registry's pattern) can parse an alternate manifest without touching the embedded default.
func (*Manifest) AptPackages ¶
AptPackages returns the apt package names for every dependency, in manifest order, for a single `apt-get install -y <names...>` invocation.
type ProvisionOptions ¶
type ProvisionOptions struct {
// Instances is how many runner instances to register on this host.
// Defaults to 1.
Instances int
// InstallRoot is the base directory each instance installs into, as
// InstallRoot/runner-<N>. Defaults to /opt/actions-runner.
InstallRoot string
// GithubURL is the repo or org URL runners register against, e.g.
// https://github.com/nself-org/cli.
GithubURL string
// RegToken is a GitHub Actions runner registration token. Sourced from
// an env var by the caller (cmd/commands/runner_provision.go) — never
// hardcoded, never included in ProvisionResult.
RegToken string
// Labels are extra labels appended after the standard
// self-hosted,Linux,X64 set.
Labels []string
}
ProvisionOptions parameterizes one provisioning run.
type ProvisionResult ¶
type ProvisionResult struct {
Steps []ProvisionStep
}
ProvisionResult is the full record of a Provision run.
func Provision ¶
func Provision(ctx context.Context, ex Executor, m *Manifest, opts ProvisionOptions) (*ProvisionResult, error)
Provision installs the manifest's dependency set, creates the runner user with passwordless sudo, ensures the work directory is a real directory (never a symlink — see verify.go's checkWorkDirNotSymlink for why that matters), and installs opts.Instances runner instances as systemd services.
type ProvisionStep ¶
ProvisionStep records one step's name and captured output.
type SSHExecutor ¶
type SSHExecutor struct {
Target deploy.RemoteTarget
}
SSHExecutor runs commands on a remote host over SSH, reusing deploy.RunRemoteCommand so runner provisioning shares exactly one SSH convention (key resolution, StrictHostKeyChecking, ForwardAgent=no) with `nself deploy` and every other remote-targeting command in the CLI.
func NewSSHExecutor ¶
func NewSSHExecutor(sshTarget, keyPath string) SSHExecutor
NewSSHExecutor builds an SSHExecutor from a "user@host" (or "user@host:/path", though runner provisioning ignores the path component) string and an SSH key path. An empty keyPath resolves the same NSELF_DEPLOY_KEY_PATH / NSELF_DEPLOY_SSH_KEY env-var convention `nself deploy` uses, defaulting to ~/.ssh/id_ed25519, so every remote-targeting command in the CLI shares one key-resolution rule.