From acd66a83f0a8711e0230c67d7ac4cac040fd06a3 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 7 Sep 2026 15:57:10 +0500 Subject: [PATCH 1/3] fix(catalog): stop assuming publisher bought paid GitHub security products Treat Enterprise Cloud, Code Security, Secret Protection and Code Quality as explicit consumer opt-in. Keep free public CodeQL, SARIF, Scorecard and attestations. Private repos without those purchases use the private-free programme. Paid profile ids stay stable. Signed-off-by: rldyourmnd --- .agents/skills/ci-consumer-adoption/SKILL.md | 76 ++-- .../.generated-from-agents-skills | 2 +- .claude/skills/ci-consumer-adoption/SKILL.md | 76 ++-- .github/workflows/nddev-security-bundle.yml | 9 +- AGENTS.md | 2 +- CHANGELOG.md | 5 + README.md | 18 +- SUPPORT.md | 2 +- catalog/capabilities.yml | 2 +- catalog/profiles.yml | 41 +- catalog/python-execution.yml | 8 + docs/00-overview.md | 29 +- docs/01-public-oss-free.md | 6 +- docs/05-runners.md | 33 +- docs/14-ai-agentic-workflows.md | 10 +- docs/16-code-quality.md | 19 +- docs/17-nddev-tier.md | 370 +++++++----------- docs/18-personal-account-tier.md | 10 +- docs/adr/0002-operating-modes-are-compiled.md | 8 +- docs/adr/0004-callers-choose-their-runner.md | 4 + docs/generated/capability-matrix.md | 2 +- examples/README.md | 6 +- examples/nddev/os-capability-routing.yml | 3 +- examples/nddev/runner-routing.yaml | 7 +- .../nddev/security-private-selfhosted.yml | 62 +-- examples/nddev/security.yml | 10 +- .../private-paid-ghas/security-selfhosted.yml | 23 ++ scripts/check_documented_commands.py | 4 +- scripts/check_examples.py | 1 + scripts/check_public_docs.py | 41 +- scripts/resolve_profile.py | 121 +++++- 31 files changed, 549 insertions(+), 461 deletions(-) create mode 100644 examples/private-paid-ghas/security-selfhosted.yml diff --git a/.agents/skills/ci-consumer-adoption/SKILL.md b/.agents/skills/ci-consumer-adoption/SKILL.md index c519fda..e1ed5b1 100644 --- a/.agents/skills/ci-consumer-adoption/SKILL.md +++ b/.agents/skills/ci-consumer-adoption/SKILL.md @@ -4,16 +4,16 @@ description: Wire a repository onto the ci-workflows reusable library correctly license: AGPL-3.0-or-later compatibility: Codex and Agent Skills compatible; OpenCode discovers .agents/skills. Generate .claude/skills mirrors for Claude Code. metadata: - version: 1.1.0 + version: 1.2.0 owner: NDDev status: proposed - reviewed_at: '2026-08-26' + reviewed_at: '2026-09-07' --- # Adopting ci-workflows in a consumer repository -This is the *caller* side. For work inside the library itself use -`nddev-repo-flow`. +This is the *caller* side. For work inside the library itself use `AGENTS.md` +and the repository validators. There is no `nddev-repo-flow` skill. Adoption is four decisions, in order. Getting them out of order is what produces the two failure shapes seen in practice: a repository that looks configured but @@ -27,10 +27,10 @@ The resolver lives in the library, not in your repository, so check the library out first. Everything below runs in that checkout, not in yours: ```bash -# Pin to the ref you intend to consume. `resolve_profile.py` does not exist in -# Use the current released surface. At the 2026-08-26 review this is 0.1.11 at -# 409817cf743e76383c84e30c72edf781d73b71a1; re-read the latest immutable -# release before adoption rather than copying this review-time value. +# Pin to the ref you intend to consume. Re-read the latest immutable release +# before adoption rather than copying a review-time value. At the 2026-09-07 +# review the current release is 0.1.16 at +# 377e5311509e3e162d7bf377c1faf57e6b0e622a and ships resolve_profile.py. LIBRARY_REF=main git clone --depth 1 --branch "$LIBRARY_REF" \ https://github.com/NDDev-OpenNetwork/ci-workflows.git /tmp/ci-workflows @@ -41,8 +41,11 @@ cd /tmp/ci-workflows python3.13 -I -B -m venv --copies .venv uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt -.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility private --plan enterprise-cloud \ - --code-security --secret-protection --code-quality +# Default: no paid add-ons. Public Free/Team keep CodeQL and attestations. +.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan free +# Explicit paid opt-in only when those products are independently held: +# --visibility private --plan enterprise-cloud \ +# --code-security --secret-protection --code-quality ``` It returns the matching profile, its controls (CodeQL mode, runner class, @@ -59,17 +62,18 @@ Pick the tier doc first; it decides which reusables are even legal to call: | --- | --- | | public repository | `docs/01-public-oss-free.md` | | private, no paid security products | `docs/02-private-free.md` | -| private, Advanced Security held | `docs/03-private-paid-ghas.md` | -| an estate that already owns the paid products | `docs/17-nddev-tier.md` | +| private, Advanced Security **selected** | `docs/03-private-paid-ghas.md` | +| opt-in paid organization programmes | `docs/17-nddev-tier.md` | | Code Quality (orthogonal to all of the above) | `docs/16-code-quality.md` | -The trap: the generic model treats *private* as the degraded case, so a private -repository inside an estate that already pays for the paid products gets -configured down to the free tier and quietly discards capability that is already -bought — most visibly by releasing through `release-supply-chain-free.yml` when -`release-supply-chain.yml` would attest. Check entitlements before believing a -tier table. Prices and quotas live in `catalog/product-facts.yml`; never quote -them from memory or from a skill. +The publisher is a GitHub Organization, not an Enterprise account, and this +library does not assume it purchased Code Security, Secret Protection, Code +Quality, or Enterprise Cloud. Do not copy the paid examples into a private +repository that has not bought those products. The inverse trap also exists: +an organization that **did** buy them and then follows private-free will +discard attested releases. Check entitlements before believing a tier table. +Prices and quotas live in `catalog/product-facts.yml`; never quote them from +memory or from a skill. ## 2. Pin — to a released tag, by full SHA @@ -102,33 +106,25 @@ everywhere": - **private → self-hosted label**, passed by the caller through the `runner` input. -For NDDev private repositories, route by capability rather than by whichever -queue looks shortest: +Private callers supply their own labels. Example class names used by some +NDDev private repositories (`nddev-linux-fast` / `-standard` / `-integration` +/ `-untrusted` / `-release`) are caller-owned; this library does not publish a +live fleet inventory. Do not put a private checkout on a checkout-free class, +Docker work on a class without a container runtime, or untrusted code on a +credentialed class. -| Workload | Runner label | -| --- | --- | -| checkout-free lint or policy | `nddev-linux-fast` | -| ordinary private build/test | `nddev-linux-standard` | -| Docker, service containers, nested runtime | `nddev-linux-integration` | -| untrusted candidate code | `nddev-linux-untrusted` | -| credentialed release/deploy | `nddev-linux-release` | -| Almaty ordinary/integration work | `nddev-priority-standard` / `nddev-priority-integration` | - -Do not put a private checkout on `fast`, Docker work on `standard`, untrusted -code on a credentialed class, or ordinary repositories on the Almaty priority -classes. - -Then close the two settings that **no workflow file can reach**, because GitHub -schedules them itself: +Then close the two settings that **no workflow file can reach**, and only when +those products are actually enabled, because GitHub schedules them itself: | Scan | Where the runner is chosen | | --- | --- | | CodeQL *default setup* | `PATCH /repos/{owner}/{repo}/code-scanning/default-setup` with `runner_type: labeled` | | Code Quality | `PATCH /repos/{owner}/{repo}/code-quality/setup` with `runner_type: labeled`, `runner_label` — or repository settings → Code quality → *Labeled runner* | -Miss either and the repository keeps consuming metered minutes while every -caller in the tree claims otherwise. Full mechanics: -`docs/05-runners.md#visibility-routing`. +Miss either **on a repository that has those products enabled** and it keeps +consuming metered minutes while every caller in the tree claims otherwise. Do +not enable or route them on a repository that has not purchased them. Full +mechanics: `docs/05-runners.md#visibility-routing`. There is **no** automatic spillover from a self-hosted label to a hosted runner. A job whose label is busy queues until a runner frees. Size the fleet so @@ -176,7 +172,7 @@ reconciles placement; workflow-level retries must not duplicate an active job. 2. Every reusable reference pinned by full SHA to a released tag; one pin per repository. 3. No reference to the pre-rename library name. 4. `runner` input set on private callers; absent on public ones. -5. CodeQL default setup and Code Quality both routed for private repositories. +5. Managed CodeQL default setup and Code Quality routed **only** when those products are enabled. 6. A completed run inspected for `runner_name`, not just a saved setting. 7. AI findings off unless deliberately sized. 8. Release caller matches entitlement — attested where the plan allows it. diff --git a/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills b/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills index 49ce2ba..3a93770 100644 --- a/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills +++ b/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills @@ -1,2 +1,2 @@ source=.agents/skills/ci-consumer-adoption/SKILL.md -sha256=8416f90ba4167712da72b55a6f448660eeeb76ddcfb5cb73c94d3255caae1eb1 +sha256=cd63484a6e9bb0f28d4151e6a95f9b645f28cc776c28c6a588a5dbba932f7a20 diff --git a/.claude/skills/ci-consumer-adoption/SKILL.md b/.claude/skills/ci-consumer-adoption/SKILL.md index c519fda..e1ed5b1 100644 --- a/.claude/skills/ci-consumer-adoption/SKILL.md +++ b/.claude/skills/ci-consumer-adoption/SKILL.md @@ -4,16 +4,16 @@ description: Wire a repository onto the ci-workflows reusable library correctly license: AGPL-3.0-or-later compatibility: Codex and Agent Skills compatible; OpenCode discovers .agents/skills. Generate .claude/skills mirrors for Claude Code. metadata: - version: 1.1.0 + version: 1.2.0 owner: NDDev status: proposed - reviewed_at: '2026-08-26' + reviewed_at: '2026-09-07' --- # Adopting ci-workflows in a consumer repository -This is the *caller* side. For work inside the library itself use -`nddev-repo-flow`. +This is the *caller* side. For work inside the library itself use `AGENTS.md` +and the repository validators. There is no `nddev-repo-flow` skill. Adoption is four decisions, in order. Getting them out of order is what produces the two failure shapes seen in practice: a repository that looks configured but @@ -27,10 +27,10 @@ The resolver lives in the library, not in your repository, so check the library out first. Everything below runs in that checkout, not in yours: ```bash -# Pin to the ref you intend to consume. `resolve_profile.py` does not exist in -# Use the current released surface. At the 2026-08-26 review this is 0.1.11 at -# 409817cf743e76383c84e30c72edf781d73b71a1; re-read the latest immutable -# release before adoption rather than copying this review-time value. +# Pin to the ref you intend to consume. Re-read the latest immutable release +# before adoption rather than copying a review-time value. At the 2026-09-07 +# review the current release is 0.1.16 at +# 377e5311509e3e162d7bf377c1faf57e6b0e622a and ships resolve_profile.py. LIBRARY_REF=main git clone --depth 1 --branch "$LIBRARY_REF" \ https://github.com/NDDev-OpenNetwork/ci-workflows.git /tmp/ci-workflows @@ -41,8 +41,11 @@ cd /tmp/ci-workflows python3.13 -I -B -m venv --copies .venv uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt -.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility private --plan enterprise-cloud \ - --code-security --secret-protection --code-quality +# Default: no paid add-ons. Public Free/Team keep CodeQL and attestations. +.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan free +# Explicit paid opt-in only when those products are independently held: +# --visibility private --plan enterprise-cloud \ +# --code-security --secret-protection --code-quality ``` It returns the matching profile, its controls (CodeQL mode, runner class, @@ -59,17 +62,18 @@ Pick the tier doc first; it decides which reusables are even legal to call: | --- | --- | | public repository | `docs/01-public-oss-free.md` | | private, no paid security products | `docs/02-private-free.md` | -| private, Advanced Security held | `docs/03-private-paid-ghas.md` | -| an estate that already owns the paid products | `docs/17-nddev-tier.md` | +| private, Advanced Security **selected** | `docs/03-private-paid-ghas.md` | +| opt-in paid organization programmes | `docs/17-nddev-tier.md` | | Code Quality (orthogonal to all of the above) | `docs/16-code-quality.md` | -The trap: the generic model treats *private* as the degraded case, so a private -repository inside an estate that already pays for the paid products gets -configured down to the free tier and quietly discards capability that is already -bought — most visibly by releasing through `release-supply-chain-free.yml` when -`release-supply-chain.yml` would attest. Check entitlements before believing a -tier table. Prices and quotas live in `catalog/product-facts.yml`; never quote -them from memory or from a skill. +The publisher is a GitHub Organization, not an Enterprise account, and this +library does not assume it purchased Code Security, Secret Protection, Code +Quality, or Enterprise Cloud. Do not copy the paid examples into a private +repository that has not bought those products. The inverse trap also exists: +an organization that **did** buy them and then follows private-free will +discard attested releases. Check entitlements before believing a tier table. +Prices and quotas live in `catalog/product-facts.yml`; never quote them from +memory or from a skill. ## 2. Pin — to a released tag, by full SHA @@ -102,33 +106,25 @@ everywhere": - **private → self-hosted label**, passed by the caller through the `runner` input. -For NDDev private repositories, route by capability rather than by whichever -queue looks shortest: +Private callers supply their own labels. Example class names used by some +NDDev private repositories (`nddev-linux-fast` / `-standard` / `-integration` +/ `-untrusted` / `-release`) are caller-owned; this library does not publish a +live fleet inventory. Do not put a private checkout on a checkout-free class, +Docker work on a class without a container runtime, or untrusted code on a +credentialed class. -| Workload | Runner label | -| --- | --- | -| checkout-free lint or policy | `nddev-linux-fast` | -| ordinary private build/test | `nddev-linux-standard` | -| Docker, service containers, nested runtime | `nddev-linux-integration` | -| untrusted candidate code | `nddev-linux-untrusted` | -| credentialed release/deploy | `nddev-linux-release` | -| Almaty ordinary/integration work | `nddev-priority-standard` / `nddev-priority-integration` | - -Do not put a private checkout on `fast`, Docker work on `standard`, untrusted -code on a credentialed class, or ordinary repositories on the Almaty priority -classes. - -Then close the two settings that **no workflow file can reach**, because GitHub -schedules them itself: +Then close the two settings that **no workflow file can reach**, and only when +those products are actually enabled, because GitHub schedules them itself: | Scan | Where the runner is chosen | | --- | --- | | CodeQL *default setup* | `PATCH /repos/{owner}/{repo}/code-scanning/default-setup` with `runner_type: labeled` | | Code Quality | `PATCH /repos/{owner}/{repo}/code-quality/setup` with `runner_type: labeled`, `runner_label` — or repository settings → Code quality → *Labeled runner* | -Miss either and the repository keeps consuming metered minutes while every -caller in the tree claims otherwise. Full mechanics: -`docs/05-runners.md#visibility-routing`. +Miss either **on a repository that has those products enabled** and it keeps +consuming metered minutes while every caller in the tree claims otherwise. Do +not enable or route them on a repository that has not purchased them. Full +mechanics: `docs/05-runners.md#visibility-routing`. There is **no** automatic spillover from a self-hosted label to a hosted runner. A job whose label is busy queues until a runner frees. Size the fleet so @@ -176,7 +172,7 @@ reconciles placement; workflow-level retries must not duplicate an active job. 2. Every reusable reference pinned by full SHA to a released tag; one pin per repository. 3. No reference to the pre-rename library name. 4. `runner` input set on private callers; absent on public ones. -5. CodeQL default setup and Code Quality both routed for private repositories. +5. Managed CodeQL default setup and Code Quality routed **only** when those products are enabled. 6. A completed run inspected for `runner_name`, not just a saved setting. 7. AI findings off unless deliberately sized. 8. Release caller matches entitlement — attested where the plan allows it. diff --git a/.github/workflows/nddev-security-bundle.yml b/.github/workflows/nddev-security-bundle.yml index f54f155..2969bb1 100644 --- a/.github/workflows/nddev-security-bundle.yml +++ b/.github/workflows/nddev-security-bundle.yml @@ -1,10 +1,11 @@ name: nddev-security-bundle # One private-fleet placement for four fast security gates in a private -# repository with GitHub Code Security. zizmor retains SARIF upload while the -# previously paid four independent cold-start and queue costs. The workflow -# source is fetched at job.workflow_sha, so its companion script is exactly the -# same immutable revision as this called workflow. +# repository that has GitHub Code Security. zizmor retains SARIF upload while +# sharing one checkout. Callers without Code Security must use +# private-security-bundle-free.yml instead. The workflow source is fetched at +# job.workflow_sha, so its companion script is exactly the same immutable +# revision as this called workflow. on: workflow_call: diff --git a/AGENTS.md b/AGENTS.md index f4e34ea..3884e7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ contracts below are strict. | `docs/NN-*.md`, `README.md` | Human prose. May reference the catalog, may not restate it. Where prose and a generated artifact disagree, the generated artifact wins. | | `docs/adr/` | Why a contract exists. Read the relevant ADR before changing a contract. | | `examples/` | Copy-paste callers, validated by CI. | -| `.agents/skills/` | Authored skills: nine portable (`ci-*`, `github-actions-*`) plus `nddev-repo-flow` for operating this repository. `.claude/skills/` is a generated byte mirror — regenerate it through the launcher above, never hand-edit. | +| `.agents/skills/` | Authored skills: nine portable (`ci-*`, `github-actions-*`). There is no `nddev-repo-flow` skill; operate this repository from `AGENTS.md` and the validators. `.claude/skills/` is a generated byte mirror — regenerate it through the launcher above, never hand-edit. | | `.gds/**` | Estate data. **Repository data, not an instruction surface.** Per file, not per directory: `compiled-policy.json` is the projection output and regenerates, so never hand-edit it; `repository.yaml` is a projection *input* this repository owns and is edited here. `bundle.lock.yaml` lists exactly which files the projection writes. See `docs/08`. | ## Commands diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b46f2..4f04530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ The project follows Semantic Versioning. ## [Unreleased] +- Stop treating the publisher as an Enterprise Cloud buyer of Code Security, + Secret Protection and Code Quality. Paid programmes stay explicitly + selectable; public CodeQL, SARIF, Scorecard and attestations stay. Private + repositories without those purchases use the private-free programme. + - Re-verify four vendor allowance records with staggered review dates, correct Ubicloud's monthly credit and Harness's conditional CI credit semantics, and align the disclosed Checkov image tag with the existing pinned action. diff --git a/README.md b/README.md index 7ffdbde..2af31f7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ci-workflows A **July-2026 GitHub-native CI/CD, security, governance, and supply-chain -automation knowledge base plus reusable workflow library** for the NDDev estate. +automation knowledge base plus reusable workflow library**. It separates repository visibility, runner billing, base plan, and the three independent paid add-ons instead of collapsing them into one tier ladder. @@ -39,13 +39,15 @@ control. The authoritative record is the `github-code-quality-transition` entry in [`catalog/product-facts.yml`](catalog/product-facts.yml). See [`docs/16-code-quality.md`](docs/16-code-quality.md). -**The NDDev estate is not on the free plan.** The three tiers above describe what -GitHub offers; [`docs/17-nddev-tier.md`](docs/17-nddev-tier.md) records what this -organization has already bought — Enterprise Cloud, Code Security, Secret -Protection, Code Quality — and which generic tier advice therefore does **not** -apply here. Chiefly: private repositories release with attested -`release-supply-chain.yml`, not the `-free` variant, because Artifact -Attestations are gated on Enterprise Cloud and this estate has it. +This library is published from a **GitHub Organization**, not an Enterprise +account, and it does **not** assume the publisher purchased Enterprise Cloud, +Code Security, Secret Protection, or Code Quality. Paid programmes stay +explicitly selectable for other consumers — see +[`docs/17-nddev-tier.md`](docs/17-nddev-tier.md). Public repositories still get +the free GitHub security surfaces (CodeQL, SARIF, dependency review, Scorecard, +attestations). Private repositories without those purchases use the +private-free programme and `release-supply-chain-free.yml`; attested private +releases require Enterprise Cloud and must be opted into. ## Capability → workflow map diff --git a/SUPPORT.md b/SUPPORT.md index 9fc036c..23f103d 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,7 +1,7 @@ # Support `ci-workflows` is a library of reusable GitHub Actions workflows consumed -across the NDDev estate by full commit SHA. Here is where to get help. +by full commit SHA. Here is where to get help. ## Read the docs first diff --git a/catalog/capabilities.yml b/catalog/capabilities.yml index e26520f..7afeee0 100644 --- a/catalog/capabilities.yml +++ b/catalog/capabilities.yml @@ -260,7 +260,7 @@ capabilities: private_free: unavailable private_paid: available workflow: .github/workflows/nddev-security-bundle.yml - example: examples/nddev/security-private-selfhosted.yml + example: examples/private-paid-ghas/security-selfhosted.yml required_permissions: - "actions: read" - "contents: read" diff --git a/catalog/profiles.yml b/catalog/profiles.yml index fee28ae..9e76f0d 100644 --- a/catalog/profiles.yml +++ b/catalog/profiles.yml @@ -3,8 +3,9 @@ # Before this file the tier model existed twice: as three columns per capability # in capabilities.yml (public_oss / private_free / private_paid), and as prose in # docs/16 and docs/17 describing modes the catalog could not express. The prose -# drifted, silently, more than once — the $80 envelope described controls that do -# not work and coverage numbers that had moved. +# drifted, silently, more than once — a fixed-cost envelope described controls +# that do not work and coverage numbers that had moved. Named paid profiles are +# opt-in consumer programmes, not a claim that the publisher holds those products. # # So modes are declared here, validated by scripts/validate_profiles.py, and # rendered into docs/generated/profile-matrix.md. Prose may reference a profile; @@ -257,7 +258,7 @@ profiles: compute promise depends on the declared self-hosted route. - id: public-enterprise-max - name: Public OSS under Enterprise governance + name: Public OSS under Enterprise Cloud with paid add-ons selected doc: docs/17-nddev-tier.md selectors: visibility: [public] @@ -279,13 +280,16 @@ profiles: metered_lines: [ai-credits] product_facts: [github-actions-public-standard, github-code-quality-transition] rationale: >- - Everything public-free provides, plus enterprise rulesets and security - overview. Code Quality applies here, but its public per-committer rate is - disputed between GitHub's own sources, so no cost is compiled for it — - see the fact ledger. + Opt-in programme for a public repository whose organization independently + holds Enterprise Cloud and the three paid add-ons. Public CodeQL, secret + scanning, dependency review and attestations remain free on every plan; + Code Quality is the add-on that still bills here. Its public per-committer + rate is disputed between GitHub's own sources, so no cost is compiled for + it — see the fact ledger. Selecting this profile does not mean the + publisher holds these products. - id: enterprise-full-private-fixed80 - name: NDDev estate — full paid private mode at a fixed $80/month + name: Private Enterprise Cloud with all paid add-ons at an itemised fixed envelope doc: docs/17-nddev-tier.md selectors: visibility: [private, internal] @@ -298,10 +302,9 @@ profiles: codeql_mode: default code_quality_ai: disabled coverage_mode: gated - # The estate fleet gives every job a fresh Incus container, runs it once - # and destroys it — executed workers are never reused. - # That is `ephemeral`, and the distinction is the one ADR 0004 named as - # what would make a public fork safe on self-hosted hardware. + # One-job ephemeral workers: a public fork must never reuse a trusted + # machine. That is the distinction ADR 0004 named; the caller supplies + # the label. This profile does not publish a live fleet inventory. runner_mode: self-hosted-ephemeral governance_mode: solo-agent enforcement: active @@ -320,8 +323,12 @@ profiles: allow_actions_overage: false active_committers: 1 rationale: >- - The four licences are billed once per active committer, so the estate pays - the same whether one repository or fifty are enabled. Runner minutes are - absent because library and managed scans route to the ephemeral fleet; - remaining metered lines are driven to zero by spend guards. The fixed total holds only at - one active committer: a second adds $59 before anyone writes a workflow. + Opt-in programme for a private or internal repository whose organization + independently holds Enterprise Cloud and the three paid add-ons, and + chooses an itemised fixed envelope at one active committer. List prices + are catalog facts, not an invoice. Runner minutes stay off the GitHub + meter only when the caller actually routes to self-hosted compute. + Selecting this profile does not mean the publisher holds these products + or pays this envelope. The fixed total holds only at one active + committer: a second adds the three add-on licences before anyone writes + a workflow. diff --git a/catalog/python-execution.yml b/catalog/python-execution.yml index 4ea16df..5b927ea 100644 --- a/catalog/python-execution.yml +++ b/catalog/python-execution.yml @@ -70,6 +70,10 @@ "role": "adoption-guide", "language": "markdown" }, + "docs/17-nddev-tier.md": { + "role": "adoption-guide", + "language": "markdown" + }, "docs/19-python-execution.md": { "role": "repository-launcher", "language": "markdown" @@ -78,6 +82,10 @@ "role": "adoption-guide", "language": "markdown" }, + "examples/nddev/security-private-selfhosted.yml": { + "role": "consumer-command", + "language": "yaml" + }, "examples/private-free/security-selfhosted.yml": { "role": "consumer-command", "language": "yaml" diff --git a/docs/00-overview.md b/docs/00-overview.md index 46c0273..22d57c4 100644 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -1,11 +1,10 @@ # ci-workflows — Overview `ci-workflows` is a **reusable GitHub Actions library plus a GitHub-native -CI/CD, security, governance, and supply-chain knowledge base** for the NDDev -estate, current for July 2026. Consumers reference the reusable workflows by -full commit SHA; the docs in this folder explain the platform behavior those -workflows depend on so you can compose them correctly for your repository's -billing reality. +CI/CD, security, governance, and supply-chain knowledge base**, current for +July 2026. Consumers reference the reusable workflows by full commit SHA; the +docs in this folder explain the platform behavior those workflows depend on so +you can compose them correctly for your repository's billing reality. The library is intentionally opinionated: @@ -53,15 +52,17 @@ the three tiers as an opt-in fourth one — see [16 Code Quality tier](16-code-quality.md). Both free tiers exclude it, and the free maintainability substitutes are listed there. -### What this estate actually owns +### Publisher posture versus opt-in paid programmes -The three tiers describe what GitHub *offers*. [17 NDDev estate -tier](17-nddev-tier.md) records what the NDDev-OpenNetwork organization has **already -bought** — Enterprise Cloud, Code Security, Secret Protection, and Code Quality — -so repositories here stop being configured as if they were on the free plan. Its -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. +The three tiers describe what GitHub *offers*. This library is published from a +GitHub Organization, not an Enterprise account, and it does **not** assume the +publisher purchased Enterprise Cloud, Code Security, Secret Protection, or Code +Quality. [17 Paid organization programmes](17-nddev-tier.md) is the opt-in +guide for consumers who independently hold those products. Public repositories +still get free CodeQL, SARIF, dependency review, Scorecard and attestations. +Private repositories without those purchases use the private-free programme and +`release-supply-chain-free.yml`; attested private releases require Enterprise +Cloud. ### Personal-account repositories @@ -133,7 +134,7 @@ 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 Paid organization programmes](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) · diff --git a/docs/01-public-oss-free.md b/docs/01-public-oss-free.md index 083362d..474fb26 100644 --- a/docs/01-public-oss-free.md +++ b/docs/01-public-oss-free.md @@ -1,9 +1,9 @@ # Public OSS tier — the free full suite Public repositories on GitHub get the **entire security and supply-chain suite -for free**, including features that are paid on private repositories. This is the -tier the NDDev estate targets for open-source work, and it is where the library -delivers its full value. +for free**, including features that are paid on private repositories. This is +the programme this public library itself runs, and it is where the library +delivers its full value without paid add-ons. > **One documented exception: GitHub Code Quality is not free on public repos.** > Unlike CodeQL and everything else in the table below, it is billed per active diff --git a/docs/05-runners.md b/docs/05-runners.md index 3681895..5db6aff 100644 --- a/docs/05-runners.md +++ b/docs/05-runners.md @@ -149,10 +149,11 @@ every local reusable call that exposes a runner selector. A validator resolves those callees and rejects an omitted, expression-based, or self-hosted value, so a private-consumer default cannot silently reroute this public repository. -Defence in depth: the estate's runner group sets +Defence in depth: a private runner group should set `allows_public_repositories: false`, so even a mistaken `runs-on` in a public -repository cannot reach the fleet — the job stays queued instead of executing. -Treat that as the backstop, not the control. +repository cannot reach that fleet — the job stays queued instead of executing. +Treat that as the backstop, not the control. This library does not publish a +live fleet inventory. ### Two independent runner settings @@ -310,21 +311,17 @@ Reusables here default `runner` to `ubuntu-latest`. A default is a property of silently adopts whatever the next pin says. Name it anyway — and if you run your own fleet you must, because a hosted default will quietly meter you. -That the default is safe today is recent. It was `amsterdam`, a private -self-hosted label, until August 2026. Amsterdam is now a bastion/application -host and not an Actions execution target. Current private Linux execution uses -the per-class Drakkars labels -`nddev-linux-fast`/`-standard`/`-integration`, backed by one-job ephemeral Incus -containers under public fleet contract v2. Both ways a stale private default -still bite: - -- **Outside this estate** the label does not resolve, so the job queues - forever against a runner that will never appear. -- **Inside an estate, on a public repository**, `pull_request` executes - untrusted fork code — and a private default puts it on trusted private - infrastructure. GitHub's own guidance is blunt about this: "forks of your - public repository can potentially run dangerous code on your self-hosted - runner machine." +That the default is safe today is recent. It was a private self-hosted label +until August 2026. Callers that still need self-hosted Linux pass their own +labels; this library does not publish live hosts. Both ways a stale private +default still bite: + +- **On a caller that does not own that label** the job queues forever against a + runner that will never appear. +- **On a public repository**, `pull_request` executes untrusted fork code — + and a private default puts it on trusted private infrastructure. GitHub's + own guidance is blunt about this: "forks of your public repository can + potentially run dangerous code on your self-hosted runner machine." So every caller states its runner, even when the pinned default already looks right. `scripts/check_examples.py` enforces this for every example outside diff --git a/docs/14-ai-agentic-workflows.md b/docs/14-ai-agentic-workflows.md index 9424205..76a0d9e 100644 --- a/docs/14-ai-agentic-workflows.md +++ b/docs/14-ai-agentic-workflows.md @@ -18,11 +18,11 @@ public preview, they are designed defensively: egress. - **Threat detection** — prompt-injection and abuse detection on inputs. -**Recommended use in this estate:** triage, summarization, docs-gap detection, -and read-only audits only. Do not grant an agentic workflow write permissions or -let it act on untrusted input from forked PRs. Treat its output as a suggestion -that a human or a trusted, least-privilege step reviews — the same trust boundary -as [pull_request_target](security/pull-request-target.md). +**Recommended use:** triage, summarization, docs-gap detection, and read-only +audits only. Do not grant an agentic workflow write permissions or let it act +on untrusted input from forked PRs. Treat its output as a suggestion that a +human or a trusted, least-privilege step reviews — the same trust boundary as +[pull_request_target](security/pull-request-target.md). ## Copilot Autofix diff --git a/docs/16-code-quality.md b/docs/16-code-quality.md index 27f196b..148c077 100644 --- a/docs/16-code-quality.md +++ b/docs/16-code-quality.md @@ -99,8 +99,10 @@ javascript-typescript, python, ruby) and `ai_findings_option` (`disabled` / The UI path below is still the fastest way to do it by hand. -1. **Enterprise** — an enterprise owner must allow Code Quality at the - enterprise level, or the org setting has no effect. +1. **Enterprise, when the organization belongs to one** — GitHub's enablement + docs currently list an enterprise-owner allow step. A standalone + organization is not an Enterprise account and does not have that control; + resolve enablement from the live plan and the org Code quality settings. 2. **Organization** — Settings → Security → **Code quality** → **Repository access**. This dropdown *is* the tier boundary: - `No repositories` — the whole org stays in the free tiers. @@ -115,13 +117,12 @@ The UI path below is still the fastest way to do it by hand. Prefer `Selected repositories` + `Enforce access`: `All repositories` silently pulls every public repo into a paid product. -> The NDDev estate runs `All repositories` + `Enforce access` on purpose. That is -> not a contradiction of the advice above — it follows from the same arithmetic: -> the licence bills once per active committer, so at one committer the fiftieth -> repository costs exactly what the first one did. Apply the cautious default -> whenever the committer count is greater than one, or when "every public repo" -> would mean repos you do not control. See -> [17 NDDev tier](17-nddev-tier.md#cost-envelope). +> `All repositories` + `Enforce access` is a deliberate choice only when the +> organization has actually purchased Code Quality and wants every repository +> on that licence. This library does not assume the publisher did that. Apply +> the cautious default (`Selected repositories` or `No repositories`) unless +> entitlements are verified. See +> [17 Paid organization programmes](17-nddev-tier.md). ## AI findings are a second, separately metered product diff --git a/docs/17-nddev-tier.md b/docs/17-nddev-tier.md index 95f2908..79ffb84 100644 --- a/docs/17-nddev-tier.md +++ b/docs/17-nddev-tier.md @@ -1,233 +1,153 @@ -# Enterprise tier — what a paid organization adds +# Paid organization programmes — opt-in, not assumed -The other four tier docs describe what GitHub *offers* at a given visibility and -plan. This one describes what an organization on a paid plan adds on top, so that -a repository under one stops being configured as if it were on the free plan. +The other tier docs describe what GitHub *offers* at a given visibility and +plan. This one describes the named programmes for consumers who independently +hold a paid GitHub plan and/or paid add-ons, and how to select them without +changing anyone else's policy. -Licence counts, repository inventory and observed spend are account state, not -library facts. They belong to whoever operates the account, never to this -library. +Licence counts, repository inventory, hosts, credentials, invoices and observed +spend are account state, not library facts. They belong to whoever operates the +account, never to this library. -| Product | Licences | Consequence for this library | -| --- | --- | --- | -| **Enterprise Cloud** | held | Artifact Attestations work on **private** repos | -| **Code Security** | held | CodeQL + SARIF upload legal on private | -| **Secret Protection** | held | Native secret scanning on private (push protection deliberately off) | -| **Code Quality** | held | Maintainability scans + PR gate — see [16](16-code-quality.md) | - -Platform side is already applied: the org security configuration -**`nddev-config`** (`enforcement: enforced`) is attached to every repository in -the organization and is the default for new ones. It enables GHAS, secret scanning, validity -checks, non-provider patterns, Dependabot security updates, and private -vulnerability reporting. - -Two settings are deliberately **not** in it, and both decisions are load-bearing: - -- **`secret_scanning_push_protection: disabled`.** Push protection is the only - control that stops a secret reaching the remote at all; with it off, detection - is after the fact and the remedy is rotation, not prevention. It was turned off - as an explicit velocity trade-off. Treat a secret-scanning alert here as an - already-leaked credential. -- **`code_scanning_default_setup: not_set`.** Config attachment is atomic per - repository: forcing default setup where an active CodeQL *advanced* setup - exists fails the **whole** attachment, taking secret scanning down with it — - observed when the stock `GitHub recommended` configuration attached to the - public repositories without an advanced setup and failed on every repository - already running `codeql.yml`. - Code scanning is therefore enabled per repository instead. - - **Coverage is tracked in the control plane, not here.** Repositories fall into - three groups: default setup, their own CodeQL workflow, and an empty language - list. The last group is - not a gap — they are submodule parents holding only Shell and Dockerfile, so - CodeQL has nothing to analyse. Note that such a record still *reads* as "code - scanning on" while scanning nothing, so count languages, not state. - - Four traps in the default-setup REST API, all learned by hitting them: - - - **The REST enum has no `rust`, but the product does.** `PATCH` accepts only - `actions, c-cpp, csharp, go, java-kotlin, javascript-typescript, python, - ruby, swift`. Yet CodeQL Rust support left public preview and went - **generally available for default setup** in October 2025. Verified - 2026-08-10 on this account: the enum rejects `rust`, and `GET` does not - offer it as available on repositories that do contain Rust. So the gap is this account's API surface - lagging the documented GA, not a product limitation — treat Rust coverage as - **pending and retryable**, not impossible, and re-probe the enum before - reaching for a substitute. - - **`GET` returns *available* languages when `not-configured` and *configured* - languages when `configured`**, and it echoes the legacy aliases `javascript` - and `typescript` which the `PATCH` enum then rejects. Filter the read-back - through the accepted set before writing it again. - - **The setup run is atomic**: one language that needs a build (`swift`, - `java-kotlin`, `ruby`) fails the run and GitHub silently reverts the whole - configuration. The `PATCH` still returned a `run_id`, so success has to be - read from the run's per-job conclusions, not the response. - - **Omitting `runner_type` resets it to `standard`.** A `PATCH` that only - changes languages will move a private repository off the fleet and onto - metered GitHub-hosted runners without saying so. Always send - `runner_type`/`runner_label` with every write. - -## The correction this tier exists to make - -[02 Private free](02-private-free.md) tells a private repository to release with -`release-supply-chain-free.yml`, because Artifact Attestations require GitHub -Enterprise Cloud on private and internal repos. - -**That gate does not apply here — this estate has Enterprise Cloud.** Every one -of the 26 private repositories can use the attested `release-supply-chain.yml` -and get SLSA build provenance. Using the `-free` variant here throws away -provenance that is already paid for. - -The same inversion applies across the board: in the generic model a private repo -is the *degraded* case. In this estate it is not. Private and public repositories -run the **same** callers — the difference is only who pays, and that is settled. - -| Capability | Generic private-free | This estate | +## Publisher posture + +This library is published from a **GitHub Organization**, not from an Enterprise +account. The public generic library: + +- does **not** assume the publisher purchased GitHub Enterprise Cloud, GitHub + Code Security, GitHub Secret Protection, or GitHub Code Quality; +- does **not** treat "organization" as synonymous with "Enterprise"; +- keeps those paid capabilities **explicitly selectable** for other consumers; +- does **not** remove free public CodeQL, SARIF upload, dependency review, + Scorecard, or artifact attestations — those are free on public repositories on + every current GitHub plan. + +When a function depends on Free versus Team, resolve it from the live plan and +from [`catalog/product-facts.yml`](../catalog/product-facts.yml). Do not copy a +plan name into this document as if it were a durable product fact. + +This repository itself is **public** and runs the +`public-free-standalone` programme: CodeQL, zizmor with SARIF, dependency +review, Scorecard, gitleaks, and attested releases. That is the free public +surface, not a paid add-on. + +## What stays free on public repositories + +Official GitHub documentation, re-read 2026-09-07: + +| Surface | Public repositories | Private / internal | | --- | --- | --- | -| CodeQL + SARIF | excluded (paid) | `public-codeql.yml`, `zizmor-sarif.yml` | -| Native secret scanning | gitleaks substitute | native, plus `secret-scan.yml` for history | -| Push protection | unavailable (paid) | licensed but **off** by choice — see above | -| Dependency review | excluded (paid) | `public-dependency-review.yml` | -| Release provenance | `release-supply-chain-free.yml` | **`release-supply-chain.yml`** (attested) | -| Maintainability | lint/coverage only | Code Quality + the free packs | - -## Callers - -Security suite: [`examples/nddev/security.yml`](../examples/nddev/security.yml). -It is the private-paid/GHAS suite plus `osv-scan.yml`, and it runs unchanged on -public repositories. - -Private repositories take the same suite with every job pinned to the -self-hosted fleet: -[`examples/nddev/security-private-selfhosted.yml`](../examples/nddev/security-private-selfhosted.yml). -The coverage is identical — only the `runner` inputs differ — so a repository -changing visibility switches example, not posture. - -Release with provenance — on **private** repos too: - -```yaml -jobs: - publish: - permissions: - contents: write - id-token: write - attestations: write - artifact-metadata: write - uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/release-supply-chain.yml@ -``` +| CodeQL code scanning, dependency review, native secret scanning | Free on current plans | Paid: Code Security and/or Secret Protection on Team or Enterprise Cloud | +| Artifact attestations | Free on current plans (not legacy Bronze/Silver/Gold) | GitHub Enterprise Cloud | +| GitHub Code Quality | Separate licence; Team or Enterprise; public rate disputed | Same licence; not included in GHAS | + +Sources: + +- [Using artifact attestations to establish provenance for builds](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations) +- [GitHub security features](https://docs.github.com/en/code-security/getting-started/github-security-features) +- [GitHub Code Quality billing](https://docs.github.com/en/billing/concepts/product-billing/github-code-quality) -The manifest records `slsa_build_level: 3` rather than the `null` that -`release-supply-chain-free.yml` writes. See -[07 Supply chain](07-supply-chain-slsa-sbom-attestations.md). - -## What this estate does *not* have - -- **Copilot Autofix.** Copilot Business is provisioned on the organization but - **zero seats are assigned** (`seat_management_setting: unconfigured`), so - Autofix on code scanning and Code Quality findings is unavailable until a seat - is assigned. That is a per-seat paid product, and the Code Quality licence does - not include it. -- **SAML SSO, IP allow list, SSH certificate authorities.** None configured at - org or enterprise level. - -**Enforced SHA pinning is now on.** Both org and enterprise report -`sha_pinning_required: true`, so the platform rejects tag-referenced actions -estate-wide, not just in this library. `scripts/check_pinned_actions.py` remains -the pre-merge gate; the platform setting is the backstop for repositories that -do not run it. - -## Cost note that governs tier choice - -Every paid product here — Code Security, Secret Protection, Code Quality — bills -per **active committer**, counted **once per organization**, not per repository. -With a single active committer the estate pays the same whether one repository or -every repository is enabled. That is why `nddev-config` is attached to all of -them rather than a chosen subset: partial coverage would have cost exactly the -same and protected less. Re-evaluate that reasoning the moment a second -committer joins. - - -## The fixed-cost envelope - -This estate is the `enterprise-full-private-fixed80` profile. The four fixed -lines, its guards and every other mode are declared in `catalog/profiles.yml` -and rendered to -[the generated profile matrix](generated/profile-matrix.md) — read the amounts -there, not here. `scripts/validate_profiles.py` checks that the itemised lines -sum to the declared total and that a fixed-cost profile cannot permit AI credits -or Actions overage, so the two cannot drift apart. - -The licence mix and the amount are declared in `catalog/profiles.yml` and -validated against it; the invoice that confirms them is account state and lives -in the control plane. Read the amounts from the generated matrix, never from -this paragraph. - -Everything else is metered and deliberately driven to zero: - -| Metered line | Control | +## Private repositories without those purchases + +A private repository whose organization has **not** bought the add-ons and is +**not** on Enterprise Cloud uses [02 Private free](02-private-free.md): + +- gitleaks, actionlint, zizmor without SARIF, OSS scanners, checksummed + `release-supply-chain-free.yml`; +- no CodeQL, no SARIF upload, no native secret scanning, no private + attestations. + +That is the honest default. Do not "upgrade" a private caller to attested +`release-supply-chain.yml` unless the plan actually unlocks Artifact +Attestations. + +## Opt-in paid programmes + +Named profiles in [`catalog/profiles.yml`](../catalog/profiles.yml) remain for +consumers who **independently** hold the matching products and select them +explicitly. Profile **ids are stable**; selecting one does not change another +consumer's policy. + +| Profile id | When to select it | | --- | --- | -| Actions minutes | $0 hard-stop budget at **org and enterprise**; private jobs routed to self-hosted | -| Actions **storage** | budgets do **not** block storage — controlled by 1-day artifact/log retention with the org maximum also pinned to 1 | -| **AI credits** | dedicated **AI credits budget**, $0, stop-usage on — see below | -| Codespaces, Packages, Git LFS, Models, Sandbox, Spark | $0 hard-stop budgets at both levels | - -Three facts worth carrying: - -- **A budget on a license-based product cannot stop anything.** The Code Quality - budget is scoped by *license count*, and the edit form says so outright: - "Stop usage when budget limit is reached — **Not available for license-based - products**". The budgets *list* still renders `Stop usage: Yes` for it, exactly - like the metered budgets where the stop does work. Set at 0 licences against 1 - legitimately consumed, it also reads `Over budget` permanently, so its alert - carries no signal. -- **AI credits have their own budget type, and that one works.** The "New budget" - flow offers **"AI credits budget — set a budget for all SKUs that consume AI - credits"** alongside product- and SKU-level. It is metered, so stop-usage - applies. Created at enterprise scope at **$0 with stop-usage**, it caps every - AI-credit source at once — which matters, because `ai_findings_option: - disabled` on every repository did **not** stop the line: credits still - accrued under product *Code Quality*, and Copilot Autofix - ("suggest fixes for CodeQL alerts using AI") remains `On` at repository level. - Do not treat the per-repository AI toggle as the control. - Budgets are not retroactive: "usage before budget creation isn't counted in the - current billing cycle". -- **Seat count is the one thing no budget bounds.** A second active committer - adds two more Advanced Security licences before anyone writes a workflow — the - single largest step change available to this envelope. Org membership, - outside-collaborator invitation, and enterprise member purchasing are all - closed for that reason. - - -## Runner routing in this estate - -Private repositories run on the estate's self-hosted fleet; public repositories -stay on GitHub-hosted runners, where minutes are free and self-hosted would be a -security defect. Mechanics, the two settings that a workflow file cannot reach, -and the reason this public repository never routes itself to self-hosted: -[05 Runners → Routing by visibility](05-runners.md#visibility-routing). - -The invariant is that every private repository with a configured default setup -runs on `runner_type: labeled` and every public one on `standard`; the current -per-repository tally is control-plane state. Re-verify after any change to -code-scanning setup — omitting `runner_type` from a `PATCH` silently resets it -to `standard`, which moves private scanning onto metered runners. - - -### The included-minutes pool is the live risk to this envelope - -Routing private work to the fleet is what keeps Actions at zero, and the routing -is only as good as its last write. The included pool has been observed emptying -well before month end when routing slips. - -What happens then is not an overspend — the Actions budget is $0 with -stop-usage, so **Actions halt**. The envelope holds and CI stops. That is the -intended trade, but it is worth stating plainly, because "the bill stayed inside -the envelope" and "CI ran all month" are not the same claim. Read the current -meter in the control plane; a figure copied into this document is stale the day -after it is written. - -Anything that adds a scheduled or per-PR job to a **private** repository draws on -this pool. Check the meter before adding one. +| `public-enterprise-max` | Public repository on Enterprise Cloud with the three add-ons selected | +| `enterprise-full-private-fixed80` | Private/internal on Enterprise Cloud with all three add-ons and an itemised fixed envelope | + +Buying one add-on never enables another. Code Security does not include Secret +Protection; neither includes Code Quality; none of them unlock private +attestations — that is a plan gate. + +Resolve rather than guess: + +```bash +.venv/bin/python -I -B scripts/check_python_execution_contract.py \ + --launch resolve_profile.py -- --visibility public --plan free +.venv/bin/python -I -B scripts/check_python_execution_contract.py \ + --launch resolve_profile.py -- --visibility private --plan team \ + --code-security --secret-protection --code-quality +``` + +The first shape is the publisher-compatible default. The second is an explicit +paid opt-in. Amounts, guards and the programme split are rendered to +[the generated profile matrix](generated/profile-matrix.md). + +Callers: + +- Public free suite: [`examples/public-oss/security.yml`](../examples/public-oss/security.yml) + and [`examples/nddev/security.yml`](../examples/nddev/security.yml) (same + surfaces; the nddev copy is the publisher-org public caller). +- Private without add-ons: [`examples/private-free/`](../examples/private-free/) + and [`examples/nddev/security-private-selfhosted.yml`](../examples/nddev/security-private-selfhosted.yml). +- Private with paid add-ons **selected**: [`examples/private-paid-ghas/`](../examples/private-paid-ghas/). +- The SARIF private bundle `nddev-security-bundle.yml` is for callers that + **have** Code Security. Callers without it use + `private-security-bundle-free.yml`. + +## GitHub platform traps that are not purchase claims + +These are GitHub product behaviours. They are not a record of one account's +current configuration. + +- **Security-configuration attachment is atomic per repository.** Forcing + CodeQL *default* setup onto a repository that already has an *advanced* setup + fails the whole attachment, taking secret scanning down with it. Enable code + scanning per repository when advanced setup is already in use, or choose + "Enabled with advanced setup allowed" in a configuration that must cover both. +- **`secret_scanning_push_protection` is a velocity trade-off.** With it off, + detection is after the fact and the remedy is rotation. Treat an alert in + that mode as an already-leaked credential. +- **CodeQL default-setup REST traps**, learned from the product rather than + from copying live account state: + - The REST enum has no `rust`, while CodeQL Rust default setup is documented + GA. Treat Rust coverage as pending and retryable until the enum catches up. + - `GET` returns *available* languages when `not-configured` and *configured* + languages when `configured`, and it echoes legacy aliases `javascript` / + `typescript` which `PATCH` then rejects. Filter the read-back through the + accepted set before writing it again. + - The setup run is atomic: one language that needs a build can fail the run + and GitHub silently reverts the whole configuration. Success is the run's + per-job conclusions, not the `PATCH` `run_id`. + - Omitting `runner_type` resets it to `standard`. A `PATCH` that only changes + languages will move a private repository onto metered GitHub-hosted runners + without saying so. Always send `runner_type` / `runner_label` with every + write. + +Managed CodeQL default setup and Code Quality scans have their own runner +settings that no workflow file can reach. Route them only when those products +are actually enabled. See [05 Runners](05-runners.md#visibility-routing). + +## Cost shape, not an invoice + +Every paid product here — Code Security, Secret Protection, Code Quality — +bills per **active committer**, counted **once per organization**, not per +repository. A fixed-cost profile that permits AI credits or Actions overage is +not fixed; `scripts/validate_profiles.py` encodes that. A budget on a +licence-based product cannot stop the licence. AI credits are a separate +metered pool. + +Do not copy an invoice, a seat count, or a live meter into this file. --- -Last verified: 2026-08-10 +Rewritten 2026-09-07 to drop stale publisher-purchase claims. Dated receipts in +[`docs/audit/`](audit/review-reconciliation-2026-07-04.md) stay historical and +are not rewritten. diff --git a/docs/18-personal-account-tier.md b/docs/18-personal-account-tier.md index 4d0cd05..76a97ba 100644 --- a/docs/18-personal-account-tier.md +++ b/docs/18-personal-account-tier.md @@ -9,11 +9,11 @@ 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-OpenNetwork` -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: +Paid GitHub add-ons and Enterprise Cloud attach to an **organization** (or +enterprise), not to a person's namespace. [17 Paid organization +programmes](17-nddev-tier.md) is the opt-in guide for an organization that +independently holds those products. **A repository owned by a personal account +has none of that**, even if the same human owns both. So: - a private personal-account repo has **no GHAS** → no CodeQL, no native secret scanning, no dependency review; diff --git a/docs/adr/0002-operating-modes-are-compiled.md b/docs/adr/0002-operating-modes-are-compiled.md index 3192373..0510bfc 100644 --- a/docs/adr/0002-operating-modes-are-compiled.md +++ b/docs/adr/0002-operating-modes-are-compiled.md @@ -13,8 +13,8 @@ That model cannot express the estate it serves. Visibility, base plan and the three paid add-ons (Code Security, Secret Protection, Code Quality) vary independently, and most repositories sit in a combination the three tiers cannot name — a private repository holding only Code Quality is neither "private-free" -nor "private-paid". Two further modes existed in prose only: the fixed-cost -NDDev tier and the personal-account tier. +nor "private-paid". Two further modes existed in prose only: a fixed-cost paid +organization programme and the personal-account tier. So a mode could be stated only in a document, and a document is exactly what nothing validates. The $80 envelope drifted in three separate ways before this @@ -31,7 +31,9 @@ was noticed. unplaceable. - Named profiles compose those axes. Four exist: `public-free-standalone`, `private-free-max`, `public-enterprise-max`, - `enterprise-full-private-fixed80`. + `enterprise-full-private-fixed80`. The two paid ids are opt-in consumer + programmes, not a claim that the publisher holds Enterprise Cloud or the + paid add-ons. - `scripts/validate_profiles.py` enforces coherence, and its rules encode failures that actually happened rather than generic schema checks: itemised fixed cost lines must sum to the declared total; a fixed-cost profile may not diff --git a/docs/adr/0004-callers-choose-their-runner.md b/docs/adr/0004-callers-choose-their-runner.md index 14db1c0..2f53ce0 100644 --- a/docs/adr/0004-callers-choose-their-runner.md +++ b/docs/adr/0004-callers-choose-their-runner.md @@ -70,6 +70,10 @@ Dependabot bump would have flipped it. the two conditions above. It does not make this library's default safe, which is a separate question about resolvability, but it removes the reason a public estate repository could not use the fleet at all. + + **Update 2026-09-07:** `examples/nddev/` remains a caller-owned map, not a live + fleet inventory and not a claim the publisher holds Enterprise Cloud or paid + GitHub security products. The default-must-be-hosted rule is unchanged. - The same trap exists in the platform API: a `PATCH` to code-scanning default setup that omits `runner_type` resets it to `standard`, silently moving a private repository off the fleet. Send the field on every write. diff --git a/docs/generated/capability-matrix.md b/docs/generated/capability-matrix.md index 6be6aea..c087974 100644 --- a/docs/generated/capability-matrix.md +++ b/docs/generated/capability-matrix.md @@ -64,7 +64,7 @@ | hadolint Dockerfile lint (`hadolint-dockerfile`) | security-scanning | ga | free | free | available | `.github/workflows/hadolint-ci.yml` | `examples/security/hadolint.yml` | | IaC scan (Checkov) (`iac-scan-checkov`) | security-scanning | ga | free | free | available | `.github/workflows/iac-scan.yml` | `examples/security/iac-scan.yml` | | GitHub native secret scanning (`native-secret-scanning`) | security-scanning | ga | free | unavailable | available | `-` | `-` | -| Consolidated private security bundle (`nddev-security-bundle`) | security-scanning | preview | unavailable | unavailable | available | `.github/workflows/nddev-security-bundle.yml` | `examples/nddev/security-private-selfhosted.yml` | +| Consolidated private security bundle (`nddev-security-bundle`) | security-scanning | preview | unavailable | unavailable | available | `.github/workflows/nddev-security-bundle.yml` | `examples/private-paid-ghas/security-selfhosted.yml` | | OpenSSF Scorecard SARIF (`ossf-scorecard`) | security-scanning | ga | free | unavailable | unavailable | `.github/workflows/public-scorecard.yml` | `examples/public-oss/scorecard-sarif.yml` | | OpenSSF Scorecard read-only SARIF analysis (`ossf-scorecard-analysis`) | security-scanning | ga | conditional | unavailable | unavailable | `.github/workflows/public-scorecard-analysis.yml` | `examples/public-oss/scorecard-analysis.yml` | | OpenSSF Scorecard JSON artifact (`ossf-scorecard-json`) | security-scanning | ga | free | unavailable | unavailable | `.github/workflows/public-scorecard-json.yml` | `examples/public-oss/scorecard.yml` | diff --git a/examples/README.md b/examples/README.md index 6e755b7..0386112 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,7 +7,11 @@ are mutable; Dependabot bumps the SHA for you). - [`public-oss/`](public-oss/) — full free security suite for public repos. - [`private-free/`](private-free/) — private stack without paid security add-ons; includes bounded-hosted and zero-GitHub-meter self-hosted callers. -- [`private-paid-ghas/`](private-paid-ghas/) — GHAS-enabled private repos. +- [`private-paid-ghas/`](private-paid-ghas/) — private repos that have **explicitly + selected** Code Security / Secret Protection, including the SARIF security + bundle. Not the publisher default. +- [`nddev/`](nddev/) — publisher-org callers: public free surfaces, and a + private-free self-hosted variant. Not a claim the publisher bought GHAS. Use-case groups shared by every tier: [`languages/`](languages/), [`quality/`](quality/), [`security/`](security/), [`testing/`](testing/), diff --git a/examples/nddev/os-capability-routing.yml b/examples/nddev/os-capability-routing.yml index 379c8cd..fe133f0 100644 --- a/examples/nddev/os-capability-routing.yml +++ b/examples/nddev/os-capability-routing.yml @@ -1,7 +1,8 @@ -# NDDev PRIVATE-repository caller compiled from runner-routing.yaml. +# PRIVATE-repository caller compiled from runner-routing.yaml. # Each useful job has its final runner before dispatch: there is no routing job # and no delayed fallback after a self-hosted label enters GitHub's queue. # Do not use the self-hosted Linux routes in a public repository. +# Labels are caller-owned; this example is not a live fleet inventory. # Replace @ with a pinned full commit SHA of ci-workflows. name: os-capability-routing on: diff --git a/examples/nddev/runner-routing.yaml b/examples/nddev/runner-routing.yaml index 76fac78..2e07108 100644 --- a/examples/nddev/runner-routing.yaml +++ b/examples/nddev/runner-routing.yaml @@ -1,6 +1,7 @@ -# Operator mapping for NDDev private repositories. This is estate-specific data, -# not a reusable-workflow default. Compile it before dispatch; GitHub will not -# fall back after a self-hosted label has entered the queue. +# Example operator mapping for private repositories that compile a runner +# before dispatch. This is caller-owned data, not a reusable-workflow default +# and not a live fleet inventory. GitHub will not fall back after a +# self-hosted label has entered the queue. schema: nddev-ci-runner-map/v1 fleet_contract: repository: NDDev-OpenNetwork/github-actions diff --git a/examples/nddev/security-private-selfhosted.yml b/examples/nddev/security-private-selfhosted.yml index d8d4afb..85e1190 100644 --- a/examples/nddev/security-private-selfhosted.yml +++ b/examples/nddev/security-private-selfhosted.yml @@ -1,41 +1,21 @@ -# NDDev estate tier — PRIVATE repository variant. +# Publisher-org PRIVATE repository variant without paid GitHub add-ons. # -# Identical coverage to examples/nddev/security.yml; the only difference is that -# every job is routed to the estate's self-hosted fleet so that the metered -# private-repository minutes never reach the bill. +# Same surfaces as examples/private-free/security-selfhosted.yml: the free +# private bundle (actionlint, zizmor without SARIF, OSV, gitleaks) plus +# private-static. No CodeQL, no SARIF upload, no dependency review — those +# need Code Security on private repositories and are not assumed here. +# Release provenance for this shape is release-supply-chain-free.yml. # # 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. The -# estate's runner group also sets allows_public_repositories: false, but treat -# that as a backstop rather than the control. +# self-hosted runner is a remote-code-execution path into your own hardware. +# A private runner group should also set allows_public_repositories: false as a +# backstop, not as the control. # -# Two scans are NOT configured by this file and must be set per repository, or -# they keep consuming metered minutes regardless of what is written here: -# * CodeQL default setup -> PATCH /repos/{owner}/{repo}/code-scanning/default-setup -# with runner_type=labeled, runner_label=nddev-linux-standard -# * Code Quality -> repository Settings -> Code quality -> Labeled runner -# See docs/05-runners.md#visibility-routing. -# -# Replace @ with a pinned full commit SHA of ci-workflows. The runner -# labels below are the estate's real scale-set classes, documented in -# `github-actions/docs/architecture.md`: -# -# nddev-linux-fast format, lint, typecheck, short unit tests -# (no job credential, no Docker) [no host yet] -# nddev-linux-standard normal builds and tests, repo-scoped credentials -# nddev-linux-integration Docker, Compose, databases, browsers -# nddev-linux-release signing, publish, deploy (isolated) [no host yet] -# -# DECLARED IS NOT DEPLOYED. As of 2026-08-11 the fleet's GARM holds one -# repository entity and serves `NDDev-OpenNetwork/github-actions` alone, so none of -# these labels can take a job from another repository until the organization -# entity and its scale sets are rolled out. Until then this file documents the -# intended routing; a caller that needs to run today names a hosted runner. -# -# The consolidated bundle runs checksum-verified Linux binaries and therefore -# needs no Docker daemon. actionlint, zizmor, OSV-Scanner and gitleaks share one -# checkout and one ephemeral standard worker instead of four cold placements. +# Replace @ with a pinned full commit SHA of ci-workflows. Replace the +# runner labels with isolated ephemeral labels registered only to private +# repositories. Destroy the worker after one job. This file does not publish a +# live fleet inventory. name: security on: push: { branches: [main] } @@ -45,20 +25,14 @@ concurrency: group: ${{ github.workflow }}-${{ github.run_id }} cancel-in-progress: false jobs: - codeql: - permissions: { actions: read, contents: read, security-events: write } - uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-codeql.yml@ - with: - languages: '["python","actions"]' - runner: nddev-linux-standard - dependabot_runner: nddev-linux-untrusted security-bundle: permissions: { contents: read } - uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/nddev-security-bundle.yml@ + uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/private-security-bundle-free.yml@ with: runner: nddev-linux-standard - dependency-review: - permissions: { contents: read, pull-requests: write } - uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/public-dependency-review.yml@ + validate: + permissions: { contents: read } + uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/private-static.yml@ with: runner: nddev-linux-standard + command: "python3 scripts/validate_all.py" diff --git a/examples/nddev/security.yml b/examples/nddev/security.yml index 96c9b2e..bc944ea 100644 --- a/examples/nddev/security.yml +++ b/examples/nddev/security.yml @@ -1,7 +1,9 @@ -# NDDev estate tier — the full paid stack the NDDev-OpenNetwork organization actually owns: -# Enterprise Cloud + Code Security + Secret Protection + Code Quality. -# Runs identically on public and private repositories; on private it is the -# GHAS licences that make CodeQL and SARIF upload legal, not the visibility. +# Publisher-org public caller: the free public security surfaces (CodeQL, SARIF, +# dependency review, gitleaks, actionlint, OSV). These are free on public +# repositories on every current GitHub plan; they are not a claim that the +# publisher purchased Code Security, Secret Protection, Code Quality, or +# Enterprise Cloud. Private repositories without those purchases must not copy +# this file — use examples/private-free/ or security-private-selfhosted.yml. # Replace @ with a pinned full commit SHA of ci-workflows. name: security on: diff --git a/examples/private-paid-ghas/security-selfhosted.yml b/examples/private-paid-ghas/security-selfhosted.yml new file mode 100644 index 0000000..b08559c --- /dev/null +++ b/examples/private-paid-ghas/security-selfhosted.yml @@ -0,0 +1,23 @@ +# Private repository WITH GitHub Code Security, on a caller-owned self-hosted +# label. This is an explicit paid opt-in: zizmor keeps SARIF upload. Callers +# without Code Security must use examples/private-free/security-selfhosted.yml +# instead. Do not copy this file into a public repository. +# Replace `private-ci` with an isolated ephemeral runner label registered only +# to private repositories. Replace @ with a pinned full commit SHA. +name: security +on: + push: { branches: [main] } + pull_request: { branches: [main] } +permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.run_id }} + cancel-in-progress: false +jobs: + security-bundle: + permissions: + actions: read + contents: read + security-events: write + uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/nddev-security-bundle.yml@ + with: + runner: private-ci diff --git a/scripts/check_documented_commands.py b/scripts/check_documented_commands.py index 59ec508..370b2a6 100644 --- a/scripts/check_documented_commands.py +++ b/scripts/check_documented_commands.py @@ -2,8 +2,8 @@ """Documented tool commands must be the commands `ci-gate` actually runs. Four places in this repository told a contributor how to run zizmor locally and -three of them were wrong, each in a different way. `AGENTS.md` and the -`nddev-repo-flow` skill named `--persona regular`; `ci.yml` passes +three of them were wrong, each in a different way. `AGENTS.md` once named +`--persona regular`; `ci.yml` passes `--persona pedantic`, which adds `undocumented-permissions`, so following the brief gave a clean local run and a red required check. `CONTRIBUTING.md` carried a comment reading "regular persona, matches CI" directly above a diff --git a/scripts/check_examples.py b/scripts/check_examples.py index 017d62d..30f5733 100644 --- a/scripts/check_examples.py +++ b/scripts/check_examples.py @@ -35,6 +35,7 @@ "examples/nddev/os-capability-routing.yml", "examples/personal/security-selfhosted.yml", "examples/private-free/security-selfhosted.yml", + "examples/private-paid-ghas/security-selfhosted.yml", ) diff --git a/scripts/check_public_docs.py b/scripts/check_public_docs.py index ae6456c..175b355 100644 --- a/scripts/check_public_docs.py +++ b/scripts/check_public_docs.py @@ -14,16 +14,21 @@ one invited a refresh commit, and the tier docs became the fastest-rotting files in a repository whose whole thesis is that unvalidated claims rot. -So the rule is structural. Durable statements stay — which products the estate -holds, why push protection is off, that a security configuration attaches -atomically, that a budget cannot stop a licence-based product. Countable account -state goes to the control plane. +A later revision then claimed the publisher had already bought Enterprise Cloud, +Code Security, Secret Protection and Code Quality. That is the same class of +defect: a current purchase is account state, not a reusable-workflow contract. +Durable statements stay — plan gates, that a security configuration attaches +atomically, that a budget cannot stop a licence-based product, that paid +programmes are explicitly selectable. Which products any particular +organization currently holds goes to the operator of that account. -Two patterns are rejected, chosen because both are unambiguously *observations* -rather than contracts: +Three patterns are rejected, chosen because they are unambiguously +*observations* rather than contracts: * **inventory counts** — "N repositories", "N repos", "N active committers"; -* **cent-precision currency** — an invoice or an accrual, never a list price. +* **cent-precision currency** — an invoice or an accrual, never a list price; +* **publisher purchase claims** — that this estate or organization has already + bought Enterprise Cloud or the paid add-ons, or is "not on the free plan". A `$0` budget setting is a control, not an observation, and is allowed. List prices are product facts and belong in `catalog/product-facts.yml`, which is @@ -49,6 +54,16 @@ ) # $12.34 — cents mean somebody read an invoice. $0 and $10 are settings/prices. OBSERVED_SPEND = re.compile(r"\$\d[\d,]*\.\d{2}\b") +# Current purchase state of the publisher. Plan gates ("private attestations +# require Enterprise Cloud") must keep passing; these phrases do not. +PUBLISHER_PURCHASE = re.compile( + r"has already bought" + r"|The NDDev estate is not on the free plan" + r"|this estate has Enterprise Cloud" + r"|organization has already bought" + r"|organization has bought Enterprise Cloud", + re.IGNORECASE, +) # Generated files render catalog rows and are checked by their own drift gate. EXEMPT_DIRS = ("docs/generated",) @@ -76,6 +91,7 @@ def check() -> list[str]: for pattern, label in ( (INVENTORY, "an estate inventory count"), (OBSERVED_SPEND, "an observed spend figure"), + (PUBLISHER_PURCHASE, "a publisher purchase claim"), ): hit = pattern.search(line) if hit: @@ -97,6 +113,9 @@ def _selftest() -> list[str]: "36 repositories on default setup", "$1.31 still accrued in August", "billed at $21 + $49 + $10 = $80.00", + "The NDDev estate is not on the free plan", + "organization has already bought — Enterprise Cloud", + "this estate has Enterprise Cloud", ] must_pass = [ "$0 hard-stop budget at org and enterprise", @@ -105,12 +124,16 @@ def _selftest() -> list[str]: "timeout-minutes: 30", "actions/checkout@3d3c42e5 # v7.0.1", "run 30702933166", + "private attestations require GitHub Enterprise Cloud", + "does not assume the publisher purchased Enterprise Cloud", ] for sample in must_flag: - if not (INVENTORY.search(sample) or OBSERVED_SPEND.search(sample)): + if not (INVENTORY.search(sample) or OBSERVED_SPEND.search(sample) + or PUBLISHER_PURCHASE.search(sample)): problems.append(f"check_public_docs self-test: missed {sample!r}") for sample in must_pass: - if INVENTORY.search(sample) or OBSERVED_SPEND.search(sample): + if INVENTORY.search(sample) or OBSERVED_SPEND.search(sample) \ + or PUBLISHER_PURCHASE.search(sample): problems.append(f"check_public_docs self-test: false positive {sample!r}") return problems diff --git a/scripts/resolve_profile.py b/scripts/resolve_profile.py index 3910cd2..12d1f50 100644 --- a/scripts/resolve_profile.py +++ b/scripts/resolve_profile.py @@ -409,7 +409,7 @@ def _render(result: dict[str, Any]) -> str: if result["visibility"] in {"private", "internal"}: if result["controls"].get("compute_billing") == "private-self-hosted": lines.append(" cost guard: pass a self-hosted runner label in every caller") - lines.append(" and route CodeQL default setup / Code Quality separately") + lines.append(" and route managed CodeQL/Code Quality only if enabled") elif result["controls"].get("runner_mode") == "github-hosted-standard": lines.append(" cost warning: private hosted minutes are metered after the plan allowance") lines.append(" cost guard: set an Actions budget with Stop paid usage enabled") @@ -737,6 +737,125 @@ def runnable(result: dict[str, Any]) -> set[str]: "resolve-profile: the two public profiles resolve identically — " "entitlements are not affecting resolution" ) + + problems.extend(_entitlement_path_tests(profiles_doc, capabilities)) + return problems + + +def _entitlement_path_tests(profiles_doc: dict[str, Any], + capabilities: list[dict[str, Any]]) -> list[str]: + """Positive and negative paths for free, public, private and paid opt-in. + + These are behaviour, not string mirrors: a public Free organization must + keep CodeQL and attestations; a private repository without add-ons must + not receive them; an explicit Code Security opt-in must. + """ + problems: list[str] = [] + none = {"code_security": False, "secret_protection": False, + "code_quality": False} + + def included(result: dict[str, Any]) -> set[str]: + return {row["capability"] for row in result["included"]} + + def programme(result: dict[str, Any]) -> dict[str, Any]: + return result["programme"] + + public_free = resolve_shape(profiles_doc, capabilities, "public", "free", none) + public_team = resolve_shape(profiles_doc, capabilities, "public", "team", none) + for label, result in (("public/free/000", public_free), + ("public/team/000", public_team)): + got = included(result) + for cap in ("codeql-code-scanning", "native-secret-scanning", + "artifact-attestations"): + if cap not in got: + problems.append( + f"resolve-profile: {label} dropped free public {cap}" + ) + if "github-code-quality" in got: + problems.append( + f"resolve-profile: {label} included Code Quality without opt-in" + ) + if programme(result)["release_workflow"] != ( + ".github/workflows/release-supply-chain.yml"): + problems.append( + f"resolve-profile: {label} must keep attested public releases" + ) + + private_free = resolve_shape( + profiles_doc, capabilities, "private", "free", none) + private_team = resolve_shape( + profiles_doc, capabilities, "private", "team", none) + for label, result in (("private/free/000", private_free), + ("private/team/000", private_team)): + got = included(result) + for cap in ("codeql-code-scanning", "dependency-review", + "native-secret-scanning", "github-code-quality"): + if cap in got: + problems.append( + f"resolve-profile: {label} included paid {cap} without opt-in" + ) + if programme(result)["release_workflow"] != ( + ".github/workflows/release-supply-chain-free.yml"): + problems.append( + f"resolve-profile: {label} must use checksummed unattested release" + ) + + private_cs = resolve_shape( + profiles_doc, capabilities, "private", "team", + {"code_security": True, "secret_protection": False, + "code_quality": False}, + ) + cs_got = included(private_cs) + if "codeql-code-scanning" not in cs_got: + problems.append( + "resolve-profile: private/team/100 dropped CodeQL after Code Security opt-in" + ) + if "native-secret-scanning" in cs_got: + problems.append( + "resolve-profile: private/team/100 leaked Secret Protection from Code Security" + ) + + private_ghec = resolve_shape( + profiles_doc, capabilities, "private", "enterprise-cloud", none) + if "codeql-code-scanning" in included(private_ghec): + problems.append( + "resolve-profile: private/enterprise-cloud/000 included CodeQL without Code Security" + ) + if programme(private_ghec)["release_workflow"] != ( + ".github/workflows/release-supply-chain.yml"): + problems.append( + "resolve-profile: private/enterprise-cloud/000 must attest — that is a plan gate" + ) + + paid = resolve_shape( + profiles_doc, capabilities, "private", "enterprise-cloud", + {"code_security": True, "secret_protection": True, + "code_quality": True}, + ) + paid_got = included(paid) | { + row["capability"] for row in paid["conditional"] + } + for cap in ("codeql-code-scanning", "native-secret-scanning", + "github-code-quality", "artifact-attestations"): + if cap not in paid_got: + problems.append( + f"resolve-profile: explicit paid opt-in dropped {cap}" + ) + + if invalid_reason(profiles_doc, "private", "free", + {"code_security": True, "secret_protection": False, + "code_quality": False}) is None: + problems.append( + "resolve-profile: private/free/100 must be refused — Code Security " + "is not a Free-plan purchase" + ) + if invalid_reason(profiles_doc, "private", "pro", + {"code_security": False, "secret_protection": False, + "code_quality": True}) is None: + problems.append( + "resolve-profile: private/pro/001 must be refused — Code Quality " + "needs Team or Enterprise Cloud" + ) return problems From 53a2798b515f78bb4a9c05cbd0db19f13f382767 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 7 Sep 2026 16:04:27 +0500 Subject: [PATCH 2/3] fix(docs): keep one organization's GitHub plan from describing another A live plan.name belongs to one account. Public docs still do not copy NDDev-OpenNetwork's observed Free plan, and they now refuse the inverse claim that this organization is on the free plan. Consumers resolve their own plan. Signed-off-by: rldyourmnd --- .agents/skills/ci-consumer-adoption/SKILL.md | 18 +++++++++++------- .../.generated-from-agents-skills | 2 +- .claude/skills/ci-consumer-adoption/SKILL.md | 18 +++++++++++------- CHANGELOG.md | 3 ++- README.md | 5 +++-- docs/00-overview.md | 13 +++++++------ docs/17-nddev-tier.md | 10 +++++++--- scripts/check_public_docs.py | 7 +++++++ 8 files changed, 49 insertions(+), 27 deletions(-) diff --git a/.agents/skills/ci-consumer-adoption/SKILL.md b/.agents/skills/ci-consumer-adoption/SKILL.md index e1ed5b1..0ede928 100644 --- a/.agents/skills/ci-consumer-adoption/SKILL.md +++ b/.agents/skills/ci-consumer-adoption/SKILL.md @@ -41,8 +41,11 @@ cd /tmp/ci-workflows python3.13 -I -B -m venv --copies .venv uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt -# Default: no paid add-ons. Public Free/Team keep CodeQL and attestations. +# Pass THIS consumer organization's live plan. Do not copy another +# organization's observed plan. Public no-addon shapes keep CodeQL and +# attestations on current GitHub plans whether the org is Free or Team: .venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan free +.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan team # Explicit paid opt-in only when those products are independently held: # --visibility private --plan enterprise-cloud \ # --code-security --secret-protection --code-quality @@ -68,12 +71,13 @@ Pick the tier doc first; it decides which reusables are even legal to call: The publisher is a GitHub Organization, not an Enterprise account, and this library does not assume it purchased Code Security, Secret Protection, Code -Quality, or Enterprise Cloud. Do not copy the paid examples into a private -repository that has not bought those products. The inverse trap also exists: -an organization that **did** buy them and then follows private-free will -discard attested releases. Check entitlements before believing a tier table. -Prices and quotas live in `catalog/product-facts.yml`; never quote them from -memory or from a skill. +Quality, or Enterprise Cloud. A live GitHub plan belongs to one organization; +do not copy one account's plan onto another. Do not copy the paid examples into +a private repository that has not bought those products. The inverse trap also +exists: an organization that **did** buy them and then follows private-free +will discard attested releases. Check entitlements before believing a tier +table. Prices and quotas live in `catalog/product-facts.yml`; never quote them +from memory or from a skill. ## 2. Pin — to a released tag, by full SHA diff --git a/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills b/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills index 3a93770..e04d932 100644 --- a/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills +++ b/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills @@ -1,2 +1,2 @@ source=.agents/skills/ci-consumer-adoption/SKILL.md -sha256=cd63484a6e9bb0f28d4151e6a95f9b645f28cc776c28c6a588a5dbba932f7a20 +sha256=02ca2f397c0e8abcc57233d06897327e8e597d2fc7b99b10159a53659fb73d0e diff --git a/.claude/skills/ci-consumer-adoption/SKILL.md b/.claude/skills/ci-consumer-adoption/SKILL.md index e1ed5b1..0ede928 100644 --- a/.claude/skills/ci-consumer-adoption/SKILL.md +++ b/.claude/skills/ci-consumer-adoption/SKILL.md @@ -41,8 +41,11 @@ cd /tmp/ci-workflows python3.13 -I -B -m venv --copies .venv uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt -# Default: no paid add-ons. Public Free/Team keep CodeQL and attestations. +# Pass THIS consumer organization's live plan. Do not copy another +# organization's observed plan. Public no-addon shapes keep CodeQL and +# attestations on current GitHub plans whether the org is Free or Team: .venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan free +.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan team # Explicit paid opt-in only when those products are independently held: # --visibility private --plan enterprise-cloud \ # --code-security --secret-protection --code-quality @@ -68,12 +71,13 @@ Pick the tier doc first; it decides which reusables are even legal to call: The publisher is a GitHub Organization, not an Enterprise account, and this library does not assume it purchased Code Security, Secret Protection, Code -Quality, or Enterprise Cloud. Do not copy the paid examples into a private -repository that has not bought those products. The inverse trap also exists: -an organization that **did** buy them and then follows private-free will -discard attested releases. Check entitlements before believing a tier table. -Prices and quotas live in `catalog/product-facts.yml`; never quote them from -memory or from a skill. +Quality, or Enterprise Cloud. A live GitHub plan belongs to one organization; +do not copy one account's plan onto another. Do not copy the paid examples into +a private repository that has not bought those products. The inverse trap also +exists: an organization that **did** buy them and then follows private-free +will discard attested releases. Check entitlements before believing a tier +table. Prices and quotas live in `catalog/product-facts.yml`; never quote them +from memory or from a skill. ## 2. Pin — to a released tag, by full SHA diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f04530..9c7e639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ The project follows Semantic Versioning. - Stop treating the publisher as an Enterprise Cloud buyer of Code Security, Secret Protection and Code Quality. Paid programmes stay explicitly selectable; public CodeQL, SARIF, Scorecard and attestations stay. Private - repositories without those purchases use the private-free programme. + repositories without those purchases use the private-free programme. A live + GitHub plan belongs to one organization and is not copied between accounts. - Re-verify four vendor allowance records with staggered review dates, correct Ubicloud's monthly credit and Harness's conditional CI credit semantics, and diff --git a/README.md b/README.md index 2af31f7..ad62e63 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,9 @@ in [`catalog/product-facts.yml`](catalog/product-facts.yml). See This library is published from a **GitHub Organization**, not an Enterprise account, and it does **not** assume the publisher purchased Enterprise Cloud, -Code Security, Secret Protection, or Code Quality. Paid programmes stay -explicitly selectable for other consumers — see +Code Security, Secret Protection, or Code Quality. A live GitHub plan belongs +to one organization; do not copy one account's plan onto another. Paid +programmes stay explicitly selectable for other consumers — see [`docs/17-nddev-tier.md`](docs/17-nddev-tier.md). Public repositories still get the free GitHub security surfaces (CodeQL, SARIF, dependency review, Scorecard, attestations). Private repositories without those purchases use the diff --git a/docs/00-overview.md b/docs/00-overview.md index 22d57c4..898204d 100644 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -57,12 +57,13 @@ free maintainability substitutes are listed there. The three tiers describe what GitHub *offers*. This library is published from a GitHub Organization, not an Enterprise account, and it does **not** assume the publisher purchased Enterprise Cloud, Code Security, Secret Protection, or Code -Quality. [17 Paid organization programmes](17-nddev-tier.md) is the opt-in -guide for consumers who independently hold those products. Public repositories -still get free CodeQL, SARIF, dependency review, Scorecard and attestations. -Private repositories without those purchases use the private-free programme and -`release-supply-chain-free.yml`; attested private releases require Enterprise -Cloud. +Quality. A live GitHub plan belongs to one organization; do not copy one +account's plan onto another. [17 Paid organization programmes](17-nddev-tier.md) +is the opt-in guide for consumers who independently hold those products. Public +repositories still get free CodeQL, SARIF, dependency review, Scorecard and +attestations. Private repositories without those purchases use the private-free +programme and `release-supply-chain-free.yml`; attested private releases require +Enterprise Cloud. ### Personal-account repositories diff --git a/docs/17-nddev-tier.md b/docs/17-nddev-tier.md index 79ffb84..7ce87df 100644 --- a/docs/17-nddev-tier.md +++ b/docs/17-nddev-tier.md @@ -22,9 +22,13 @@ account. The public generic library: Scorecard, or artifact attestations — those are free on public repositories on every current GitHub plan. -When a function depends on Free versus Team, resolve it from the live plan and -from [`catalog/product-facts.yml`](../catalog/product-facts.yml). Do not copy a -plan name into this document as if it were a durable product fact. +When a function depends on Free versus Team, resolve it from **that +organization's** live plan and from +[`catalog/product-facts.yml`](../catalog/product-facts.yml). A live GitHub +`plan.name` belongs to one organization. Observing it for one account does not +describe any other organization, including other NDDev accounts, and this +library does not copy that field here. Do not treat "organization" as Free, as +Team, or as Enterprise until that organization's own plan is read. This repository itself is **public** and runs the `public-free-standalone` programme: CodeQL, zizmor with SARIF, dependency diff --git a/scripts/check_public_docs.py b/scripts/check_public_docs.py index 175b355..46ac57e 100644 --- a/scripts/check_public_docs.py +++ b/scripts/check_public_docs.py @@ -59,6 +59,9 @@ PUBLISHER_PURCHASE = re.compile( r"has already bought" r"|The NDDev estate is not on the free plan" + r"|this organization is on the free plan" + r"|the publisher is on the free plan" + r"|NDDev-OpenNetwork is on the free plan" r"|this estate has Enterprise Cloud" r"|organization has already bought" r"|organization has bought Enterprise Cloud", @@ -114,6 +117,8 @@ def _selftest() -> list[str]: "$1.31 still accrued in August", "billed at $21 + $49 + $10 = $80.00", "The NDDev estate is not on the free plan", + "this organization is on the free plan", + "NDDev-OpenNetwork is on the free plan", "organization has already bought — Enterprise Cloud", "this estate has Enterprise Cloud", ] @@ -126,6 +131,8 @@ def _selftest() -> list[str]: "run 30702933166", "private attestations require GitHub Enterprise Cloud", "does not assume the publisher purchased Enterprise Cloud", + "A live GitHub plan belongs to one organization", + "resolve it from the live plan", ] for sample in must_flag: if not (INVENTORY.search(sample) or OBSERVED_SPEND.search(sample) From 56e118e502a5e965717bd0ab1550aa8007a13286 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 7 Sep 2026 16:10:28 +0500 Subject: [PATCH 3/3] fix(skill): resolve adoption from the pinned release, not main Checkout the consumer-selected immutable release tag, drop the frozen latest release SHA, and state that private attestations need Enterprise Cloud rather than Code Security, Secret Protection or Code Quality. Signed-off-by: rldyourmnd --- .agents/skills/ci-consumer-adoption/SKILL.md | 41 +++++++++++-------- .../.generated-from-agents-skills | 2 +- .claude/skills/ci-consumer-adoption/SKILL.md | 41 +++++++++++-------- AGENTS.md | 2 +- CHANGELOG.md | 3 ++ 5 files changed, 51 insertions(+), 38 deletions(-) diff --git a/.agents/skills/ci-consumer-adoption/SKILL.md b/.agents/skills/ci-consumer-adoption/SKILL.md index 0ede928..d6f30c4 100644 --- a/.agents/skills/ci-consumer-adoption/SKILL.md +++ b/.agents/skills/ci-consumer-adoption/SKILL.md @@ -12,8 +12,8 @@ metadata: # Adopting ci-workflows in a consumer repository -This is the *caller* side. For work inside the library itself use `AGENTS.md` -and the repository validators. There is no `nddev-repo-flow` skill. +This is the *caller* side. For work inside this library, use `AGENTS.md` and +`scripts/` in the library checkout. Adoption is four decisions, in order. Getting them out of order is what produces the two failure shapes seen in practice: a repository that looks configured but @@ -27,14 +27,14 @@ The resolver lives in the library, not in your repository, so check the library out first. Everything below runs in that checkout, not in yours: ```bash -# Pin to the ref you intend to consume. Re-read the latest immutable release -# before adoption rather than copying a review-time value. At the 2026-09-07 -# review the current release is 0.1.16 at -# 377e5311509e3e162d7bf377c1faf57e6b0e622a and ships resolve_profile.py. -LIBRARY_REF=main -git clone --depth 1 --branch "$LIBRARY_REF" \ +# LIBRARY_TAG is the immutable SemVer release you verified and will pin. +# Resolve the programme from that same revision. Do not check out main. +LIBRARY_TAG="${LIBRARY_TAG:?set LIBRARY_TAG to the release tag you will pin}" +git clone --depth 1 --branch "$LIBRARY_TAG" \ https://github.com/NDDev-OpenNetwork/ci-workflows.git /tmp/ci-workflows cd /tmp/ci-workflows +LIBRARY_SHA=$(git rev-parse HEAD) +# Write $LIBRARY_SHA in every consumer `uses: ...@` line. # The library runs every Python tool through one launcher, which needs its own # environment. A bare `python3 scripts/...` aborts with ModuleNotFoundError. @@ -46,9 +46,8 @@ uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt # attestations on current GitHub plans whether the org is Free or Team: .venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan free .venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan team -# Explicit paid opt-in only when those products are independently held: -# --visibility private --plan enterprise-cloud \ -# --code-security --secret-protection --code-quality +# Private attestations need --plan enterprise-cloud. Add-on flags are separate: +# --code-security, --secret-protection, --code-quality. ``` It returns the matching profile, its controls (CodeQL mode, runner class, @@ -72,12 +71,18 @@ Pick the tier doc first; it decides which reusables are even legal to call: The publisher is a GitHub Organization, not an Enterprise account, and this library does not assume it purchased Code Security, Secret Protection, Code Quality, or Enterprise Cloud. A live GitHub plan belongs to one organization; -do not copy one account's plan onto another. Do not copy the paid examples into -a private repository that has not bought those products. The inverse trap also -exists: an organization that **did** buy them and then follows private-free -will discard attested releases. Check entitlements before believing a tier -table. Prices and quotas live in `catalog/product-facts.yml`; never quote them -from memory or from a skill. +do not copy one account's plan onto another. + +Public repositories keep CodeQL, native secret scanning, dependency review and +artifact attestations on current GitHub plans without those add-ons. Code +Security, Secret Protection and Code Quality are independent purchases; none +of them unlocks private Artifact Attestations. That is an Enterprise Cloud +**plan** gate. Following private-free on a private repository without +Enterprise Cloud is correct even if Code Security is held. Following +private-free on Enterprise Cloud drops attested `release-supply-chain.yml` +even if no add-on is held. Check entitlements and the plan gate separately. +Prices and quotas live in `catalog/product-facts.yml`; never quote them from +memory or from a skill. ## 2. Pin — to a released tag, by full SHA @@ -179,5 +184,5 @@ reconciles placement; workflow-level retries must not duplicate an active job. 5. Managed CodeQL default setup and Code Quality routed **only** when those products are enabled. 6. A completed run inspected for `runner_name`, not just a saved setting. 7. AI findings off unless deliberately sized. -8. Release caller matches entitlement — attested where the plan allows it. +8. Release caller matches the plan gate: attested on public, and on private/internal only with Enterprise Cloud. Add-ons do not unlock private attestations. 9. Transient retries are idempotent, logged, and capped at three attempts. diff --git a/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills b/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills index e04d932..311f4b7 100644 --- a/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills +++ b/.claude/skills/ci-consumer-adoption/.generated-from-agents-skills @@ -1,2 +1,2 @@ source=.agents/skills/ci-consumer-adoption/SKILL.md -sha256=02ca2f397c0e8abcc57233d06897327e8e597d2fc7b99b10159a53659fb73d0e +sha256=7eff04b0c5e8be82549cd25560bbb18c504de9869daaf1e777974e05383165c6 diff --git a/.claude/skills/ci-consumer-adoption/SKILL.md b/.claude/skills/ci-consumer-adoption/SKILL.md index 0ede928..d6f30c4 100644 --- a/.claude/skills/ci-consumer-adoption/SKILL.md +++ b/.claude/skills/ci-consumer-adoption/SKILL.md @@ -12,8 +12,8 @@ metadata: # Adopting ci-workflows in a consumer repository -This is the *caller* side. For work inside the library itself use `AGENTS.md` -and the repository validators. There is no `nddev-repo-flow` skill. +This is the *caller* side. For work inside this library, use `AGENTS.md` and +`scripts/` in the library checkout. Adoption is four decisions, in order. Getting them out of order is what produces the two failure shapes seen in practice: a repository that looks configured but @@ -27,14 +27,14 @@ The resolver lives in the library, not in your repository, so check the library out first. Everything below runs in that checkout, not in yours: ```bash -# Pin to the ref you intend to consume. Re-read the latest immutable release -# before adoption rather than copying a review-time value. At the 2026-09-07 -# review the current release is 0.1.16 at -# 377e5311509e3e162d7bf377c1faf57e6b0e622a and ships resolve_profile.py. -LIBRARY_REF=main -git clone --depth 1 --branch "$LIBRARY_REF" \ +# LIBRARY_TAG is the immutable SemVer release you verified and will pin. +# Resolve the programme from that same revision. Do not check out main. +LIBRARY_TAG="${LIBRARY_TAG:?set LIBRARY_TAG to the release tag you will pin}" +git clone --depth 1 --branch "$LIBRARY_TAG" \ https://github.com/NDDev-OpenNetwork/ci-workflows.git /tmp/ci-workflows cd /tmp/ci-workflows +LIBRARY_SHA=$(git rev-parse HEAD) +# Write $LIBRARY_SHA in every consumer `uses: ...@` line. # The library runs every Python tool through one launcher, which needs its own # environment. A bare `python3 scripts/...` aborts with ModuleNotFoundError. @@ -46,9 +46,8 @@ uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt # attestations on current GitHub plans whether the org is Free or Team: .venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan free .venv/bin/python -I -B scripts/check_python_execution_contract.py --launch resolve_profile.py -- --visibility public --plan team -# Explicit paid opt-in only when those products are independently held: -# --visibility private --plan enterprise-cloud \ -# --code-security --secret-protection --code-quality +# Private attestations need --plan enterprise-cloud. Add-on flags are separate: +# --code-security, --secret-protection, --code-quality. ``` It returns the matching profile, its controls (CodeQL mode, runner class, @@ -72,12 +71,18 @@ Pick the tier doc first; it decides which reusables are even legal to call: The publisher is a GitHub Organization, not an Enterprise account, and this library does not assume it purchased Code Security, Secret Protection, Code Quality, or Enterprise Cloud. A live GitHub plan belongs to one organization; -do not copy one account's plan onto another. Do not copy the paid examples into -a private repository that has not bought those products. The inverse trap also -exists: an organization that **did** buy them and then follows private-free -will discard attested releases. Check entitlements before believing a tier -table. Prices and quotas live in `catalog/product-facts.yml`; never quote them -from memory or from a skill. +do not copy one account's plan onto another. + +Public repositories keep CodeQL, native secret scanning, dependency review and +artifact attestations on current GitHub plans without those add-ons. Code +Security, Secret Protection and Code Quality are independent purchases; none +of them unlocks private Artifact Attestations. That is an Enterprise Cloud +**plan** gate. Following private-free on a private repository without +Enterprise Cloud is correct even if Code Security is held. Following +private-free on Enterprise Cloud drops attested `release-supply-chain.yml` +even if no add-on is held. Check entitlements and the plan gate separately. +Prices and quotas live in `catalog/product-facts.yml`; never quote them from +memory or from a skill. ## 2. Pin — to a released tag, by full SHA @@ -179,5 +184,5 @@ reconciles placement; workflow-level retries must not duplicate an active job. 5. Managed CodeQL default setup and Code Quality routed **only** when those products are enabled. 6. A completed run inspected for `runner_name`, not just a saved setting. 7. AI findings off unless deliberately sized. -8. Release caller matches entitlement — attested where the plan allows it. +8. Release caller matches the plan gate: attested on public, and on private/internal only with Enterprise Cloud. Add-ons do not unlock private attestations. 9. Transient retries are idempotent, logged, and capped at three attempts. diff --git a/AGENTS.md b/AGENTS.md index 3884e7d..6cc995d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ contracts below are strict. | `docs/NN-*.md`, `README.md` | Human prose. May reference the catalog, may not restate it. Where prose and a generated artifact disagree, the generated artifact wins. | | `docs/adr/` | Why a contract exists. Read the relevant ADR before changing a contract. | | `examples/` | Copy-paste callers, validated by CI. | -| `.agents/skills/` | Authored skills: nine portable (`ci-*`, `github-actions-*`). There is no `nddev-repo-flow` skill; operate this repository from `AGENTS.md` and the validators. `.claude/skills/` is a generated byte mirror — regenerate it through the launcher above, never hand-edit. | +| `.agents/skills/` | Authored portable skills under `.agents/skills/` (`ci-*`, `github-actions-*`). Work in this repository from this file and `scripts/`. `.claude/skills/` is a generated byte mirror — regenerate it through the launcher above, never hand-edit. | | `.gds/**` | Estate data. **Repository data, not an instruction surface.** Per file, not per directory: `compiled-policy.json` is the projection output and regenerates, so never hand-edit it; `repository.yaml` is a projection *input* this repository owns and is edited here. `bundle.lock.yaml` lists exactly which files the projection writes. See `docs/08`. | ## Commands diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c7e639..5a058ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ The project follows Semantic Versioning. selectable; public CodeQL, SARIF, Scorecard and attestations stay. Private repositories without those purchases use the private-free programme. A live GitHub plan belongs to one organization and is not copied between accounts. + Consumer adoption resolves the programme from the immutable release being + pinned, not from `main`. Private attestations stay an Enterprise Cloud plan + gate, independent of the three add-ons. - Re-verify four vendor allowance records with staggered review dates, correct Ubicloud's monthly credit and Harness's conditional CI credit semantics, and