# CI Setup

> Check CI prerequisites and configure the self-hosted runner SSH config. Use when first installing the plugin, after adding runners, or when CI commands fail with auth or connectivity errors.

- Skill: `kinginyellows/ci-setup` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kinginyellows/ci-setup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kinginyellows/ci-setup/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: kinginyellows (https://skillmd.com/u/kinginyellows)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kinginyellows/ci-setup

---


## What It Does

Verifies the CI prerequisites (`gh`, `jq`, `ssh`) and GitHub CLI authentication,
then optionally walks a wizard to configure the plugin's runner SSH config —
previewing the config and confirming before any write.

## When to Use

- First installing the plugin, after adding runners, or when CI commands fail
  with auth or connectivity errors.

## Usage

This skill is wizard-driven and takes no arguments; the argument text after the
skill name (if any) is available as context.

**Config location.** This skill operates on *the plugin's runner SSH config
file*, `yellow-ci.local.md`. When an invoking command supplies a host-resolved
path (on Claude Code, the repo-local plugin config), use that path. Otherwise
— for example, on direct invocation with no invoking command — fall back to
the host-neutral default:
`${XDG_CONFIG_HOME:-$HOME/.config}/yellow-ci/yellow-ci.local.md`. Never
hard-code a host-specific config directory into this body.

**Runner scope.** yellow-ci targets **Linux** self-hosted runners. Windows/macOS
runners are not probed by the health/cleanup workflows; note this if a
non-Linux runner is configured.

### Step 1: Check Prerequisites

Run all prerequisite checks in a single Bash call:

```bash
printf '=== Prerequisites ===\n'
command -v gh  >/dev/null 2>&1 && printf 'gh:  ok\n' || printf 'gh:  NOT FOUND\n'
command -v jq  >/dev/null 2>&1 && printf 'jq:  ok\n' || printf 'jq:  NOT FOUND\n'
command -v ssh >/dev/null 2>&1 && printf 'ssh: ok\n' || printf 'ssh: NOT FOUND (needed for runner health/cleanup)\n'
```

Collect **all** missing-tool failures before stopping — report them together.

Stop conditions (after reporting all failures):

- `gh` not found: "GitHub CLI is required. Install from https://cli.github.com/
  then run `gh auth login`."
- `jq` not found: "jq is required. Install from
  https://jqlang.github.io/jq/download/".

`ssh` missing is a warning only — it affects the runner health, cleanup, and
self-hosted-optimization workflows. Diagnosis and linting are unaffected.

### Step 2: Check GitHub CLI Auth

```bash
gh auth status 2>&1
```

- **Non-zero exit:** if the output says "not logged in", report "GitHub CLI is
  not authenticated. Run: `gh auth login`" and stop; if it looks like a network
  error, report "Could not reach GitHub API — check connectivity." and stop.
- **Zero exit:** record PASS; parse the logged-in account and the
  `Active token scopes:` line. Warn (do not stop) if `repo` or `workflow` are
  absent, pointing at `gh auth login --scopes repo,workflow,read:org`.

### Step 3: Check Existing Config

Check whether the runner SSH config file already exists and its YAML front
matter (after the opening `---` delimiter) contains a `schema: 1` line. If it
does, read it, count runners
(`awk '/^[[:space:]]*- name:/{c++} END{print c+0}'` — always emits one integer),
list the runner names, and ask via `AskUserQuestion`: "Runner config already
exists (N runner(s): …). Reconfigure?"

- **No** → show the current config summary and stop.
- **Yes** → continue to Step 4.

If absent, continue to Step 4.

### Step 4: SSH Config Wizard (Optional)

Ask via `AskUserQuestion`: "Do you use self-hosted GitHub Actions runners?
(Required for runner health and cleanup)".

- **No** → prepare a minimal config (no runners block); skip to Step 5.
- **Yes** → collect runner details below, looping until the user says done.

For each runner, collect and **validate** (reject and re-prompt on failure —
never warn-and-continue):

- **Runner name** — must match `^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$` (length 2-64,
  DNS-safe).
- **SSH host** — accept only a private IPv4 (`10.x`, `127.x`, `172.16-31.x`,
  `192.168.x`) or an internal FQDN ending in `.internal`, `.local`, `.lan`,
  `.corp`, `.home`, `.intra`, or `.private`. Public IPs and public-TLD hostnames
  are rejected — private network only.
- **SSH user** — must match `^[a-z_][a-z0-9_-]{0,31}$`.
- **SSH key path** (optional) — if provided, must start with `~/` or `/`,
  contain only safe characters, and contain no `..` or shell metacharacters.
  Reject the `~user/...` form: it would pass a looser "starts with `~`" check,
  but `ci-runner-health`'s expansion only resolves `~/`, so such a key would
  reach `ssh` as a literal tilde path and silently fail the probe. Accepting
  only the forms that are actually expanded keeps setup and health-check in
  agreement on which key paths are usable.

**Security — never place a raw collected value inside a shell heredoc body.**
A quoted delimiter (`<<'EOF'`) only blocks `$`/backtick expansion inside the
body; it does not stop the body from containing a line that matches the
delimiter itself. A value with an embedded newline followed by that line
closes the heredoc early, and everything after it runs as shell commands —
before any pattern check above ever sees the value. Do not "fix" this with a
longer or more unusual delimiter either: this skill body is committed to the
repo and world-readable, so a deliberately hostile answer can just reuse
whatever delimiter is documented here — a unique delimiter only rules out
accidental collision, not a chosen attack.

Instead, treat every raw answer as opaque until it has been checked: write it
to a fresh temp file with the `Write` tool (its content is a structured
parameter, never parsed as shell) before it touches any Bash command, then run
the pattern check above by reading that file's content (for example, `grep
-Eq '<pattern>' "$tmpfile"`) — never by interpolating the raw text inline in a
command string or heredoc body. Reject and re-prompt on any check failure, and
delete the temp file once the field is accepted or rejected. Only a value that
has passed its check — which already excludes newlines, quotes, `$`, `;`, and
other shell metacharacters — may be reused afterward.

### Step 5: Preview and Confirm, Then Write Config

**Preview first (R32).** Render the exact config content (schema header, each
`- name:` runner block with `host`/`user`/optional `ssh_key`, and the `defaults`
block) and show it to the user. Then ask via `AskUserQuestion`: "Write this
runner config? [Write / Edit / Cancel]". Only write after explicit
confirmation; on a host without `AskUserQuestion`, obtain an equivalent explicit
user confirmation first — never write config without one. Write the confirmed
content with the `Write` tool, not a shell heredoc or redirection — the
rendered content still carries the collected `host`/`user`/`name` values, so
Step 4's no-heredoc rule applies here too.

Config shape (with runners):

```yaml
---
schema: 1
runners:
  - name: [name]
    host: [host]
    user: [user]
    ssh_key: ~/.ssh/runner-key   # only when the user provided a key path
defaults:
  ssh_timeout: 3
  max_parallel_ssh: 5
  cache_dirs:
    - /home/runner/.cache
  log_retention_days: 14
  docker_prune_age: 168h
---

## Runner Notes

Configured on [ISO-8601-UTC timestamp]. Edit this file directly to add runners
or change defaults, or re-run setup to reconfigure interactively.
```

With no runners (user declined), write the same shape with a commented-out
`# runners: []` placeholder and the `defaults` block. Create the parent config
directory first if it does not exist. Obtain the timestamp with
`date -u +%Y-%m-%dT%H:%M:%SZ`.

### Step 6: Validate the Written Config

Confirm the written file's YAML front matter (after the opening `---`
delimiter) contains `schema: 1`. Then re-read it and verify: the runner count
matches the number collected in Step 4, and each runner name entered appears
verbatim. On any mismatch, report it and stop.

### Step 7: Report

Report prerequisites, GitHub CLI account/scopes, and the runner config summary
(where it was written, runner count, names). If the config was written to a
repo-local path (an invoking command's host-resolved path inside the
repository), advise: the config contains runner hostnames — if this is a
shared repository, add the config to `.gitignore` to avoid committing host
details. The host-neutral fallback location lives outside the repository, so
this advice does not apply when the config was written there.

### Error Handling

- Missing `gh`/`jq` → collect and stop with install links; missing `ssh` →
  warn and continue.
- `gh` not authenticated → "Run: `gh auth login`"; network error → "Could not
  reach GitHub API".
- Invalid runner name / SSH host / SSH user / SSH key path → report the specific
  rule and re-prompt.
- Config directory not creatable, or write/validation failure → report a
  permissions hint and stop.

### Success Criteria

- Prerequisites and auth are reported accurately.
- The runner config is previewed and confirmed before any write, validated
  after writing, and the written runner names/count match what was collected.

