diff --git a/.github/workflows/governance-issue.yml b/.github/workflows/governance-issue.yml new file mode 100644 index 0000000..d1964c1 --- /dev/null +++ b/.github/workflows/governance-issue.yml @@ -0,0 +1,37 @@ +name: Governance — Issue + +on: + workflow_call: + +permissions: + issues: write + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const body = context.payload.issue.body || ''; + const add = new Set(); + const f = n => ((body.match(new RegExp(`###\\s*${n}\\s*\\n+([\\s\\S]*?)(?=\\n###\\s|$)`,'i'))||[,''])[1]||'').replace(/_No response_/gi,'').trim(); + + const key = (f('Severity').match(/S[1-4]/)||[])[0]; + const PRIORITY = { S1: 'P0', S2: 'P1', S3: 'P2', S4: 'P3' }; + if (key) { add.add(`sev:${key}`); add.add('priority:' + PRIORITY[key]); } + + const SECRETS = [ + [/\bgh[pousr]_[A-Za-z0-9]{20,}\b/,'GitHub token'], + [/\bAKIA[0-9A-Z]{16}\b/,'AWS key id'], + [/\bsk-[A-Za-z0-9]{20,}\b/,'API secret'], + [/-----BEGIN [A-Z ]*PRIVATE KEY-----/,'private key'], + ]; + const hits = SECRETS.filter(([re]) => re.test(body)).map(([,n]) => n); + if (hits.length) { + add.add('security:possible-leak'); + await github.rest.issues.createComment({ ...context.repo, issue_number: context.payload.issue.number, + body: `> [!CAUTION]\n> Possible **${hits.join(', ')}** in this issue. Edit it out and **rotate the credential** — edit history is public.` }); + } + if (add.size) await github.rest.issues.addLabels({ ...context.repo, issue_number: context.payload.issue.number, labels: [...add] }); + if (hits.length) core.setFailed('Possible credential: ' + hits.join(', ')); diff --git a/.github/workflows/governance-pr.yml b/.github/workflows/governance-pr.yml new file mode 100644 index 0000000..4b9ca91 --- /dev/null +++ b/.github/workflows/governance-pr.yml @@ -0,0 +1,45 @@ +name: Governance — PR + +on: + workflow_call: + inputs: + strict: + type: boolean + default: true + +permissions: + contents: read + pull-requests: write + +jobs: + gates: + if: github.event.pull_request.draft == false && github.event.pull_request.user.login != 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const body = context.payload.pull_request.body || ''; + const fail = []; + const sec = n => (body.match(new RegExp(`##\\s*${n}([\\s\\S]*?)(?=\\n##\\s|$)`,'i'))||[,''])[1]; + + const problem = sec('Problem').replace(//g,'').replace(/Closes #\d*/i,'').trim(); + if (problem.length < 30) fail.push('**Problem** empty or boilerplate.'); + + const risk = [...sec('Risk').matchAll(/^\s*-\s*\[([ xX])\]/gm)].filter(m => m[1] !== ' '); + if (risk.length !== 1) fail.push(`**Risk**: check exactly one (found ${risk.length}).`); + + if (!/```[\s\S]*?```/.test(sec('Evidence')) && !/actions\/runs\/\d+/.test(sec('Evidence'))) + fail.push('**Evidence**: paste output or link a CI run.'); + + for (const line of sec('Gates').split('\n')) { + const m = line.match(/^\s*-\s*\[ \]\s*(.+)$/); + if (m && !/(n\/a|not applicable|—|--|:)\s*\S{4,}/i.test(m[1])) + fail.push(`**Gate** unchecked without reason: _${m[1].slice(0,60)}_`); + } + + if (fail.length) { + core.summary.addHeading('Governance gates',2).addList(fail).write(); + if (${{ inputs.strict }}) core.setFailed(`${fail.length} gate issue(s).`); + else core.warning(`${fail.length} gate issue(s) — advisory mode.`); + } diff --git a/.github/workflows/seed-governance.yml b/.github/workflows/seed-governance.yml new file mode 100644 index 0000000..3f1c5ae --- /dev/null +++ b/.github/workflows/seed-governance.yml @@ -0,0 +1,106 @@ +name: Seed non-inheritable governance + +# Community health files (SECURITY.md, CONTRIBUTING.md, PR/issue templates) are +# INHERITED automatically — this workflow does not touch them. +# +# It seeds only the two things GitHub cannot inherit: +# 1. .github/CODEOWNERS (not a community health file) +# 2. .github/workflows/governance.yml (a 12-line caller pinned to @v1) +# +# This is a ONE-TIME seed per repo. After it lands, updating governance logic means +# editing governance-pr.yml here and moving the v1 tag — no fan-out, no token use. + +on: + workflow_dispatch: + inputs: + mode: + description: Dry run lists targets only; seed opens PRs. + type: choice + options: [dry-run, seed] + default: dry-run + repo_filter: + description: Optional substring filter, e.g. "l9-" to pilot a subset. + type: string + default: "" + +permissions: + contents: read + +jobs: + seed: + runs-on: ubuntu-latest + environment: governance-distribution # add required reviewers here + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: token + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + with: + app-id: ${{ vars.GOVERNANCE_APP_ID }} + private-key: ${{ secrets.GOVERNANCE_APP_PRIVATE_KEY }} + owner: Quantum-L9 + + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.token.outputs.token }} + script: | + const fs = require('fs'); + const MODE = '${{ inputs.mode }}'; + const FILTER = '${{ inputs.repo_filter }}'; + const BRANCH = 'chore/seed-governance'; + + const payload = { + '.github/CODEOWNERS': fs.readFileSync('templates/CODEOWNERS.repo', 'utf8'), + '.github/workflows/governance.yml': fs.readFileSync('templates/governance-caller.yml', 'utf8'), + }; + + const all = await github.paginate(github.rest.repos.listForOrg, { org: 'Quantum-L9', type: 'all', per_page: 100 }); + const targets = all.filter(r => !r.archived && !r.fork && r.name !== '.github' && (!FILTER || r.name.includes(FILTER))); + + const rows = []; + for (const repo of targets) { + const o = repo.owner.login, n = repo.name; + try { + // idempotency: skip if both files already exist + const present = await Promise.all(Object.keys(payload).map(p => + github.rest.repos.getContent({ owner: o, repo: n, path: p }).then(() => true).catch(() => false))); + if (present.every(Boolean)) { rows.push([n, 'already seeded']); continue; } + + if (MODE === 'dry-run') { rows.push([n, 'would seed']); continue; } + + const base = repo.default_branch; + const ref = await github.rest.git.getRef({ owner: o, repo: n, ref: `heads/${base}` }); + await github.rest.git.createRef({ owner: o, repo: n, ref: `refs/heads/${BRANCH}`, sha: ref.data.object.sha }) + .catch(e => { if (e.status !== 422) throw e; }); + + for (const [path, content] of Object.entries(payload)) { + const existing = await github.rest.repos.getContent({ owner: o, repo: n, path, ref: BRANCH }).catch(() => null); + await github.rest.repos.createOrUpdateFileContents({ + owner: o, repo: n, path, branch: BRANCH, + message: `chore(governance): seed ${path}`, + content: Buffer.from(content).toString('base64'), + sha: existing?.data?.sha, + }); + } + + const pr = await github.rest.pulls.create({ + owner: o, repo: n, head: BRANCH, base, + title: 'chore(governance): seed CODEOWNERS and governance caller', + body: [ + 'Seeds the only two governance files GitHub cannot inherit from `Quantum-L9/.github`.', + '', + '- `.github/CODEOWNERS` — CODEOWNERS is not a community health file, so it must be physical.', + '- `.github/workflows/governance.yml` — a caller pinned to `@v1`. Logic lives in `.github`; this file should never need to change again.', + '', + 'Everything else (SECURITY.md, CONTRIBUTING.md, PR and issue templates) is already inherited automatically — nothing was copied.', + ].join('\n'), + }); + rows.push([n, `PR #${pr.data.number}`]); + } catch (e) { + rows.push([n, `failed: ${e.status || ''} ${e.message}`.slice(0, 90)]); + } + } + + core.summary.addHeading(`Seed (${MODE}) — ${rows.length} repos`, 2) + .addTable([[{data:'repo',header:true},{data:'result',header:true}], ...rows.map(r => r.map(String))]) + .write(); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be1fd66..57c75ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,23 +14,27 @@ Before opening any pull request, verify each item: --- -## Quick Setup (3 Steps) +## Quick Setup -```bash -# Step 1: Clone Cursor-Governance alongside your target repo -git clone https://github.com/Quantum-L9/Cursor-Governance.git - -# Step 2: Run workspace symlink wiring -cd Cursor-Governance -bash scripts/setup_workspace_symlinks.sh +Governance defaults (PR/issue templates, `SECURITY.md`, this file) are **inherited +automatically** from `Quantum-L9/.github` — no cloning or copying required. For a +new or existing repo, one idempotent command verifies your setup and reports +anything that still needs local wiring: -# Step 3: Validate symlinks -ls -la .cursor/rules .cursor/skills .cursor/commands -# Expected: all three resolve without error +```bash +# one-time bootstrap for a new or existing repo (idempotent, safe to re-run) +curl -fsSL https://raw.githubusercontent.com/Quantum-L9/.github/main/scripts/bootstrap.sh | bash ``` -Per [CANONICAL_LAW.md §2](https://github.com/Quantum-L9/Cursor-Governance/blob/main/CANONICAL_LAW.md#2-symlink-contract): -the workspace root must have `.cursor/` symlinks resolving to `Cursor-Governance/rules/`, `skills/`, and `commands/`. +`bootstrap.sh` reports which files inherit from the org defaults and which have +local overrides. It never duplicates inherited files — duplication is the drift +mechanism this repo exists to eliminate (see `docs/AUDIT.md` finding #4 for the +migration away from the previous "clone Cursor-Governance alongside your repo" step). + +For Cursor workspace wiring (rules/skills/commands symlinks), follow +[CANONICAL_LAW.md §2](https://github.com/Quantum-L9/Cursor-Governance/blob/main/CANONICAL_LAW.md#2-symlink-contract): +the workspace root must have `.cursor/` symlinks resolving to `Cursor-Governance/rules/`, `skills/`, and `commands/`, +validated with `ls -la .cursor/rules .cursor/skills .cursor/commands`. --- diff --git a/README.md b/README.md new file mode 100644 index 0000000..153dd3b --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# Quantum-L9 — org defaults & governance + +Canonical, inheritance-first governance for the Quantum-L9 constellation. + +## Status of the five findings + +| # | Finding | Fix | Propagation | Token | +| --- | --- | --- | --- | --- | +| 1 | PR template was at repo root, so it never propagated | `.github/pull_request_template.md` | Inherited | No | +| 2 | No canonical CODEOWNERS | `.github/CODEOWNERS` + `templates/CODEOWNERS.repo` | Seeded once | Yes | +| 3 | SECURITY.md duplicated per repo | canonical `SECURITY.md` | Inherited | No | +| 4 | CONTRIBUTING.md was a manual clone step | rewritten + `scripts/bootstrap.sh` | Inherited | No | +| 5 | No cross-repo mechanism | `workflow_call` callees + 12-line caller | By reference | No | + +Four of five need **no credentials at all**. See `docs/DISTRIBUTION.md`. + +## Layout + +``` +.github/ +├── pull_request_template.md # inherited org-wide ← finding 1 +├── CODEOWNERS # governs THIS repo ← finding 2 +└── workflows/ + ├── governance-pr.yml # workflow_call callee ← finding 5 + ├── governance-issue.yml # workflow_call callee + └── seed-governance.yml # dispatch-only, seeds the 2 non-inheritable files +templates/ +├── CODEOWNERS.repo # copied into each repo +└── governance-caller.yml # 12-line caller, pinned @v1 +SECURITY.md # inherited org-wide ← finding 3 +CONTRIBUTING.md # inherited org-wide ← finding 4 +docs/ +├── AUDIT.md # findings + evidence +└── DISTRIBUTION.md # what the token does and does not block +scripts/ +├── preflight.sh # read-only verification, run first +└── bootstrap.sh +``` + +## Before you merge + +```bash +./scripts/preflight.sh +``` + +It checks that every `@Quantum-L9/` slug in CODEOWNERS actually exists (an +unresolvable owner makes the rule silently inert), that this repo is public, and +which repos have local overrides that block inheritance. + +## Preflight coverage + +`./scripts/preflight.sh` is read-only and checks five things: + +1. Every `@Quantum-L9/` slug in CODEOWNERS resolves to a real org team — an + unresolvable owner makes the rule silently inert. +2. This repo is public, without which nothing inherits. +3. Which repos already have `CODEOWNERS` / the governance caller (seed idempotency). +4. Which repos hold local overrides that block inheritance. +5. **Actions policy per repo** — a `local_only` or disabled-Actions repo will fail a + cross-repo `workflow_call` before any step runs. See `docs/DISTRIBUTION.md` + Appendix B. + +## Placeholders — resolved at integration + +The pack shipped with unverified placeholder slugs (`@Quantum-L9/maintainers`, +`governance`, `infra`, `ci-cd`, `security`). At integration time the live org was +queried (`gh api orgs/Quantum-L9/teams`): only **`platform`** exists. All CODEOWNERS +rules in `templates/CODEOWNERS.repo` now use `@Quantum-L9/platform` (+ `@cryptoxdog` +on blast-radius paths), matching this repo's existing `.github/CODEOWNERS`. + +## Integration notes (v2.0.1 → live repo) + +- `.github/CODEOWNERS` — the repo already had a canonical copy with real teams; + the pack's placeholder version was **not** installed over it. +- `.github/pull_request_template.md` — owned by PR #15 (pr-template-kit), which + ships a superset of the pack's template (adds *Changes by intent* + the + `pr-files.yml` bot). Finding 1's fix (nested path) is satisfied there. +- `ISSUE_TEMPLATE/` — owned by PR #16 (issue-template-kit), per the pack's own + "what not to do yet" guidance. +- `SECURITY.md` / `CONTRIBUTING.md` — merged: existing detail retained, pack's + canonical-single-source clause, disclosure timeline, and bootstrap-first setup added. diff --git a/SECURITY.md b/SECURITY.md index e2cd9d4..3e8c28b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,18 @@ ## Scope -This policy applies to all repositories in the **Quantum-L9** GitHub organization. +This policy applies to all repositories in the **Quantum-L9** GitHub organization, +including internal tooling (`l9-*` cartridges), infrastructure-as-code, and CI/CD +workflows. This file is the **single canonical source**, inherited org-wide from +`Quantum-L9/.github` — individual repos MUST NOT maintain a competing `SECURITY.md`; +link here instead. (One local copy makes that repo ignore this file entirely; there +is no merging.) + +## Out of Scope + +Vulnerabilities requiring physical access, social engineering of maintainers, or +issues in third-party dependencies without a demonstrated exploit path against +Quantum-L9 systems specifically — report those upstream instead. ## Reporting a Vulnerability diff --git a/docs/AUDIT.md b/docs/AUDIT.md new file mode 100644 index 0000000..d676a4d --- /dev/null +++ b/docs/AUDIT.md @@ -0,0 +1,156 @@ +# AUDIT — Quantum-L9/.github findings and fixes + +Evidence-based, per the Leverage kernel: confirmed facts are cited; everything else +is labeled UNKNOWN. Full API tree access was unavailable when this was compiled — +treat item ordering as leverage-ranked hypothesis, and re-verify against the live +repo before merging. + +## Finding 1 — PR template at wrong path (leverage_score: 5, backbone_candidate) + +**Confirmed**: `PULL_REQUEST_TEMPLATE.md` is at repo root +(github.com/Quantum-L9/.github/blob/main/PULL_REQUEST_TEMPLATE.md), not at +`.github/pull_request_template.md` inside the `.github` folder. + +**Why it matters**: GitHub only serves org-wide default PR templates from the +nested path. A root-level file may render for the `.github` repo's own PRs but does +**not** propagate to the other ~28 repos in the org. Every PR across the +constellation is currently missing whatever governance this template intended to +enforce. + +**Fix shipped**: `.github/pull_request_template.md` at the correct nested path, +carrying forward Problem/Risk/Evidence/Gates structure consistent with the +`pr-template-kit` already delivered in this thread. + +**Future acceleration**: every repo's next PR inherits governance with zero +per-repo setup. **Existing amplification**: repos with no local override +immediately gain structure they previously lacked. + +## Finding 2 — No canonical CODEOWNERS (leverage_score: 4.5) + +**Confirmed**: no CODEOWNERS file surfaced in any search across the org's public +surface. **Inference, not confirmed**: this likely means zero automatic reviewer +assignment org-wide — UNKNOWN whether a private/undiscovered copy exists per repo. + +**Why it matters**: without CODEOWNERS, review routing is tribal knowledge, which +is exactly the "unclear ownership" friction pattern your own framework flags in +`pass_4_canonicalization`. + +**Fix shipped**: `.github/CODEOWNERS` with placeholder team slugs +(`@Quantum-L9/maintainers`, `governance`, `infra`, `ci-cd`, `security`) — **these +slugs are UNKNOWN and must be confirmed against actual GitHub org teams before +merge**, or CODEOWNERS silently fails validation. + +## Finding 3 — SECURITY.md duplicated per repo (leverage_score: 4) + +**Confirmed**: `SECURITY.md` found on `Quantum-L9/Cursor-Governance`, containing +org-wide-scoped policy language ("applies to all repositories in the Quantum-L9 +... organization") — meaning the same text is likely copy-pasted per repo rather +than inherited from one canonical source. + +**Why it matters**: 29 near-identical copies is the "duplicated concepts / +scattered configs" pattern — a single edit (e.g. disclosure window change) requires +N manual edits and inevitably drifts. + +**Fix shipped**: canonical `SECURITY.md` in `.github` (this repo). Note: unlike PR +templates, `SECURITY.md` in `.github` **does** auto-serve as the org-wide fallback +security policy shown on every repo's Security tab that lacks its own — this is a +genuine one-shot canonicalization, no distribution mechanism needed. + +## Finding 4 — CONTRIBUTING.md is a manual, non-idempotent clone step +(leverage_score: 4) + +**Confirmed**: current instructions are "clone Cursor-Governance alongside your +target repo" as a 3-step manual process. + +**Why it matters**: this is human-executed setup friction repeated per repo, per +contributor, per machine — the opposite of a reusable primitive, and it has no +CI-verifiable outcome. + +**Fix shipped**: `CONTRIBUTING.md` rewritten around `scripts/bootstrap.sh`, a +single idempotent command replacing the manual clone. **Note**: `bootstrap.sh` +ships as a starter — it currently only reports what it would do; wiring it to your +actual governance-hook installation is the next concrete step, flagged rather than +faked. + +## Finding 5 — No cross-repo distribution mechanism (leverage_score: 5, +constellation multiplier) + +**Confirmed by structural necessity, not direct evidence**: templates +(`pull_request_template.md`, issue forms, `SECURITY.md`) auto-propagate from +`.github`. **Workflows do not.** With 29 repos, any workflow fix made only in +`.github` has zero effect elsewhere until distributed. + +**Fix shipped**: `.github/workflows/distribute-defaults.yml` — an org-fan-out +workflow enumerating all non-archived repos via the Org Repos API. **Shipped as +dry-run intentionally**: it lists distribution targets in the job summary but does +not yet open PRs, since that requires an `ORG_DISTRIBUTION_TOKEN` with +`repo:write` scope across 29 repos — a decision your governance owner should make +explicitly, not one this kit should make silently. + +## What was NOT fixed (per "what_not_to_do_yet") + +- **Issue forms** — UNKNOWN whether `ISSUE_TEMPLATE/` exists in this repo. Not + touched here to avoid overwriting an unverified asset; use the + `issue-template-kit` delivered earlier in this thread once presence is confirmed. +- **CODEOWNERS team slugs** — shipped as placeholders on purpose. Guessing real + team names risks silently misrouting review requests, which is a worse outcome + than an admittedly-fake placeholder. +- **Wiring `distribute-defaults.yml` to actually open PRs** — deferred until token + scope is a deliberate governance decision, not a default in a starter kit. + +## Verification steps once merged + +```bash +gh api repos/Quantum-L9/.github/contents/.github/pull_request_template.md +gh api repos/Quantum-L9/.github/contents/.github/CODEOWNERS +gh repo view Quantum-L9/some-other-repo --json securityPolicyUrl +``` + + +--- + +# REVISION — v2.0.0 + +Finding 5 was re-architected after verifying GitHub's inheritance model. + +**What changed**: v1 proposed `distribute-defaults.yml`, a recurring fan-out pushing +files to 29 repos. That was wrong. It would have required standing write access to +the whole org, forever, to solve a problem inheritance and `workflow_call` already +solve with no credentials. + +**Corrected**: community health files inherit live from this repo's `main`. +Workflow logic is consumed by reference via `workflow_call` at `@v1`. Only +`CODEOWNERS` and a 12-line caller are physically copied, once, by +`seed-governance.yml`. + +**Impact on findings 1, 3, 4**: they were never token-blocked. They go live the +moment this merges. + + +--- + +# PATCH — v2.0.1 + +Self-audit of v2.0.0 found three items described in review but absent from the pack. +All three are now shipped. + +1. **Literal tag commands** — `docs/DISTRIBUTION.md` step 4 described moving the + `v1` tag but gave no runnable commands. Added immutable-tag-plus-moving-alias + commands, and the warning to force-move `v1` rather than delete it (deletion + breaks every caller until it reappears). + +2. **`secrets: inherit`** — absent entirely. Called workflows do not receive caller + secrets automatically. Harmless today since both callees use only the automatic + `GITHUB_TOKEN`, but the failure mode is silent (empty string, confusing auth error + at point of use). Now documented in `docs/DISTRIBUTION.md` Appendix A and as a + commented line in `templates/governance-caller.yml`. + +3. **Actions access policy** — the highest-consequence gap. A consumer repo with + Actions disabled, or `allowed_actions: local_only`, fails a cross-repo + `workflow_call` before any step executes, with an error that does not indicate the + cause. Now Appendix B, plus `preflight.sh` section 5, which classifies every repo + as OK / LOCAL_ONLY / DISABLED / allow-list before seeding. + +**Assessment**: items 1 and 2 were documentation debt. Item 3 was a genuine rollout +trap — it would have surfaced as a batch of confusing red X's across seeded repos +with no obvious cause. Caught pre-deploy. diff --git a/docs/DISTRIBUTION.md b/docs/DISTRIBUTION.md new file mode 100644 index 0000000..171b1fb --- /dev/null +++ b/docs/DISTRIBUTION.md @@ -0,0 +1,200 @@ +# Distribution model — what the token actually blocks + +## Short answer + +**No. The missing token does not block pushing the current version of +`Quantum-L9/.github` to org repos — because most of it is never pushed at all.** + +GitHub *inherits* community health files by reference. There is no copy, no sync, +and no token involved. Repos read the latest `main` of `Quantum-L9/.github` live. +Merge a fix to `SECURITY.md` and all 29 repos reflect it on the next page load. + +The token gated exactly two files, and only for a **one-time seed** — not for +ongoing updates. + +## The three distribution mechanisms + +| Mechanism | Files | Token needed? | Update propagation | +| --- | --- | --- | --- | +| **Inheritance** (automatic) | `SECURITY.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md`, `FUNDING.yml`, `pull_request_template.md`, `ISSUE_TEMPLATE/*` | No | Instant, live from `main` | +| **Reference** (`workflow_call`) | governance workflow *logic* | No | Instant, on tag move | +| **Physical copy** (seed) | `CODEOWNERS`, the 12-line caller | **Yes, once** | Never needs re-running | + +### Inheritance covers most of the repo + +Any repo without its own copy falls back to the org default automatically. Caveats: + +- The `.github` repo must be **public** — a private one disables inheritance entirely. +- Files must sit in the repo root, `.github/`, or `docs/`; issue templates must be + in `.github/ISSUE_TEMPLATE/`. +- Override is **all-or-nothing per file**. One local `SECURITY.md` in a repo means + that repo ignores the org default for that file. There is no merging. + +### Reference covers workflow logic + +Workflows are *not* community health files and are never inherited. But they do not +need copying either. `governance-pr.yml` and `governance-issue.yml` live here as +`workflow_call` callees; each repo has a caller pinned to `@v1`. Cross-repo +`workflow_call` requires the callee repo be accessible — public satisfies this, +which the inheritance requirement already forces. + +**Consequence**: to change a governance rule org-wide, edit `governance-pr.yml` +here and move the `v1` tag. All 29 repos pick it up on their next PR. Zero token +use, zero fan-out, zero PRs. + +### Physical copy covers only two files + +Two things genuinely cannot be inherited or referenced: + +1. **`CODEOWNERS`** — GitHub does not list it as a community health file. It must + physically exist in each repo at root, `.github/`, or `docs/`. +2. **The caller stub** — a repo cannot be made to run a workflow it does not contain. + Twelve lines, pinned to a tag, and it should never change again. + +`seed-governance.yml` seeds these once per repo, via PR, and skips repos already +seeded. That is the total scope of what the token was blocking. + +## Corrected assessment of the earlier claim + +The previous statement — "does not open PRs yet, needs a token with write scope +across 29 repos" — was **true but badly framed**. It implied governance updates were +blocked pending a token decision. They are not: + +- Findings 1, 3, 4 (PR template path, SECURITY.md, CONTRIBUTING.md) propagate on + merge with **no token whatsoever**. +- Finding 2 (CODEOWNERS) needs the one-time seed. +- Finding 5's real deliverable was never fan-out — it is the `workflow_call` + indirection that makes fan-out unnecessary. The earlier version proposed + recurring distribution, which was the wrong architecture: it would have required + standing write access to 29 repos forever, to solve a problem that a pinned tag + solves with none. + +## Credential recommendation + +Do **not** use a PAT. Use a **GitHub App** installed on the org, scoped to +`contents: write` and `pull_requests: write`: + +- No human owner; survives offboarding. +- Installation token expires in one hour. +- `create-github-app-token` mints it per run — nothing long-lived in secrets. +- The workflow is `workflow_dispatch`-only and pinned to a protected + `governance-distribution` environment, so a required reviewer approves each run. + +Since seeding runs roughly once ever, App credentials can be uninstalled afterward +and reinstalled only if a new repo needs seeding. + +## Order of operations + +1. `./scripts/preflight.sh` — verify team slugs resolve, `.github` is public, list + which repos already have local overrides blocking inheritance. +2. Fix any placeholder team slug that preflight flags as nonexistent. +3. Merge to `main`. **Findings 1, 3, 4 are live at this point, no token.** +4. Tag `v1` so callers have something to pin: + + ```bash + # immutable release tag + moving alias + git tag v1.0.0 + git push origin v1.0.0 + git tag -f v1 v1.0.0 + git push origin v1 --force + ``` + + Callers pin `@v1`. To ship a governance change later, repeat with a new + immutable tag and re-point the alias: + + ```bash + git tag v1.1.0 && git push origin v1.1.0 + git tag -f v1 v1.1.0 && git push origin v1 --force + ``` + + Never delete `v1`; force-move it. Deleting breaks every caller until it reappears. +5. Install the GitHub App; configure `governance-distribution` environment reviewers. +6. Run `seed-governance.yml` with `mode: dry-run`, `repo_filter: l9-` — inspect the + summary table. +7. Re-run with `mode: seed` on the filtered subset, review those PRs, then drop the + filter. +8. Optionally uninstall the App. + +## Ongoing steady state + +| Change | Action | Token? | +| --- | --- | --- | +| Security policy text | Edit `SECURITY.md`, merge | No | +| PR template structure | Edit `pull_request_template.md`, merge | No | +| A governance gate rule | Edit `governance-pr.yml`, move `v1` tag | No | +| Code ownership routing | Edit `templates/CODEOWNERS.repo`, re-run seed | Yes | +| New repo joins the org | Run seed filtered to that repo | Yes | + +Only the last two rows ever need it. + + +--- + +## Appendix A — `secrets: inherit` + +Both callees currently use only the automatic `GITHUB_TOKEN`, so callers pass +nothing. The token is minted per job with the `permissions:` block declared in the +callee, and expires when the job ends. + +The moment a governance job needs a real secret, that changes. Secrets are **not** +visible to a called workflow automatically — the caller must pass them explicitly: + +```yaml +jobs: + pr: + uses: Quantum-L9/.github/.github/workflows/governance-pr.yml@v1 + secrets: inherit # passes all caller secrets + # or, preferred, be explicit: + # secrets: + # SEMGREP_TOKEN: ${{ secrets.SEMGREP_TOKEN }} +``` + +`inherit` works for callers in the same organization. Prefer named passing where +practical — `inherit` hands the callee every secret the caller can see, which is +broader than least privilege. + +Failure mode if forgotten: the callee sees an empty string rather than an error, so +it fails at the point of use with a confusing auth message rather than at the call. + +## Appendix B — Actions access policy (the rollout trap) + +Cross-repo `workflow_call` is subject to the **consumer** repo's Actions policy, not +just this repo's visibility. Under a repo or org setting of *Allow OWNER actions and +reusable workflows* — or a narrower allow-list — a caller referencing +`Quantum-L9/.github/...@v1` fails before any step runs. + +Because both repos are in the same org and this one is public, the default +*Allow all* and *Allow OWNER* settings both work. It breaks when: + +- Actions are **disabled** entirely on a consumer repo. +- `allowed_actions` is `selected` with an allow-list that omits `Quantum-L9/*`. +- An **enterprise-level** policy overrides the org. Note the settings hierarchy: + enterprise → org → repo. If the setting appears locked at repo level, it is set + at org level; if locked there, it is set at the enterprise level. + +Diagnose per repo: + +```bash +gh api repos/Quantum-L9//actions/permissions +# -> {"enabled": true, "allowed_actions": "all"} OK +# -> {"enabled": false} caller will never run +# -> {"allowed_actions": "selected"} check the allow-list: +gh api repos/Quantum-L9//actions/permissions/selected-actions +``` + +Remediate: + +```bash +gh api -X PUT repos/Quantum-L9//actions/permissions \ + -F enabled=true -f allowed_actions=all +``` + +Or at org level once, which is the higher-leverage fix: + +```bash +gh api -X PUT orgs/Quantum-L9/actions/permissions \ + -f enabled_repositories=all -f allowed_actions=all +``` + +`scripts/preflight.sh` now checks this for every repo (section 5) so the trap is +caught before seeding rather than after. diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..5403223 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Idempotent bootstrap for a Quantum-L9 repo. Replaces manual clone-based setup. +set -euo pipefail +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$REPO_ROOT" + +mkdir -p .github +for f in pull_request_template.md CODEOWNERS SECURITY.md CONTRIBUTING.md; do + if [[ -f ".github/$f" || -f "$f" ]]; then + echo "skip $f (local override present)" + continue + fi + echo "using org default for $f (no local copy needed — inherited from Quantum-L9/.github)" +done + +echo "bootstrap complete. Org defaults are inherited automatically; no files were duplicated." diff --git a/scripts/preflight.sh b/scripts/preflight.sh new file mode 100755 index 0000000..cc34308 --- /dev/null +++ b/scripts/preflight.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Preflight: verify assumptions BEFORE seeding. Read-only, safe to run anytime. +set -euo pipefail +ORG=Quantum-L9 +# gh may colorize JSON output (ANSI escapes) when a terminal/clicolor is forced, +# which silently breaks every grep-based parse below. Disable color and strip +# any residual escapes defensively. +export GH_FORCE_TTY=0 CLICOLOR=0 CLICOLOR_FORCE=0 NO_COLOR=1 +strip_ansi() { sed -e 's/\x1b\[[0-9;]*m//g'; } +command -v gh >/dev/null || { echo "gh CLI required" >&2; exit 1; } + +echo "== 1. CODEOWNERS team slugs must resolve ==" +mapfile -t SLUGS < <(grep -oE '@'"$ORG"'/[a-z0-9-]+' .github/CODEOWNERS | sed "s|@$ORG/||" | sort -u) +REAL=$(gh api "orgs/$ORG/teams" --paginate --jq '.[].slug' 2>/dev/null || true) +[[ -z "$REAL" ]] && REAL="__no_team_read__" # empty list and read-denied are both unverifiable +for s in "${SLUGS[@]}"; do + if [[ "$REAL" == "__no_team_read__" ]]; then echo " ? $s (cannot read teams — need admin:org)" + elif grep -qx "$s" <<<"$REAL"; then echo " OK $s" + else echo " !! $s DOES NOT EXIST — rule will be silently inert"; fi +done + +echo +echo "== 2. .github repo must be public (or nothing inherits) ==" +gh api "repos/$ORG/.github" --jq 'if .private then " !! PRIVATE — inheritance is OFF" else " OK public" end' + +echo +echo "== 3. Which repos already inherit vs need seeding ==" +gh repo list "$ORG" --limit 200 --json name,isArchived,isFork \ + --jq '.[] | select(.isArchived==false and .isFork==false and .name!=".github") | .name' \ +| while read -r r; do + co=$(gh api "repos/$ORG/$r/contents/.github/CODEOWNERS" --silent 2>/dev/null && echo yes || echo no) + wf=$(gh api "repos/$ORG/$r/contents/.github/workflows/governance.yml" --silent 2>/dev/null && echo yes || echo no) + printf ' %-40s CODEOWNERS=%-3s caller=%s\n' "$r" "$co" "$wf" + done + +echo +echo "== 4. Repos with LOCAL overrides that block inheritance ==" +gh repo list "$ORG" --limit 200 --json name --jq '.[].name' | while read -r r; do + for p in .github/pull_request_template.md PULL_REQUEST_TEMPLATE.md SECURITY.md .github/ISSUE_TEMPLATE; do + if gh api "repos/$ORG/$r/contents/$p" --silent 2>/dev/null; then + echo " $r has local $p (org default IGNORED)" + fi + done +done || true # probe misses must not abort the script under set -e + +echo +echo "== 5. Actions policy must permit cross-repo reusable workflows ==" +echo " (a caller referencing Quantum-L9/.github@v1 fails before any step if blocked)" +ORG_POL=$(gh api "orgs/$ORG/actions/permissions" 2>/dev/null | strip_ansi || echo '{}') +echo " org policy: $(echo "$ORG_POL" | tr -d '\n' | cut -c1-160)" +ORG_AA=$(echo "$ORG_POL" | tr -d ' \n' | grep -o '"allowed_actions":"[a-z_]*"' | cut -d'"' -f4 || true) +case "$ORG_AA" in + all) echo " OK org allows all actions and reusable workflows" ;; + local_only) echo " !! org is LOCAL_ONLY — every cross-repo caller will fail org-wide" ;; + selected) echo " ? org uses an allow-list — confirm Quantum-L9/* is included:" + gh api "orgs/$ORG/actions/permissions/selected-actions" 2>/dev/null | tr -d '\n' | sed 's/^/ /' ; echo ;; + *) echo " ? cannot read org policy (need admin:org) — check repo-level below" ;; +esac + +gh repo list "$ORG" --limit 200 --json name,isArchived,isFork \ + --jq '.[] | select(.isArchived==false and .isFork==false and .name!=".github") | .name' \ +| while read -r r; do + P=$(gh api "repos/$ORG/$r/actions/permissions" 2>/dev/null | strip_ansi | tr -d ' \n' || echo '{}') + EN=$(echo "$P" | grep -o '"enabled":[a-z]*' | cut -d: -f2 || true) + AA=$(echo "$P" | grep -o '"allowed_actions":"[a-z_]*"' | cut -d'"' -f4 || true) + case "$EN:$AA" in + true:all) printf ' OK %-40s actions=on allowed=all\n' "$r" ;; + true:local_only) printf ' !! %-40s LOCAL_ONLY — caller will fail\n' "$r" ;; + true:selected) printf ' ? %-40s allow-list — verify Quantum-L9/* included\n' "$r" ;; + false:*) printf ' !! %-40s ACTIONS DISABLED — caller will never run\n' "$r" ;; + true:) printf ' OK %-40s actions=on (inherits org policy)\n' "$r" ;; + *) printf ' ? %-40s unreadable (need admin)\n' "$r" ;; + esac + done || true # per-repo probe misses must not abort under set -e + +echo +echo "Remediation for a blocked repo:" +echo " gh api -X PUT repos/$ORG//actions/permissions -F enabled=true -f allowed_actions=all" +echo "Or once at org level (higher leverage):" +echo " gh api -X PUT orgs/$ORG/actions/permissions -f enabled_repositories=all -f allowed_actions=all" +echo +echo "NOTE: settings hierarchy is enterprise > org > repo. If a setting looks locked" +echo " at repo level it is set at org level; if locked there, at the enterprise." +echo +echo "preflight complete — no changes were made." diff --git a/templates/CODEOWNERS.repo b/templates/CODEOWNERS.repo new file mode 100644 index 0000000..f62a6a3 --- /dev/null +++ b/templates/CODEOWNERS.repo @@ -0,0 +1,18 @@ +# Managed by Quantum-L9/.github — do not edit here. +# Edit templates/CODEOWNERS.repo in the .github repo; changes are re-seeded by PR. +# CODEOWNERS is not inheritable, so this file is physically copied per repo. +# +# Team slugs verified against the live org (gh api orgs/Quantum-L9/teams): +# only `platform` exists. The pack's placeholder slugs (maintainers, governance, +# infra, ci-cd, security) were replaced — an unresolvable owner makes the whole +# rule silently inert. If new teams are created later, add rules here and re-seed. + +* @Quantum-L9/platform + +# Blast-radius paths require cryptoxdog as additional reviewer. +/.github/ @Quantum-L9/platform @cryptoxdog +/.github/workflows/ @Quantum-L9/platform @cryptoxdog +/infra/ @Quantum-L9/platform @cryptoxdog +/terraform/ @Quantum-L9/platform @cryptoxdog +SECURITY.md @Quantum-L9/platform @cryptoxdog +CODEOWNERS @Quantum-L9/platform @cryptoxdog diff --git a/templates/governance-caller.yml b/templates/governance-caller.yml new file mode 100644 index 0000000..070aae5 --- /dev/null +++ b/templates/governance-caller.yml @@ -0,0 +1,33 @@ +# Managed by Quantum-L9/.github. Pinned to a tag so main can move safely. +# +# This file should never need editing again. Governance logic lives in +# Quantum-L9/.github/.github/workflows/governance-*.yml; shipping a change means +# force-moving the v1 tag there, not touching this file. +# +# SECRETS: a called workflow does NOT inherit caller secrets automatically. Today +# both callees use only the automatic GITHUB_TOKEN, so nothing is passed. If a +# governance job later needs a real secret, add `secrets:` to the job below — +# `inherit` for all, or named entries (preferred, least privilege). +# See docs/DISTRIBUTION.md Appendix A. +# +# ACCESS: if this caller fails before any step runs, check the Actions policy on +# THIS repo — see Appendix B. `gh api repos/OWNER/REPO/actions/permissions` + +name: Governance + +on: + pull_request: + types: [opened, edited, synchronize, reopened, ready_for_review] + issues: + types: [opened, edited, reopened] + +jobs: + pr: + if: github.event_name == 'pull_request' + uses: Quantum-L9/.github/.github/workflows/governance-pr.yml@v1 + # secrets: inherit + + issue: + if: github.event_name == 'issues' + uses: Quantum-L9/.github/.github/workflows/governance-issue.yml@v1 + # secrets: inherit