diff --git a/CHANGELOG.md b/CHANGELOG.md index 3981fc7..1916953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ ## [Unreleased] +### Added + +- **Personal-account consumer tier.** A repository owned by a *personal* GitHub + account (not an organization) inherits the private-free posture but cannot + reach an org-level self-hosted runner group — it needs a **repo-level** + runner registration and a caller that routes every job to that label. New + [docs/18-personal-account-tier.md](docs/18-personal-account-tier.md) records + the runner-registration procedure (org-runner vs repo-runner reachability is a + platform gate, not a setting), the inversion against [02 private-free], and + the **trigger caveat**: cross-owner reusable workflows do not resolve jobs for + `push` events to the default branch of a personal-account repo (verified + empirically — `pull_request` works, `push` hangs in `pending` with zero jobs). + New [examples/personal/security-selfhosted.yml](examples/personal/security-selfhosted.yml) + is the private-free stack (gitleaks, actionlint, zizmor-no-SARIF) with a + `runner:` input on every call and `pull_request`-only triggers. No catalog or + workflow file changed — the capability set is identical to private-free; only + the runner and trigger differ. + ## [0.13.3] - 2026-08-03 ### Added diff --git a/docs/00-overview.md b/docs/00-overview.md index 1a311cc..53cdb8c 100644 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -47,6 +47,16 @@ most consequential correction: Artifact Attestations are gated on Enterprise Cloud for private repos, and this estate **has** it, so private repositories release with full provenance instead of the `-free` variant. +### Personal-account repositories + +A repository owned by a **personal** account (not an organization) takes the +[private-free](02-private-free.md) posture regardless of visibility — it has no +GHAS, no Enterprise Cloud, and a 2 000 min/month Actions quota with no org pool. +Its runner strategy differs from a generic private-free repo: every job is +routed to a **repo-level** self-hosted runner, because an org-level runner is +not reachable from a personal-account namespace. See +[18 Personal-account tier](18-personal-account-tier.md). + ## How to consume a reusable workflow Reference by `owner/repo/.github/workflows/.yml@` from a caller @@ -106,7 +116,8 @@ For end-to-end caller examples per tier, see the tier docs and the repository [02 Private free](02-private-free.md) · [03 Private paid / GHAS](03-private-paid-ghas.md) · [16 Code Quality](16-code-quality.md) · - [17 NDDev estate](17-nddev-tier.md) + [17 NDDev estate](17-nddev-tier.md) · + [18 Personal account](18-personal-account-tier.md) - Platform: [04 Actions core](04-actions-core.md) · [05 Runners](05-runners.md) - Security: [06 Security scanning](06-security-scanning.md) · [07 Supply chain / SLSA / SBOM / attestations](07-supply-chain-slsa-sbom-attestations.md) diff --git a/docs/18-personal-account-tier.md b/docs/18-personal-account-tier.md new file mode 100644 index 0000000..c27fc20 --- /dev/null +++ b/docs/18-personal-account-tier.md @@ -0,0 +1,114 @@ +# Personal-account tier — a private repo under a personal GitHub account + +The other tier docs classify a repository by **visibility** (public / private) +and **plan** (free / GHAS). This one adds a third axis the generic model does +not surface: whether the repository is owned by an **organization** or by a +**personal account**. A personal-account repository is the *degenerate* case of +private-free — it inherits that posture in full, *and* it cannot reach an +organization's self-hosted runner fleet, so its runner strategy is different. + +## Why this doc exists + +The [NDDev estate tier](17-nddev-tier.md) records that the `NDDev-it-com` +organization has bought Enterprise Cloud, Code Security, Secret Protection, and +Code Quality, so its private repositories run the paid stack. **A repository +owned by a personal account has none of that**, even if the same human owns both. +Licenses attach to the organization, not to the user's personal namespace, so: + +- a private personal-account repo has **no GHAS** → no CodeQL, no native secret + scanning, no dependency review; +- a private personal-account repo has **no Enterprise Cloud** → no Artifact + Attestations; +- a private personal-account repo is billed against the **2 000 min/month** + personal Actions quota (catalog fact + `github-actions-private-free-personal`), with no organization pool to spill + into. + +Functionally this is identical to [02 private-free](02-private-free.md). The +runner routing is the only part that differs, and it is the reason this doc +exists. + +## The correction this tier exists to make + +[02 private-free] tells a private repo to use GitHub-hosted runners and simply +accept the 2 000 min/month ceiling. For a *personal-account* repository that +ceiling is per-user, not per-organization, and there is no committer-pool to +amortize it across. A single active project with a few minutes of CI per push +can exhaust the monthly quota in days. **Route every job to a self-hosted +runner registered on the repository itself.** + +| Concern | Generic private-free | Personal-account repo | +| --- | --- | --- | +| Posture (capabilities) | private-free | **private-free** (same) | +| CodeQL / secret scanning / dep review | excluded (paid) | **excluded** (no GHAS, same exclusions) | +| Artifact attestations | `release-supply-chain-free.yml` | **`release-supply-chain-free.yml`** (same) | +| Runner | GitHub-hosted `ubuntu-latest` | **self-hosted, repo-level registration** | + +## Runner registration: org vs personal + +A self-hosted runner registered on an **organization** is reachable only from +repositories *inside that organization*. A repository owned by a personal +account is in a different namespace and **cannot** consume an org-level runner +group, regardless of the group's `visibility` setting — `visibility=all` means +"all repos *in this org*", not "all repos everywhere". This is a platform gate, +not a configuration oversight; it cannot be relaxed by settings. + +To serve a personal-account repository, register the runner **on the repository +itself**: + +1. `gh api -X POST repos/{owner}/{repo}/actions/runners/registration-token` → a + short-lived token (≈1 h). This endpoint is available on private repos with + admin access; GitHub Pro is **not** required for repo-level runner + registration. +2. On the runner host, run `./config.sh --url https://github.com/{owner}/{repo} + --token --labels ",linux,x64"` once per repository. A single + runner process serves exactly one repository; N repositories need N + separate runner install directories. +3. Install the systemd unit (`./svc.sh install && ./svc.sh start`) so the + listener survives reboots. +4. In the caller workflow, set `runs-on: [self-hosted, Linux, X64, ]` and + pass the same label as `runner:` to every reusable workflow call. + +See [05 Runners → Routing by visibility](05-runners.md#routing-by-visibility) +for the load-bearing invariant: a self-hosted runner reachable from a public +repository is an RCE path, so this routing is for **private** personal-account +repos only. If the repository is or may become public, use the GitHub-hosted +caller in [examples/private-free/security.yml](../examples/private-free/security.yml). + +## Recommended caller + +[`examples/personal/security-selfhosted.yml`](../examples/personal/security-selfhosted.yml) +— the private-free stack (gitleaks, actionlint, zizmor-no-SARIF) with every job +routed to the repository's self-hosted runner. It is +`examples/private-free/security.yml` plus a `runner:` input on each call; the +capability set is identical because the tier posture is identical. + +## Trigger caveat: `pull_request` only, not `push` + +A reusable workflow owned by a **different account** (e.g. this library, +`NDDev-it-com/ci-workflows`, called from a personal-account repository) does +**not** resolve jobs for `push` events to the default branch. The run is +created but hangs in `pending` with zero jobs — indefinitely. This was +verified empirically on `rldyourmnd/declaro-platform`: the same workflow file +succeeded instantly on `pull_request` (run completed in seconds) while `push` +runs to `main` hung for 8+ hours with no jobs spawned. The mechanism is a +GitHub-side cross-owner workflow resolution gate, not a misconfiguration — +`allowed_actions: all` and full admin access do not unblock it. + +**Use `pull_request` + `workflow_dispatch` as the only triggers.** Do not add +`push: branches: [main]`. Direct pushes to the default branch are gated by +branch protection requiring this CI to pass on the PR first, which is the +intended flow anyway. The example caller reflects this. + +## What is deliberately not here + +- **No CodeQL, no SARIF upload, no dependency review, no native secret + scanning.** These are paid on private repos and a personal account has not + bought them. The free substitutes (gitleaks, zizmor-no-SARIF, semgrep) are + what this tier runs. +- **No Artifact Attestations on release.** They require GitHub Enterprise Cloud + on private repos; use `release-supply-chain-free.yml` (SBOM + checksums, no + provenance). +- **No cross-account runner sharing.** An org runner does not serve a + personal-account repo; do not attempt to relax this with settings, it is a + platform gate. diff --git a/examples/personal/security-selfhosted.yml b/examples/personal/security-selfhosted.yml new file mode 100644 index 0000000..fd426c0 --- /dev/null +++ b/examples/personal/security-selfhosted.yml @@ -0,0 +1,51 @@ +# Personal-account tier — PRIVATE repository variant. +# +# A repository owned by a *personal* GitHub account (e.g. `rldyourmnd/*`) takes +# the **private-free** posture regardless of visibility: even when private, it +# has no GitHub Advanced Security, no native secret scanning, no CodeQL, no +# dependency review — exactly the same exclusions as [02 private-free]. The +# difference from `examples/private-free/security.yml` is only the **runner**: +# a personal account has a 2 000 min/month Actions quota with no org pool to +# draw on, so every job is routed to a self-hosted runner to keep the meter at +# zero. See [docs/18-personal-account-tier.md]. +# +# Do NOT copy this file into a public repository. A forked pull request against +# a public repo executes attacker-controlled code, so pointing a public repo at +# a self-hosted runner is a remote-code-execution path into your own hardware. +# If the repository is or may become public, use `examples/private-free/security.yml` +# (GitHub-hosted) or the public-OSS suite instead. +# +# Replace @ with a pinned full commit SHA of ci-workflows, and