# gha-runner-image-proof

`gha-runner-image-proof` is the GitHub Actions runner image and toolcache compatibility oracle for offline CI. It validates a runner-image manifest against the host or a Docker image, classifies whether `actions/setup-node@v4` can be satisfied from the toolcache, and emits text/JSON/Markdown receipts that an offline runner (such as `ci-forge`) can attach to plan/run provenance.

It is part of Wildmason's offline GitHub Actions proof-tool lane:

- `gha-workflow-proof` validates workflow YAML structure.
- `gha-eventsmith` generates event/context fixtures.
- `gha-expression-proof` evaluates `${{ … }}` expressions.
- `gha-cache-proof` models `actions/cache`.
- `gha-artifact-proof` models `actions/upload-artifact` and `actions/download-artifact`.
- `gha-service-proof` models service-container networking and readiness.
- `gha-command-proof` validates runtime command/env-file behavior.
- `gha-github-service-proof` classifies the GitHub API side-effect surface.
- **`gha-runner-image-proof`** validates runner images and toolcache layout against pinned manifests (this crate).

## What it covers

- A canonical, versioned runner-image manifest (`schema_version: 1`) covering image identity, labels, tools, packages, capabilities, environment defaults, toolcache root, and upstream evidence.
- `check` validates the manifest's internal shape and self-consistency.
- `probe-host` verifies the current host against the manifest by shelling out to declared tool binaries and OS package queries.
- `probe-docker` verifies a Docker image against the manifest using `docker image inspect` and `docker run --rm <image>`. CLI-only — no Docker socket, no SDK. Gracefully skips when Docker is absent; reports clearly when the daemon is unreachable.
- `setup-node` is offline: it classifies whether `actions/setup-node@v4` can be satisfied from the toolcache for a requested version/arch using only the manifest's own data and declared aliases. Broad version specs that cannot be resolved offline return `setup-node.version_resolution_requires_catalog`.
- No baked-in catalog of GitHub-hosted runner images. The manifest is the expectation.

## Install

```powershell
cargo install gha-runner-image-proof --version 1.0.0 --locked
```

## Manifest

The manifest is JSON, owned by this crate, and versioned. A minimal manifest:

```json
{
  "schema_version": 1,
  "tool": { "name": "ci-forge", "version": "0.1.0" },
  "generated_at": "2026-05-12T00:00:00Z",
  "image": {
    "name": "ci-forge/mortar-ubuntu-22.04",
    "tag": "local",
    "os": { "family": "linux", "distro": "ubuntu", "version": "22.04", "arch": "x64" }
  },
  "labels": ["self-hosted", "linux", "x64", "ubuntu-22.04"],
  "capabilities": ["docker", "docker-socket"],
  "toolcache": { "root": "/opt/hostedtoolcache" },
  "tools": [
    {
      "name": "node",
      "version": "22.11.0",
      "arch": "x64",
      "path": "/opt/hostedtoolcache/node/22.11.0/x64",
      "aliases": ["22", "lts/jod"],
      "in_toolcache": true
    },
    { "name": "cargo", "version": "1.85.0", "path": "/usr/local/cargo/bin/cargo" },
    { "name": "docker", "version": "27.3.1", "path": "/usr/bin/docker" }
  ],
  "packages": [
    { "manager": "apt", "name": "docker.io", "version": "27.3.1-0ubuntu1" }
  ],
  "environment": {
    "RUNNER_OS": "Linux",
    "RUNNER_ARCH": "X64",
    "RUNNER_TOOL_CACHE": "/opt/hostedtoolcache",
    "ImageOS": "ubuntu22",
    "ImageVersion": "20260512.0"
  },
  "evidence": {
    "source_urls": [
      "https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2204-Readme.md"
    ]
  }
}
```

See `docs/manifest-schema.md` for the full schema.

## Commands

### `check`

Validate a manifest's shape and internal consistency:

```powershell
gha-runner-image-proof check `
  --manifest path/to/runner-manifest.json `
  --format json `
  --output target/runner-image-proof.json
```

### `probe-host`

Verify the current host against the manifest:

```powershell
gha-runner-image-proof probe-host `
  --manifest path/to/runner-manifest.json `
  --tool node `
  --tool cargo `
  --package docker.io `
  --format json
```

Tool probes run `<tool> --version`; package probes use the manager declared in the manifest (`apt` → `dpkg -s`, `dnf` → `rpm -qi`, `brew` → `brew info`, etc.).

### `probe-docker`

Verify a Docker image against the manifest:

```powershell
gha-runner-image-proof probe-docker `
  --manifest path/to/runner-manifest.json `
  --image ci-forge/mortar-ubuntu-22.04:local `
  --tool node `
  --tool cargo `
  --tool docker `
  --package libwebkit2gtk-4.1-dev `
  --format json
```

Receipts capture `docker image inspect` evidence (digest, repo tags) plus per-probe command, exit code, and stdout/stderr excerpts. If the Docker CLI is absent the probe is `skip` with `probe.docker_cli_not_found`; if the daemon is unreachable the probe is `fail` with `probe.docker_daemon_unreachable`.

### `setup-node`

Offline classification of `actions/setup-node@v4` against the manifest:

```powershell
gha-runner-image-proof setup-node `
  --manifest path/to/runner-manifest.json `
  --version 22 `
  --arch x64 `
  --format json
```

The receipt names the requested version, the resolved version (if known from a manifest alias), the arch, the toolcache path that would be hit, the PATH entry that would be prepended, and the compatibility classification (`exact`, `simulated`, or `unsupported`). Broad specs (`22`, `lts/*`) require manifest aliases to resolve; otherwise the receipt fails with `setup-node.version_resolution_requires_catalog`.

## Classification model

- `exact` — the manifest declares the requested artifact and the host/image matches.
- `simulated` — the manifest is internally plausible but the host/image evidence is partial or local-only.
- `unsupported` — the manifest cannot satisfy the request (no toolcache entry, wrong arch, broad spec without an alias, etc.). Each unsupported receipt carries a reason code.

## GitHub Action

```yaml
- uses: wildmason/gha-runner-image-proof@v1
  with:
    command: check
    manifest: path/to/runner-manifest.json
    format: json
    output: target/runner-image-proof.json
```

The action installs the crate with Cargo unless `gha-runner-image-proof` is already on `PATH`.

## Exit behavior

The command exits nonzero when any check fails. With `--strict`, warnings also fail the command. Receipts are valid in all three formats regardless of exit status.

## Receipts

Receipts are available as `text`, `json`, or `markdown`. JSON receipts are intended for offline runners to attach as runner-image provenance. The receipt schema is stable; new fields are additive and `schema_version` advances on breaking changes.
