From 5d4314d9e3936d92d09d17a3d2b52adf89f6f839 Mon Sep 17 00:00:00 2001 From: Smarter Harder <33955773+NWarila@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:46:36 -0400 Subject: [PATCH] docs: add pt-m1 documentation layout --- PLAN.md | 4 +- docs/README.md | 23 ++ docs/decision-records/README.md | 31 ++ docs/decision-records/org/.gitkeep | 0 .../0001-use-architecture-decision-records.md | 299 ++++++++++++++++++ ...-adopt-diataxis-documentation-framework.md | 213 +++++++++++++ .../0003-use-deny-all-gitignore-strategy.md | 202 ++++++++++++ ...004-use-renovate-for-dependency-updates.md | 235 ++++++++++++++ ...terraform-and-provider-versions-exactly.md | 179 +++++++++++ ...p-github-control-planes-namespace-local.md | 155 +++++++++ ...rsal-ci-reusables-within-each-namespace.md | 156 +++++++++ .../0008-enforce-repo-hygiene-by-repo-type.md | 146 +++++++++ ...lassify-baseline-manifest-byte-identity.md | 157 +++++++++ ...p-ai-attribution-out-of-version-control.md | 159 ++++++++++ docs/decision-records/repo/.gitkeep | 0 docs/decision-records/template/.gitkeep | 0 ...-scripts-are-standalone-and-stdlib-only.md | 0 ...ull-based-manifest-driven-template-sync.md | 0 docs/explanation/why-two-layer-pull-sync.md | 52 +++ docs/how-to/add-a-qa-check.md | 57 ++++ .../github-token-limitation.md} | 0 docs/reference/qa-contract.md | 72 +++++ docs/runbooks/refresh-released-scripts.md | 62 ++++ docs/tutorials/getting-started.md | 86 +++++ pyproject.toml | 2 +- 25 files changed, 2287 insertions(+), 3 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/decision-records/README.md create mode 100644 docs/decision-records/org/.gitkeep create mode 100644 docs/decision-records/org/0001-use-architecture-decision-records.md create mode 100644 docs/decision-records/org/0002-adopt-diataxis-documentation-framework.md create mode 100644 docs/decision-records/org/0003-use-deny-all-gitignore-strategy.md create mode 100644 docs/decision-records/org/0004-use-renovate-for-dependency-updates.md create mode 100644 docs/decision-records/org/0005-pin-terraform-and-provider-versions-exactly.md create mode 100644 docs/decision-records/org/0006-keep-github-control-planes-namespace-local.md create mode 100644 docs/decision-records/org/0007-centralize-universal-ci-reusables-within-each-namespace.md create mode 100644 docs/decision-records/org/0008-enforce-repo-hygiene-by-repo-type.md create mode 100644 docs/decision-records/org/0009-classify-baseline-manifest-byte-identity.md create mode 100644 docs/decision-records/org/0010-keep-ai-attribution-out-of-version-control.md create mode 100644 docs/decision-records/repo/.gitkeep create mode 100644 docs/decision-records/template/.gitkeep rename docs/decision-records/{ => template}/0001-scripts-are-standalone-and-stdlib-only.md (100%) rename docs/decision-records/{ => template}/0002-pull-based-manifest-driven-template-sync.md (100%) create mode 100644 docs/explanation/why-two-layer-pull-sync.md create mode 100644 docs/how-to/add-a-qa-check.md rename docs/{GITHUB_TOKEN_LIMITATION.md => reference/github-token-limitation.md} (100%) create mode 100644 docs/reference/qa-contract.md create mode 100644 docs/runbooks/refresh-released-scripts.md create mode 100644 docs/tutorials/getting-started.md diff --git a/PLAN.md b/PLAN.md index 8093839..9e8621c 100644 --- a/PLAN.md +++ b/PLAN.md @@ -548,7 +548,7 @@ Exit criteria: `[build-system]` sections) that each script will duplicate independently — no shared module - [ ] Remove every `.github/scripts` path assumption from the scripts -- [ ] Remove every resume-specific path, package name, and CLI assumption +- [x] Remove every resume-specific path, package name, and CLI assumption - [ ] Standardize script CLI contracts (`--fix`, `--paths`, `--skip`, config lookup, clean exit codes) - [ ] Make `qa.py` auto-discover `check_*.py` scripts and honor repo profile @@ -595,7 +595,7 @@ Exit criteria: sections (setup, QA) and a clearly marked repo-specific region - [ ] Replace `reference/settings.json` and `reference/extensions.json` with Python-generic defaults -- [ ] Strip resume-specific content from `.gitignore`, `.gitattributes`, and +- [x] Strip resume-specific content from `.gitignore`, `.gitattributes`, and workflow examples - [ ] Remove `reference/build-resume.yml`, `reference/build-resumes-action.yml`, and `reference/release.yml` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..f1acf8e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,23 @@ +# Documentation + +Documentation for this template follows the [Diataxis framework](https://diataxis.fr/). + +| Area | Path | Purpose | +| --- | --- | --- | +| Tutorials | `tutorials/` | Learning-oriented, start-to-finish walkthroughs | +| How-to | `how-to/` | Task-oriented guides for a specific goal | +| Reference | `reference/` | Lookup-oriented facts about the QA and sync contract | +| Explanation | `explanation/` | Background and rationale behind the architecture | +| Runbooks | `runbooks/` | Operator procedures for template maintenance | +| Decisions | `decision-records/` | Architecture Decision Records | +| Diagrams | `diagrams/` | Mermaid (`.mmd`) source for architecture diagrams | + +## Start here + +- New to the template? Read [tutorials/getting-started.md](tutorials/getting-started.md). +- Adding or changing a quality gate? Use [how-to/add-a-qa-check.md](how-to/add-a-qa-check.md). +- Need the exact script and CI contract? See [reference/qa-contract.md](reference/qa-contract.md). +- Want the architecture rationale? Read + [explanation/why-two-layer-pull-sync.md](explanation/why-two-layer-pull-sync.md). +- Want the flow diagram? See [diagrams/qa-template-sync-flow.mmd](diagrams/qa-template-sync-flow.mmd). +- Looking for decisions? Start with [decision-records/README.md](decision-records/README.md). diff --git a/docs/decision-records/README.md b/docs/decision-records/README.md new file mode 100644 index 0000000..a6c6419 --- /dev/null +++ b/docs/decision-records/README.md @@ -0,0 +1,31 @@ +# Decision Records + +This index separates inherited organization decisions, Python-template +decisions, and repository-local decisions. Org records are byte-identical +mirrors from `NWarila/.github`; template records are owned by this repository. + +## Org-Mirrored ADRs + +| ADR | Status | Summary | +| --- | --- | --- | +| [ADR-0001](org/0001-use-architecture-decision-records.md) | Accepted | Use ADRs to document design rationale. | +| [ADR-0002](org/0002-adopt-diataxis-documentation-framework.md) | Accepted | Organize non-ADR docs with Diataxis. | +| [ADR-0003](org/0003-use-deny-all-gitignore-strategy.md) | Accepted | Use deny-all `.gitignore` baselines. | +| [ADR-0004](org/0004-use-renovate-for-dependency-updates.md) | Accepted | Use Renovate for dependency updates. | +| [ADR-0005](org/0005-pin-terraform-and-provider-versions-exactly.md) | Accepted | Pin Terraform and provider versions exactly. | +| [ADR-0006](org/0006-keep-github-control-planes-namespace-local.md) | Accepted | Keep GitHub control planes namespace-local. | +| [ADR-0007](org/0007-centralize-universal-ci-reusables-within-each-namespace.md) | Accepted | Centralize universal CI reusables per namespace. | +| [ADR-0008](org/0008-enforce-repo-hygiene-by-repo-type.md) | Accepted | Enforce repo hygiene according to repository type. | +| [ADR-0009](org/0009-classify-baseline-manifest-byte-identity.md) | Accepted | Use byte identity only for true shared baselines. | +| [ADR-0010](org/0010-keep-ai-attribution-out-of-version-control.md) | Accepted | Keep tool attribution residue out of version control. | + +## Template ADRs + +| ADR | Status | Summary | +| --- | --- | --- | +| [ADR-0001](template/0001-scripts-are-standalone-and-stdlib-only.md) | Accepted | Keep QA scripts standalone and stdlib-only. | +| [ADR-0002](template/0002-pull-based-manifest-driven-template-sync.md) | Accepted | Sync template updates through pull-based manifest PRs. | + +## Repo ADRs + +No repository-specific ADRs yet. diff --git a/docs/decision-records/org/.gitkeep b/docs/decision-records/org/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/decision-records/org/0001-use-architecture-decision-records.md b/docs/decision-records/org/0001-use-architecture-decision-records.md new file mode 100644 index 0000000..5fe0038 --- /dev/null +++ b/docs/decision-records/org/0001-use-architecture-decision-records.md @@ -0,0 +1,299 @@ + + +# ADR-0001: Use Architecture Decision Records to Document Design Rationale + +| Field | Value | +| ---------------- | --------------------------------------------------------------------------- | +| ID | ADR-0001 | +| Scope | Org baseline | +| Status | Accepted | +| Decision-subject | ADR scope, format, lifecycle, and maintenance rules for decision records. | +| Date accepted | 2026-04-22 | +| Date | 2026-06-02 | +| Last reviewed | 2026-06-02 | +| Authors | Nick Warila (@NWarila) | +| Decision-makers | Nick Warila (sole portfolio maintainer) | +| Consulted | None. | +| Informed | None. | +| Reversibility | Medium | +| Review-by | 2026-11-29 | + +## TL;DR + +We will use `docs/decision-records/` as the conventional home for architecturally significant decisions across the `NWarila` organization. ADRs are organized into three scopes: **org-baseline** ADRs whose master copies live in this `NWarila/.github` repository at `docs/decision-records/` and are mirrored into every adopting child repository at `docs/decision-records/org/`; **type-template** ADRs whose master copies live in a type-template repository (for example `NWarila/terraform-runner-template` for Terraform consumers) at `docs/decision-records/template/` and are mirrored into every consumer of that template at `docs/decision-records/template/`; and **repository-specific** ADRs that live only in their owning repository at `docs/decision-records/repo/`. The format is MADR 4.0-aligned but uses a visible Markdown metadata table, adds explicit reversibility, implementing-PR links, `Last reviewed`, an append-only Changelog, and a conservative compliance-notes crosswalk, and uses the more readable `decision-records` directory name in place of MADR's conventional `adr/`. Accepted ADRs are living records when the same decision subject evolves: update the record in place, preserve auditability in the Changelog, and reserve supersession or obsolescence for decisions whose subject has been replaced or is no longer applicable. This gives the organization a single source of truth for org-level governance that travels alongside the code in every adopting repository, a per-stack source of truth that travels alongside every consumer of a given type-template, and a place for each repository to record its own architectural choices without conflicting with either shared baseline. + +## Context and Problem Statement + +Every nontrivial repository accumulates architectural choices. Why is authentication handled one way and not another? Why was one platform or workflow chosen over another? Why does CI enforce one supply-chain posture instead of a weaker one? A year later, the code can still show *what* exists, but it rarely explains *why* it exists. + +This `NWarila/.github` repository is the canonical home for org-level governance artifacts. ADRs that establish org-wide conventions (this one, the documentation framework, the source-control hygiene policy, etc.) are authored here and replicated into every adopting child repository. In addition to the org baseline, each repository may inherit decisions from a **type-template** — a per-stack template repository (for example `NWarila/terraform-runner-template` for Terraform consumers, or `NWarila/packer-framework-template` for Packer consumers) whose ADRs apply to every consumer of that stack but not to consumers of other stacks. Type-template ADRs cover decisions that are too specific for the org baseline (because they only matter to that stack) but too widely applicable for any single repository (because they recur across every consumer of the template). Repositories that participate inherit the org baseline by syncing the mirrored copies into their own `docs/decision-records/org/` directory, mirror their type-template's decisions into `docs/decision-records/template/`, and may add their own repository-scoped ADRs at `docs/decision-records/repo/` for decisions that affect only that repository. + +Three audiences matter here: + +1. **Future maintainers**, trying to determine whether a past decision still makes sense. +2. **Prospective collaborators, students, and hiring managers**, trying to understand the quality of judgment behind the work. +3. **Reviewers and auditors**, who may need source-controlled rationale for security-relevant or compliance-relevant design choices. + +A wiki, a Notion page, a README section, or a folder of ad hoc design notes fails at least one of those audiences. External tools drift from code, READMEs get crowded with user-facing content, and loosely managed documents often disappear or become misleading as ownership changes. + +Architecture Decision Records (ADRs) solve this well: they are lightweight, source-controlled, and widely understood. Michael Nygard introduced the pattern in 2011. ThoughtWorks later recommended lightweight ADRs in source control instead of a wiki or website, and MADR 4.0.0, released on 2024-09-17, provides a well-known community template that is easy to adapt. MADR does not require accepted records to be immutable; this portfolio deliberately uses a living ADR model with explicit changelog evidence instead of silently treating git history as the whole audit trail. + +The remaining question is not whether to keep a decision log. It is which format to use, how much structure to require, and where those records should live. + +## Decision Drivers + +The following forces shaped this decision. Subsequent ADRs in repositories that adopt this baseline should name the drivers relevant to their own scope in this same section: + +1. **Reader-first clarity.** A reader without deep software-architecture vocabulary should be able to open any ADR and follow the reasoning. +2. **Portfolio-grade professionalism.** The format should read as serious and disciplined without becoming performatively bureaucratic. +3. **Clone-and-use friendliness.** Another developer should be able to fork a repository and use its ADR structure immediately, with no proprietary tools or dashboards. +4. **Traceability.** Readers should be able to connect a decision to the code and pull requests that put it into effect. +5. **Compliance support without overclaim.** Security-relevant ADRs should make it easier to assemble review evidence without pretending that the document alone proves compliance. +6. **Durability.** The format should remain readable even if tools, vendors, or hosting platforms change. +7. **Low authoring friction.** If the format is painful to write, it will not be used consistently. + +## Considered Options + +1. **No formal decision documentation.** Rely on the README, git history, and commit messages. +2. **Wiki-hosted decision log.** Use GitHub Wiki, Confluence, Notion, or similar. +3. **Canonical Nygard ADRs.** Use the classic five-section ADR format. +4. **Vanilla MADR 4.0.** Use the community standard with its default structure and conventions. +5. **Custom MADR 4.0-aligned format with portfolio-specific extensions.** Keep MADR's core shape, but use a readable `decision-records` directory name, a visible Markdown metadata table, and add reversibility, implementing-PR traceability, and compliance notes. + +## Decision Outcome + +Chosen option: **Option 5, a MADR 4.0-aligned Markdown template with small portfolio-specific extensions.** + +ADRs are organized into three scopes with independent four-digit numbering namespaces: + +- **Org-baseline ADRs** capture decisions that apply to the entire `NWarila` organization, regardless of stack. Their master copies live in this `NWarila/.github` repository at `docs/decision-records/NNNN-short-kebab-title.md`. Every adopting child repository mirrors them at `docs/decision-records/org/NNNN-short-kebab-title.md` (identical content, copied byte-for-byte from the master). Numbers in the org namespace are allocated monotonically and never reused. + +- **Type-template ADRs** capture decisions that apply to every repository derived from a particular type-template — for example, every Terraform consumer derived from `NWarila/terraform-runner-template`, or every Packer consumer derived from `NWarila/packer-framework-template`. They are the right home for decisions that are too stack-specific for the org baseline (because they only matter to that stack) and too widely applicable for any single repository (because they recur across every consumer of the template). Their master copies live in the type-template at `docs/decision-records/template/NNNN-short-kebab-title.md`. Every consumer of that template mirrors them at `docs/decision-records/template/NNNN-short-kebab-title.md` (identical content, copied byte-for-byte from the master). Numbers in a type-template's namespace are allocated monotonically per template and are independent of the org namespace and of every other type-template's namespace. + +- **Repository-specific ADRs** capture decisions that apply only to one repository. They live in that repository at `docs/decision-records/repo/NNNN-short-kebab-title.md` and are not mirrored to any other repository, to the org `.github` repo, or to any type-template. Numbers in a repository's `repo` namespace are independent of the org and template namespaces; the same number can appear in `org/`, `template/`, and `repo/` without conflict because they are in different directories. + +Within all three scopes, `NNNN` is the next unused four-digit number in that scope's own namespace, allocated monotonically and never reused. The directory name is `decision-records` because it is immediately understandable to readers who do not already know the acronym. The subdirectory split (`org/`, `template/`, `repo/`) keeps the three scopes visually and structurally distinct, so a reader scanning a repository can immediately see which decisions were inherited from the organization, which were inherited from the repository's type-template, and which were made locally. + +ADRs follow the structure demonstrated by this file itself, in this order: metadata table, TL;DR, Context and Problem Statement, Decision Drivers, Considered Options, Decision Outcome, Pros and Cons of the Options, Confirmation, Consequences (Positive / Negative / Neutral), Assumptions, Supersedes, Superseded by, Implementing PRs, Related ADRs, Compliance Notes, and Changelog. Sections that genuinely do not apply are kept and filled with "None." or "N/A (reason)." so readers can distinguish "not applicable" from "forgotten." + +The metadata table records `ID`, `Scope`, `Status`, `Decision-subject`, `Date accepted`, `Date`, `Last reviewed`, `Authors`, `Decision-makers`, `Consulted`, `Informed`, `Reversibility`, and `Review-by`. `Date accepted` is immutable once the ADR is accepted. `Date` follows MADR's "last updated" meaning. `Last reviewed` is the most recent explicit review date and is refreshed on the cadence for the ADR scope: 180 days for org-baseline and type-template ADRs, and 365 days for repository-specific ADRs. + +A decision is **architecturally significant** and warrants an ADR when any of the following are true: + +- It has multiple serious alternatives with nontrivial trade-offs. +- It shapes how future work in the repository will be done, not just one implementation task. +- It materially affects security, compliance posture, or supply-chain posture. +- A reader six months from now would reasonably ask "why did we choose X over Y?" and the answer will not be obvious from the code alone. + +Decisions that are **not** ADR-worthy include forced choices with no practical alternatives, style-level preferences with negligible downstream impact, runbook procedures, and single-PR implementation details. + +Accepted ADRs are living records when the same `Decision-subject` evolves. A pull request may update the decision, scope, rationale, consequences, or review metadata in place, but every substantive change MUST add a new Changelog row that says what changed, why, who or what role made the change, and whether the ADR body changed. When the active decision text changes, the prior text MUST remain recoverable in the Changelog row or in a `Previous decisions` subsection. A `Last reviewed` bump with no body change still requires an explicit re-review row. Supersession is reserved for a different-subject ADR that replaces this one; obsolescence is reserved for a subject that is no longer applicable and has no replacement. + +This ADR is the canonical example for this baseline and the starting point for participating repositories in the portfolio. When another repository seeds its own `ADR-0001` from this file, it must rewrite the metadata, context, consequences, and compliance notes so the record is true for that repository. + +## Pros and Cons of the Options + +### Option 1: No formal decision documentation + +- **Good, because** it has effectively zero authoring overhead. +- **Good, because** it requires no new conventions or templates. +- **Bad, because** reasoning behind important choices is quickly lost. +- **Bad, because** it leaves reviewers and future maintainers to reverse-engineer intent from code and commit messages. +- **Bad, because** it provides no durable source-controlled artifact for security-relevant design rationale. + +### Option 2: Wiki-hosted decision log + +- **Good, because** wikis are easy to edit and cross-link in a browser. +- **Good, because** they support long-form documentation well. +- **Bad, because** wiki or website content can drift away from the code it describes; ThoughtWorks explicitly recommends source control instead. +- **Bad, because** GitHub wikis are stored and cloned separately from the main repository, which weakens the "docs travel with the code" property. +- **Bad, because** externally hosted tools create additional access, lifecycle, and vendor dependencies. + +### Option 3: Canonical Nygard ADRs + +- **Good, because** the format is widely recognized and easy to explain. +- **Good, because** its five-section shape is approachable for non-specialist readers. +- **Neutral, because** its minimalism is a strength until stronger traceability or reviewability is needed. +- **Bad, because** it does not natively prompt explicit decision drivers, option-by-option trade-off analysis, or confirmation criteria. +- **Bad, because** reversibility, implementing-PR traceability, and compliance notes would all need to be reinvented locally as ad hoc extensions. + +### Option 4: Vanilla MADR 4.0 + +- **Good, because** MADR 4.0.0 is a well-known and maintained community standard. +- **Good, because** Decision Drivers, Considered Options, option-level pros and cons, and Confirmation add rigor beyond the classic Nygard structure. +- **Good, because** YAML front matter and existing tooling make machine processing feasible. +- **Neutral, because** YAML front matter is slightly less approachable to some readers than a visible Markdown metadata table. +- **Bad, because** the default conventions do not capture this portfolio's preference for the clearer `decision-records` directory name. +- **Bad, because** reversibility, implementing PRs, and conservative compliance mapping still need local conventions. + +### Option 5: Custom MADR 4.0-aligned format with portfolio-specific extensions (chosen) + +- **Good, because** it stays close to MADR 4.0 while remaining easy to read in plain GitHub Markdown. +- **Good, because** the explicit `decision-records` directory name is clearer for first-time readers. +- **Good, because** a visible Markdown metadata table keeps key governance facts readable in rendered and raw form. +- **Good, because** an explicit **Reversibility** field encourages better judgment about how expensive it will be to change course later. +- **Good, because** **Implementing PRs** and supersession links improve traceability between rationale and code. +- **Good, because** **Compliance Notes** creates a place to record how a decision may support external review frameworks without claiming that the ADR alone proves compliance. +- **Neutral, because** the additional fields only need short entries when a decision is simple. +- **Bad, because** any future automation must target this exact schema rather than stock MADR. +- **Bad, because** readers familiar with vanilla MADR may need a brief orientation to the differences. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Org-baseline mirror check.** A child repository that adopts this baseline MUST contain `docs/decision-records/org/` populated with byte-identical copies of every accepted org-baseline ADR from `NWarila/.github/docs/decision-records/`. A CI job or `pre-commit` hook MAY fail a pull request that adds an `org/` file that does not match the master, removes a master that still exists upstream, or omits a master that has been added upstream. +2. **Type-template mirror check.** A child repository derived from a type-template MUST contain `docs/decision-records/template/` populated with byte-identical copies of every accepted type-template ADR from that template's `docs/decision-records/template/`. The same CI mechanism that enforces the org-baseline mirror SHOULD enforce the template-baseline mirror, run once per type-template the repository derives from. +3. **Layout-skeleton check.** Every adopting child repository MUST contain a complete decision-records directory skeleton — `docs/decision-records/org/`, `docs/decision-records/template/`, and `docs/decision-records/repo/` — even when some scopes contain no ADRs yet. Empty scopes are kept in source control via byte-identical `.gitkeep` placeholders mirrored from the org canonical, so a reader scanning any repo immediately sees the same predictable layout regardless of which scopes are populated. *Content* in each scope remains opt-in per scope (a repo with no repository-specific decisions has an empty `repo/`; a repo that does not derive from a type-template has an empty `template/`); only the *layout* is mandatory. +4. **Repo-scope check.** Repository-specific ADRs MUST live at `docs/decision-records/repo/NNNN-short-kebab-title.md`. They MUST NOT appear in `docs/decision-records/org/` or `docs/decision-records/template/`, and MUST NOT be promoted to either namespace without first being authored as a new ADR in `NWarila/.github` (for the org baseline) or in the appropriate type-template (for a template baseline). A CI script MAY assert this directory split. +5. **Schema check.** A CI script SHOULD verify that every file matching `docs/decision-records/{org,template,repo}/[0-9][0-9][0-9][0-9]-*.md` contains the required section headings from this template: `## TL;DR`, `## Context and Problem Statement`, `## Decision Drivers`, `## Considered Options`, `## Decision Outcome`, `## Pros and Cons of the Options`, `## Confirmation`, `## Consequences`, `## Assumptions`, `## Supersedes`, `## Superseded by`, `## Implementing PRs`, `## Related ADRs`, `## Compliance Notes`, and `## Changelog`. `## Considered Options` and `## Pros and Cons of the Options` are especially important because they preserve rejected alternatives and trade-offs. +6. **Index check.** A repository that has any ADRs MUST contain `docs/decision-records/README.md` listing every ADR (org-mirrored, template-mirrored, and repo-specific, in clearly separated sections) with its current Status and Summary. A CI script SHOULD diff the directory listing against the index and fail on drift. +7. **Human review.** Every pull request that introduces a new ADR MUST be reviewed. Every pull request that materially contradicts an Accepted ADR SHOULD either update the code to comply, supersede the ADR, or explain why the ADR never actually applied to the change in question. +8. **Living-edit rule.** After acceptance, edits MAY change the decision, scope, rationale, consequences, Status, review metadata, supersession fields, or `Implementing PRs` only when the change is explicit in the pull request and recorded in a new Changelog row. Silent changes to the decision body are not allowed. +9. **Changelog check.** CI SHOULD reject a changed ADR when an ADR body diff has no new Changelog row, a `Last reviewed` value advances without either a body diff or an explicit re-review row, a Changelog row was removed or modified instead of appended, a `Superseded` status lacks a resolvable `Superseded by` link, or a supersession link is not reciprocal. + +Enforcement tooling is recommended but not mandatory at acceptance time. A solo-maintainer repository MAY rely on manual discipline; a team repository or a compliance-critical repository SHOULD automate at least the presence, schema, index, and living-edit checks. + +## Consequences + +### Positive + +- Decisions are explained, findable, and version-controlled alongside the code they govern. +- The explicit `decision-records` directory name is easier for first-time readers to understand. +- Reviewers, contributors, and hiring audiences can reconstruct the reasoning behind important architectural choices without a synchronous conversation. +- Security-relevant ADRs can contribute reusable evidence for reviews, assessments, and compliance preparation. +- The format is self-documenting: this ADR both adopts the format and demonstrates how to use it. +- Living ADRs keep active decisions current without forcing readers to chase a chain of same-subject replacement records. +- The Changelog is the primary audit trail for ADR edits and survives squash merges, mirrors, and automation-authored sync commits better than git history alone. + +### Negative + +- Every architecturally significant change now carries some documentation overhead. +- The format is custom enough that future automation and linting will likely need repository-specific support. +- If an adopting repository copies this ADR mechanically instead of rewriting repository-specific content, it can create a polished but false record. +- Without enforcement tooling, ADRs can still drift from the code they describe. +- Living updates require reviewer discipline; without a meaningful Changelog row, they can hide the same decision drift they are meant to prevent. + +### Neutral + +- The directory layout `docs/decision-records/{org,template,repo}/` and the filename pattern `NNNN-short-kebab-title.md` are established conventions for the organization. +- Each adopting repository's `docs/decision-records/README.md` becomes the canonical local index for its full ADR set (org-mirrored, template-mirrored, and repo-specific) and is the single page to read to understand the repository's decision posture. +- Future repositories may introduce carefully scoped local extensions, but those should be documented in their own repo-specific ADRs (under `docs/decision-records/repo/`) rather than by silently mutating this baseline or any type-template baseline. +- Org-baseline ADRs and type-template ADRs are duplicated content (masters in their respective canonical repositories; mirrors in every adopting child repo). The duplication is deliberate — it keeps governance content traveling with the code that implements it — but it means amendments to either upstream require a coordinated update across all adopting repositories. +- Git history corroborates ADR evolution, but the in-document Changelog is the reader-facing record of what changed and why. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. GitHub, or an equivalent Git-hosting service, remains the primary home for source control in repositories that adopt this baseline. +2. Markdown remains a widely supported, human-readable plain-text format. +3. Participating repositories continue to benefit from keeping governance artifacts near the code instead of in a separate knowledge base. +4. Repositories using this template have a clear decision-making path. In a solo-maintainer repository that may be the maintainer; in a team repository it may be a designated approver or architecture owner. +5. Pull requests that change ADRs have enough base-branch context for CI to detect whether a body diff or Changelog diff occurred. + +## Supersedes + +None. This is the inaugural ADR. + +## Superseded by + +None (current). + +## Implementing PRs + +This section lists downstream pull requests that implement or operationalize the decision described in this ADR. It does not need to list the pull request that introduced the ADR itself; that is already discoverable from version control. This is one of the few sections expected to gain entries after acceptance. + +None yet. The primary expected follow-ons for this ADR are enforcement checks for presence, schema, and index drift, plus propagation of the structure into participating repositories that adopt this baseline. + +## Related ADRs + +None at this time. Subsequent ADRs that depend on this one, refine this format, or add repository-specific extensions should back-link here in their own `Related ADRs` sections. + +## Compliance Notes + +This ADR establishes a documentation mechanism, not a deployed security control. Its value is evidentiary: later ADRs can capture rationale, alternatives, and security trade-offs in a form that is reusable during reviews. The table below indicates where such evidence may help; it is illustrative rather than exhaustive, and it is not a claim that a repository is compliant merely because ADRs exist. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| NIST SP 800-53 Rev. 5 | SA-17 (Developer Security and Privacy Architecture and Design) | ADRs can document the architecture and design rationale considered during development. | +| NIST SP 800-53 Rev. 5 | PL-8 (Security and Privacy Architectures) | ADRs can support architecture narratives and link design choices to source-controlled artifacts. | +| NIST SP 800-53 Rev. 5 | SA-8 (Security and Privacy Engineering Principles) | Security-relevant ADRs can record how engineering principles shaped specific choices. | +| NIST SP 800-218 (SSDF) | PW.1 (Design Software to Meet Security Requirements and Mitigate Security Risks) | Security-focused ADRs can record identified risks, planned mitigations, and why a requirement was accepted, relaxed, or judged out of scope. | +| FedRAMP SSP artifacts | System-description and architecture narratives | ADRs can provide reusable source material and traceability for SSP drafting, but they do not replace the SSP or the assessment evidence set. | + +Subsequent ADRs should keep only the rows that genuinely apply to the decision at hand and should describe the relationship conservatively. + +## Changelog + +| Date | Change | Reason | Author/Role | Body-diff? | +| ---------- | ------------------------------------------- | --------------------------------------------------------- | --------------------------------- | ---------- | +| 2026-06-02 | Adopted the living ADR lifecycle guardrails. | Keep accepted ADRs current while preserving auditability. | Portfolio maintainer / governance | Yes | diff --git a/docs/decision-records/org/0002-adopt-diataxis-documentation-framework.md b/docs/decision-records/org/0002-adopt-diataxis-documentation-framework.md new file mode 100644 index 0000000..040c69b --- /dev/null +++ b/docs/decision-records/org/0002-adopt-diataxis-documentation-framework.md @@ -0,0 +1,213 @@ +# ADR-0002: Adopt Diátaxis as the Documentation Framework + +| Field | Value | +| ---------------- | ------------------------------------------------------------------------- | +| ID | ADR-0002 | +| Scope | Org baseline | +| Status | Accepted | +| Decision-subject | Non-ADR documentation layout and supported documentation genres. | +| Date accepted | 2026-04-24 | +| Date | 2026-06-02 | +| Last reviewed | 2026-06-02 | +| Authors | Nick Warila (@NWarila) | +| Decision-makers | Nick Warila (sole portfolio maintainer) | +| Consulted | Documentation-governance migration findings. | +| Informed | Maintainers of repositories adopting the org documentation baseline. | +| Reversibility | Medium | +| Review-by | 2026-11-29 | + +## TL;DR + +We will use the [Diátaxis](https://diataxis.fr) documentation framework for all non-ADR documentation in repositories that adopt this baseline. Each adopting repository organizes long-form documentation into the four Diátaxis quadrants — **tutorials**, **how-to guides**, **reference**, and **explanation** — under a `docs/` directory whose immediate subdirectories mirror those quadrant names. The baseline also recognizes two top-level adjunct genres that Diátaxis does not cleanly house on its own: `docs/diagrams/` for diagram source files and `docs/runbooks/` for operator or break-glass procedures. ADRs themselves remain governed by [ADR-0001](0001-use-architecture-decision-records.md) and live in their own subtree at `docs/decision-records/{org,template,repo}/` (org-mirrored, template-mirrored, or repository-specific). This gives every repository a consistent, reader-first information architecture that is easy to navigate, easy to maintain, and easy for new contributors to understand. + +## Context and Problem Statement + +[ADR-0001](0001-use-architecture-decision-records.md) established a format for *decisions*, but a repository's documentation surface is much larger than its decision log. Repositories accumulate setup procedures, permission matrices, troubleshooting guides, conceptual explainers, contributor onboarding material, and operational runbooks. Without a shared organizing principle, that material lands wherever the original author thought to put it — a `README.md` section, a long monolithic design document, a folder of unstructured Markdown files, or worse, an out-of-band tool. + +Three failure modes consistently follow from ad-hoc documentation organization: + +1. **Drift.** Reference material, procedural material, and conceptual material accumulate in the same file. Updates to one type silently invalidate the others, and readers cannot tell which sentences are authoritative reference and which are narrative explanation. +2. **Findability collapse.** A reader who needs a specific permission, a specific command, or a specific definition cannot predict where to look. Repositories develop "you have to know to look in `DESIGN.md` §15.2" tribal knowledge. +3. **Authoring paralysis.** Without a framework that names what kind of document is needed, every contributor reinvents structure from scratch. Some attempts produce comprehensive prose explainers when a one-page reference would suffice; others produce bare command listings when readers actually need conceptual grounding. + +This portfolio is solo-maintained today but is built to be reviewable, hireable-from, and potentially shared with collaborators. Documentation that fails the three modes above is invisible work: it costs time to write, costs time to maintain, and produces little durable value. + +The remaining question is which documentation framework to adopt. The choice has to balance authoring cost, reader experience, durability against the framework's own future, and accessibility for solo and small-team maintainers. + +## Decision Drivers + +The following forces shaped this decision: + +1. **Reader-first organization.** A reader's first question is rarely "what topic is this?" — it is usually "what am I trying to do?" The framework should organize by user need, not by author convenience. +2. **Authorial clarity.** A contributor sitting down to write should know what kind of document they are producing before they begin. A framework that names doc types reduces the "what should this be?" decision to a one-step lookup. +3. **Findability.** A reader who has visited the documentation once should be able to predict the location of any subsequent piece of information from the framework's conventions alone. +4. **Active community and durability.** The framework's own home should still be active in five years. Frameworks whose flagship implementations are archived create future migration cost. +5. **Named-adopter signal.** Adoption by recognized organizations is empirical evidence that the framework survives contact with real engineering teams. +6. **Accessibility.** A solo maintainer must be able to produce a useful first draft without building tooling, hiring a writer, or reading a 200-page manual on documentation theory. +7. **Co-existence with ADR-0001.** Whatever framework is chosen must accommodate ADRs as a distinct, separately governed artifact rather than competing with them. +8. **Co-existence with operational reality.** Some real-world documentation is composite by necessity — runbooks naturally combine reference, procedure, and troubleshooting. The framework must accommodate composite documents without forcing artificial fragmentation. + +## Considered Options + +1. **No formal documentation framework.** Continue with ad-hoc structure: `README.md`, monolithic `DESIGN.md`, scattered notes. +2. **POSIX `man(7)` format.** Use the Unix manual-page convention with `NAME / SYNOPSIS / DESCRIPTION / OPTIONS / EXAMPLES / SEE ALSO`. +3. **The Good Docs Project templates.** Use the community-maintained per-doc-type Markdown templates. +4. **Single-doc-per-topic runbook structure.** Use one Markdown file per topic, internally structured along Google SRE / Atlassian / PagerDuty runbook conventions (`Purpose / Prerequisites / Procedure / Verification / Rollback / Troubleshooting`). +5. **Diátaxis.** Organize all documentation into four quadrants (tutorials, how-to guides, reference, explanation) with each document classified into exactly one quadrant. +6. **Custom in-house framework.** Define a bespoke documentation taxonomy specific to this portfolio. + +## Decision Outcome + +Chosen option: **Option 5, Diátaxis.** + +In a repository that adopts this baseline, all non-ADR documentation lives under `docs/` with subdirectories named exactly `tutorials/`, `how-to/`, `reference/`, and `explanation/`. Every Markdown file under those four subdirectories (other than an index `README.md`) lives in exactly one of them and is authored to one Diátaxis purpose. ADRs live in their own sibling subtree at `docs/decision-records/{org,template,repo}/` as established by ADR-0001 and are not subject to the Diátaxis quadrant rule. + +Two additional top-level subtrees are sanctioned because they are structural support genres rather than Diátaxis quadrants: + +- `docs/diagrams/` contains diagram-as-code source files. Mermaid `.mmd` is the org baseline format. Diátaxis pages MAY embed a rendered Mermaid copy inline for reader ergonomics, but the matching `.mmd` file is the source of truth and the page MUST link to it. +- `docs/runbooks/` contains operator-facing procedures, including break-glass and recovery procedures. Runbooks may combine reference, procedure, verification, rollback, and troubleshooting because their primary reader is an operator completing or recovering a real action. + +A repository is not required to populate every quadrant or adjunct genre. A repository that has no learning-oriented onboarding need not create tutorial content, and a repository with no diagrams or operator procedures may keep `docs/diagrams/` and `docs/runbooks/` empty. Empty subtrees are kept visible with `.gitkeep` sentinels when they are part of a shared baseline. + +Composite operational documents are treated as `docs/runbooks/` when their primary purpose is operator execution or recovery. Troubleshooting material belongs in `docs/runbooks/` when it guides an operator through recovery, and in `docs/reference/` when its primary purpose is lookup. Composite documents MUST use explicit second-level section headings such as `## Purpose`, `## Prerequisites`, `## Procedure`, `## Verification`, `## Rollback`, and `## Troubleshooting` so readers can predict where lookup material ends and procedural material begins. Composite documents are an explicit accommodation of operational reality and are not an exception that is allowed to expand silently into general non-operational docs. + +The directory name `docs/` is preferred over alternatives such as `documentation/` or `book/` for terseness and broad community familiarity. The subdirectory names match Diátaxis terminology exactly: `tutorials`, `how-to`, `reference`, `explanation`. Plural for `tutorials` matches Diátaxis usage. Hyphenated `how-to` matches Diátaxis usage and avoids an awkward `how_to` or `howto`. The plural `references` is explicitly avoided because Diátaxis uses the mass noun `reference`. The adjunct names `diagrams` and `runbooks` are reserved top-level names under `docs/`. + +## Pros and Cons of the Options + +### Option 1: No formal documentation framework + +- **Good, because** it has zero authoring overhead at the framework level. +- **Good, because** it imposes no structure on contributors who already know what they want to write. +- **Bad, because** it allows reference, procedural, and conceptual material to accumulate in the same files, where one type silently invalidates the others. +- **Bad, because** readers cannot predict the location of new information without reading the whole repository. +- **Bad, because** every contributor reinvents the wheel; the result reads as a collage rather than a documentation set. + +### Option 2: POSIX `man(7)` format + +- **Good, because** the format is one of the most stable in software history, with effectively 100% adoption in Unix-derived CLI tooling for over fifty years. +- **Good, because** the structure is well-defined and predictable: `NAME / SYNOPSIS / DESCRIPTION / OPTIONS / EXAMPLES / SEE ALSO`. +- **Bad, because** the format is designed for CLI program reference, not for operational documentation, conceptual explainers, onboarding tutorials, or repository-level governance docs. +- **Bad, because** it has no equivalent of how-to guides, tutorials, or explanation, so it would force every non-reference document into an awkward shape. +- **Bad, because** community familiarity outside CLI tooling is limited; readers expect this format only for executable manuals. + +### Option 3: The Good Docs Project templates + +- **Good, because** it provides explicit Markdown templates for several distinct document types. +- **Good, because** templates lower the barrier to authoring, especially for non-writers. +- **Bad, because** the project's flagship templates repository on GitHub was archived on 2022-09-24, and active development has migrated to a less-discoverable GitLab home; the public signal of momentum has weakened. +- **Bad, because** the project has no publicly named enterprise adopters of comparable visibility to the framework chosen below. +- **Bad, because** it is a templates-bundle rather than a documentation philosophy; it tells contributors what template to use but not why a particular reader needs a particular type of document. + +### Option 4: Single-doc-per-topic runbook structure + +- **Good, because** it is the most accessible structure when the constraint is "one URL per topic for a hurried operator." +- **Good, because** it matches the mental model of operations and SRE teams, who already think in runbooks. +- **Good, because** it accommodates reference, procedure, and troubleshooting in a single file, which simplifies cross-linking. +- **Neutral, because** it is a structure pattern rather than a documentation framework; it does not categorize non-operational documentation at all. +- **Bad, because** it encourages exactly the drift problem this ADR is trying to prevent: reference, how-to, and explanation accumulate in one file with no enforced separation. +- **Bad, because** it has no taxonomy for tutorials, conceptual explainers, or repository-level governance, so non-operational docs end up unfiled. +- **Bad, because** there is no canonical runbook standard; choosing this option requires also choosing one of several competing runbook templates and maintaining it as a local convention. + +### Option 5: Diátaxis (chosen) + +- **Good, because** it is the most-adopted explicit documentation methodology in modern software-engineering practice, with public adopters including Canonical, Cloudflare, Gatsby, LangChain, Vonage, Sequin, and StreamingFast, and an active framework repository at over a thousand stars. +- **Good, because** it is reader-first: it organizes documentation by what the reader is trying to do, not by what topic it is about. +- **Good, because** the four quadrants are mutually exclusive and collectively exhaustive in everyday practice; an authoring contributor knows in seconds which quadrant their document belongs in. +- **Good, because** it is a *philosophy* not just a *template bundle*; it explains why a particular reader needs a particular type of document, which is more durable than any single template. +- **Good, because** it accommodates ADR-0001 cleanly: ADRs are a specialized governance artifact, not a Diátaxis category, and live in their own directory governed by their own ADR. +- **Good, because** repositories may adopt it incrementally, populating only the quadrants they currently need. +- **Neutral, because** composite operational documents (runbooks, troubleshooting guides) span quadrant lines and require explicit accommodation; this ADR provides that accommodation in the Decision Outcome. +- **Bad, because** topics that today live in a single file may need to be split across two or more files when adopted, creating short-term re-shelving cost. +- **Bad, because** it requires authors to classify each new document, which is a small but nonzero per-doc decision. + +### Option 6: Custom in-house framework + +- **Good, because** it could be tailored to this portfolio's exact needs. +- **Bad, because** the maintenance and onboarding cost of a bespoke framework is unjustified when an actively maintained external framework already covers the same ground. +- **Bad, because** it provides no signal of community recognition to external readers, contributors, or hiring audiences. +- **Bad, because** it directly contradicts ADR-0001's preference for established community standards over local invention. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Layout check.** A repository that adopts this baseline MUST place all non-ADR documentation under `docs/`. Markdown under `docs/` MUST be in a top-level subdirectory named exactly `tutorials`, `how-to`, `reference`, `explanation`, `runbooks`, or `decision-records`. Diagram source files MUST be in `docs/diagrams/`. A CI script or `pre-commit` hook MAY fail a pull request that adds a file under `docs/` outside the sanctioned subtrees. +2. **Layout-skeleton check.** Every adopting repository MUST contain the four quadrant directories — `docs/tutorials/`, `docs/how-to/`, `docs/reference/`, `docs/explanation/` — plus `docs/diagrams/` and `docs/runbooks/`, even when some are empty. Empty subtrees are kept in source control via byte-identical `.gitkeep` placeholders mirrored from the org canonical, so a reader scanning any repo immediately sees the same predictable layout. *Content* in each subtree remains opt-in; only the *layout* is mandatory. +3. **Quadrant-purity check.** Each non-composite document under `docs/` SHOULD address exactly one Diátaxis quadrant. Composite operational documents belong in `docs/runbooks/` when their primary purpose is operator execution or recovery. +4. **Diagram source check.** Mermaid `.mmd` files under `docs/diagrams/` are source of truth. Markdown pages that embed those diagrams inline SHOULD link to the matching `.mmd` source immediately before or after the embedded diagram. +5. **Index check.** A repository's `docs/README.md`, if present, SHOULD link to every populated quadrant and adjunct genre and SHOULD label each linked document with its purpose. A CI script MAY diff the directory listing against the index and fail on drift. +6. **Co-existence check.** ADRs live at `docs/decision-records/{org,template,repo}/` (a sibling subtree, not a Diátaxis quadrant) and are not subject to the quadrant rule. A pull request that misfiles an ADR under one of the content directories SHOULD be rejected with a pointer to ADR-0001. +7. **Editorial rule.** After acceptance of this ADR, document re-shelving (moving an existing doc into the appropriate quadrant or sanctioned adjunct subtree) is editorial, not architectural; it does not require its own ADR. A material *change* to the framework choice — adopting a different framework, abandoning Diátaxis, or extending the top-level documentation taxonomy beyond `diagrams` and `runbooks` — requires an in-place update to this ADR when the decision subject remains documentation layout, or a superseding ADR when a different subject replaces it. + +Enforcement tooling is recommended but not mandatory at acceptance time. A solo-maintainer repository MAY rely on manual discipline; a team repository SHOULD automate at least the layout and index checks. + +## Consequences + +### Positive + +- New documentation has a predictable home from the moment it is written. +- Readers can navigate any adopting repository's `docs/` tree using the same mental model. +- Authoring decisions reduce to a one-step quadrant classification rather than open-ended structural design. +- Drift between reference, procedural, and conceptual material becomes harder, because each document has a single declared purpose. +- The choice carries community recognition: external readers, contributors, and hiring audiences encounter a framework they likely already know. +- Diagram and runbook material now has a sanctioned home instead of being forced into a quadrant that does not match its reader need. + +### Negative + +- Existing monolithic documents (in particular `DESIGN.md` in `github-terraform-framework`, and any plan documents in other repositories) do not yet conform to Diátaxis. Re-shelving them is a non-trivial editorial pass that is deferred to per-repository follow-on work. +- Some topics that today live in a single file will need to be split across two or more files; readers who already know the old single-file layout will have a one-time re-orientation cost. +- Authors must perform a small classification step per document. + +### Neutral + +- The four Diátaxis quadrant names are now reserved at `docs/{tutorials,how-to,reference,explanation}/` in adopting repositories. The adjunct genre names `docs/diagrams/` and `docs/runbooks/` are also reserved. Future changes to top-level directories under `docs/` should update this ADR explicitly. +- Composite operational documents are an explicit accommodation rather than a violation; their boundaries are codified in the Decision Outcome and Confirmation sections above. +- ADRs continue to be governed by ADR-0001 and are unaffected by this decision other than by cross-reference. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. The Diátaxis framework remains actively maintained and documented at [diataxis.fr](https://diataxis.fr) or an equivalent successor URL. +2. The four-quadrant taxonomy continues to map cleanly to the documentation needs that arise in this portfolio. If a sustained category of need cannot be classified into one of the four quadrants, that pattern is itself evidence to reconsider. +3. Markdown remains the primary documentation format in adopting repositories. +4. Repositories prefer source-controlled documentation that travels with the code over externally hosted knowledge bases. +5. Mermaid remains a viable text-based diagram format that GitHub can render natively. + +## Supersedes + +None. + +## Superseded by + +None (current). + +## Implementing PRs + +This section lists downstream pull requests that implement or operationalize the decision described in this ADR. It does not need to list the pull request that introduced the ADR itself. + +Pending. The first expected implementer is `github-terraform-framework`, which will adopt the Diátaxis layout for its initial PAT and AWS IAM documentation set, with `DESIGN.md` deferred for separate refactor. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) — establishes the ADR convention itself. ADR-0001 governs `docs/decision-records/{org,template,repo}/`; this ADR governs the rest of `docs/` (everywhere else). + +## Compliance Notes + +This ADR establishes a documentation organization convention, not a deployed security control. The table below indicates where evidence produced under this convention may help during reviews; it is illustrative rather than exhaustive, and is not a claim that a repository is compliant merely because Diátaxis is adopted. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| NIST SP 800-53 Rev. 5 | SA-5 (System Documentation) | A consistently structured `docs/` tree supports the system-documentation requirement by making operational, reference, and conceptual material findable. | +| NIST SP 800-53 Rev. 5 | AT-2 (Literacy Training and Awareness) | `docs/tutorials/` and `docs/how-to/` material supports onboarding and operational literacy. | +| NIST SP 800-218 (SSDF) | PO.3.2 (Document the security policies of the SDLC) | A standardized `docs/` location for security-relevant procedural and reference material reduces the assembly cost of evidence packages. | +| ISO/IEC 27001:2022 | A.5.37 (Documented operating procedures) | `docs/how-to/` and operational composite documents under `docs/reference/` provide a predictable location for documented procedures. | + +Subsequent repository-level ADRs that scope this convention to specific compliance contexts should keep only the rows that genuinely apply to their decision. + +## Changelog + +| Date | Change | Reason | Author/Role | Body-diff? | +| ---------- | ------------------------------------------- | --------------------------------------------------------- | --------------------------------- | ---------- | +| 2026-06-02 | Added diagram and runbook documentation genres. | House diagram source files and operator procedures without distorting Diátaxis quadrants. | Portfolio maintainer / governance | Yes | diff --git a/docs/decision-records/org/0003-use-deny-all-gitignore-strategy.md b/docs/decision-records/org/0003-use-deny-all-gitignore-strategy.md new file mode 100644 index 0000000..624d1c4 --- /dev/null +++ b/docs/decision-records/org/0003-use-deny-all-gitignore-strategy.md @@ -0,0 +1,202 @@ +# ADR-0003: Use a Deny-All `.gitignore` Strategy + +| Field | Value | +| -------------- | ---------------------------------------- | +| Status | Accepted | +| Date | 2026-04-25 | +| Authors | Nick Warila (@NWarila) | +| Decision-maker | Nick Warila (sole portfolio maintainer) | +| Consulted | None. | +| Informed | None. | +| Reversibility | Cheap | +| Review-by | N/A (Accepted) | + +## TL;DR + +In repositories that adopt this baseline, `.gitignore` is structured as **deny-all by default** with an **explicit allowlist** of files and directories that are intended to be tracked. The first non-comment rule in `.gitignore` is `**` (ignore everything), followed by `!`-prefixed allowlist entries. New files are not tracked unless their path is explicitly added to the allowlist. This inverts the dominant `.gitignore` convention (allow-all with scattered denies) in exchange for default-safe behaviour: secrets, terraform state, build artifacts, IDE files, and any future class of accidentally-introduced sensitive content cannot enter the repository through `git add` alone. + +## Context and Problem Statement + +The default `.gitignore` convention across the open-source community is **allow-all with explicit denies**: a base of "track everything" with a list of patterns to ignore. Tooling reinforces this default — IDEs, scaffolding tools, and language ecosystems ship pre-built `.gitignore` files that enumerate known build artifacts, vendor directories, and temporary files for that ecosystem. + +The dominant convention has a structural failure mode: anything *not* in the deny list is tracked. New tooling, new build outputs, new artifact types, and new categories of sensitive file all default to tracked. The list of patterns to deny is open-ended and grows with every new tool a repository adopts. A repository that integrates a new tool without simultaneously updating `.gitignore` silently begins tracking that tool's outputs. + +For the categories of file that matter most to a security-aware portfolio, this failure mode is dangerous: + +1. **Credentials.** `.env`, `credentials.json`, `.aws/`, OAuth tokens, GitHub Personal Access Tokens accidentally written to disk by a script. +2. **Terraform state.** `terraform.tfstate` files contain decrypted secrets and resource metadata in plaintext after `apply`. A leaked tfstate is an immediate compromise of the infrastructure it describes. +3. **Build artifacts.** Compiled binaries, bundled JavaScript, container layers, and similar outputs that bloat the repository and add no source-of-truth value. +4. **Personal-environment leakage.** IDE configuration, editor swap files, OS-level metadata files (`.DS_Store`, `Thumbs.db`, `desktop.ini`). +5. **Tool-specific transient files.** `.terraform/` plugin caches, `node_modules/` (when the project intends to commit `package-lock.json` only), `__pycache__/`, `.coverage`, `.pytest_cache/`, and a long tail. + +A repository that uses the dominant convention defends against (5) and parts of (3) and (4) only because someone, at some point, added each pattern to `.gitignore`. The defence against (1) and (2) is implicit — *if* the repository's `.gitignore` was thoughtful enough to deny `*.env`, `*.tfstate`, etc., the file is excluded; otherwise it is tracked the moment someone runs `git add .`. There is no signal at the moment of failure: `git add` does not distinguish between intended commits and accidental commits, and `git status` shows the file as a normal addition. + +This portfolio's `github-terraform-framework` repository has carried a deny-all `.gitignore` since its initial commit, and the strategy has prevented multiple categories of accidental commit during normal development. The remaining question is whether to elevate that ad hoc choice into an explicit, named decision that other repositories in the portfolio inherit, and to document the trade-offs honestly so future contributors understand both the value and the friction. + +## Decision Drivers + +The following forces shaped this decision: + +1. **Default-safe behaviour.** A new file should be ignored unless explicit action is taken to track it. The cost of one extra allowlist line is negligible; the cost of one accidentally committed credential is potentially catastrophic. +2. **Reviewability.** Every new tracked file should be visible in pull-request review as an `.gitignore` allowlist edit, not as a silent inclusion. This makes "what files does this PR start tracking?" a single grep, not a directory walk. +3. **Failure-mode visibility.** The strategy should fail in a way that is *visible*. A file that is silently ignored is bad if the contributor expected it to be tracked; the absence from `git status` should be diagnosable in seconds with documented tooling. +4. **Consistency across repositories.** Contributors who learn the pattern in one repository should encounter the same pattern in others. Mixing strategies across the portfolio costs cognitive overhead disproportionate to any per-repo optimisation. +5. **Reversibility.** A repository that adopts this strategy and later regrets it should be able to migrate back to allow-all-with-denies in a single PR. The choice should not lock the repository in. +6. **Compatibility with established tooling.** Pre-commit hooks, CI lint stages, and IDE integrations should not need to be retrained. The strategy must use only standard `.gitignore` syntax. +7. **Community familiarity.** The dominant convention is allow-all-with-denies. Choosing the opposite imposes a learning cost on contributors. The strategy should carry enough documentation that the cost is paid once, by the contributor's first encounter, not repeatedly. + +## Considered Options + +1. **No `.gitignore`.** Track every file in the working directory. +2. **Allow-all with explicit denies (community default).** Use a base of "track everything" with `.gitignore` entries that enumerate patterns to skip. +3. **Hybrid.** Allow-all base with periodic deny additions, supplemented by ad hoc per-directory `.gitignore` files. +4. **Deny-all with explicit allowlist (chosen).** First non-comment rule is `**` (ignore everything); subsequent `!`-prefixed entries explicitly allowlist tracked paths. +5. **`git add` discipline only.** No `.gitignore`; rely on contributors to use `git add ` rather than `git add .`. +6. **Sparse checkout / worktree partitioning.** Use git's sparse-checkout to limit which files are visible. + +## Decision Outcome + +Chosen option: **Option 4, deny-all with an explicit allowlist.** + +In a repository that adopts this baseline, `.gitignore` is organised as follows: + +1. The first non-comment rule is `**` (or an equivalent globstar that excludes the entire working tree). +2. Subsequent rules are `!`-prefixed allowlist entries that re-include specific files and directories. Allowlist entries are organised in groups corresponding to the categories of tracked content (source code, configuration, fixtures, documentation, CI workflows, license, README). +3. Each allowlist group is preceded by a `#`-prefixed comment that names the group and, where useful, explains why those entries are tracked. +4. New files added to the repository require an `.gitignore` allowlist edit. The allowlist edit and the new file MUST be in the same pull request and ideally in the same commit. A pull request that adds a file without allowlisting it is a reviewer-detectable defect: the new file will not appear in `git status` after `git add`. +5. Directories require **two** allowlist entries: one for the directory itself (`!/path/to/dir/`) and one for its contents (`!/path/to/dir/**`). A single entry of either form does not suffice in all git versions. + +The strategy applies recursively. A repository's top-level `.gitignore` SHOULD carry the deny-all rule and the full allowlist. Per-directory `.gitignore` files MAY exist but MUST NOT contradict the top-level deny: a per-directory file may add denials, never re-add allows. + +This baseline ADR establishes the default. Repositories that have a sustained reason to opt out — for example, a repository whose primary purpose is hosting a large generated artifact tree where allowlisting every file is impractical — MAY do so by recording a repository-level ADR that supersedes ADR-0003 in scope. An opt-out is itself an architectural decision and is treated as such. + +The strategy explicitly does not prescribe enforcement tooling. A pre-commit hook, CI check, or `git hook` MAY verify that the first non-comment line of `.gitignore` is the deny-all rule, that the allowlist contains no contradictions, or that every tracked file in the working tree is explicitly allowlisted. None of these checks are mandatory at acceptance time. Adopting repositories MAY add them as separate decisions. + +## Pros and Cons of the Options + +### Option 1: No `.gitignore` + +- **Good, because** it has zero authoring overhead and zero rule maintenance. +- **Bad, because** every transient file (build artifacts, IDE files, OS metadata, secrets) lands in pull requests by default. +- **Bad, because** it is not a viable strategy for any real-world repository and is included only for completeness. + +### Option 2: Allow-all with explicit denies (community default) + +- **Good, because** it is the dominant convention; contributors recognise it without explanation. +- **Good, because** language and tool ecosystems ship pre-built `.gitignore` files that contributors can drop in. +- **Good, because** small repositories with predictable noise (a single language, a single build system) can rely on community templates with minimal customisation. +- **Neutral, because** the rule list grows over time with every new tool the repository adopts. +- **Bad, because** anything *not* in the deny list is tracked by default. New file types slip through silently. +- **Bad, because** the failure mode for sensitive files (credentials, state, secrets) is "tracked unless explicitly denied." A forgotten deny is a security incident. +- **Bad, because** there is no review-time signal that a new tracked file was *intentionally* tracked rather than accidentally swept in by `git add .`. + +### Option 3: Hybrid + +- **Good, because** it allows incremental adoption: existing allow-all repositories can add per-directory denies without restructuring the top-level file. +- **Bad, because** it inherits all of Option 2's failure modes. +- **Bad, because** per-directory `.gitignore` files are easy to overlook; the strategy is implicitly distributed and hard to audit. +- **Bad, because** it picks the worst of both ends: contributors must understand both top-level and per-directory rules, but the safety properties of deny-all are not gained. + +### Option 4: Deny-all with explicit allowlist (chosen) + +- **Good, because** new files default to ignored; sensitive files (credentials, state, secrets) cannot enter the repository through `git add` alone. +- **Good, because** each new tracked file is an explicit, reviewable allowlist edit. "What files does this PR begin tracking?" reduces to a `.gitignore` diff. +- **Good, because** the inventory of intentionally tracked file paths *is* `.gitignore` itself; the file becomes self-documenting. +- **Good, because** the failure mode (a file not appearing in `git status` after `git add`) is detectable in seconds with `git check-ignore -v `, which names the rule that is excluding it. +- **Good, because** it requires only standard `.gitignore` syntax; pre-commit hooks, IDEs, and CI tooling work without modification. +- **Neutral, because** the initial setup of the allowlist for an existing repository is a one-time effort proportional to the breadth of currently tracked content. +- **Bad, because** it is unusual; contributors familiar only with the dominant convention can be surprised when a new file does not appear in `git status`. +- **Bad, because** a directory addition requires two allowlist entries (the directory and its contents); the most-elegant single-entry form `!/foo/**` does not allow the directory itself in all git versions. +- **Bad, because** repositories that legitimately track a very large number of file types (e.g., a generated documentation site with thousands of files) face high allowlist-maintenance overhead. Such repositories should consider opting out. + +### Option 5: `git add` discipline only + +- **Good, because** it imposes no rule maintenance. +- **Bad, because** human discipline is the weakest possible control. A single `git add .` undoes years of careful commits. +- **Bad, because** it provides no defence against IDE-driven file additions, automated tooling, or contributors unfamiliar with the discipline. +- **Bad, because** it provides no review-time signal for intentional vs. accidental tracking. + +### Option 6: Sparse checkout / worktree partitioning + +- **Good, because** it limits the surface area of `git add .` per checkout configuration. +- **Bad, because** sparse checkout is a *per-clone* setting, not a repository property. It does not protect contributors who do not configure it. +- **Bad, because** it is a checkout strategy, not a tracking strategy. Files outside the sparse area are still tracked in the repository if they were ever committed. +- **Bad, because** it adds significant tooling complexity that is disproportionate to the problem being solved. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Structural check.** A repository that adopts this baseline MUST contain a top-level `.gitignore` whose first non-comment, non-blank line is `**` (or an equivalent globstar excluding the entire working tree). A `pre-commit` hook or CI script MAY assert this. +2. **Allowlist-only-after-deny check.** All `!`-prefixed allowlist entries in the top-level `.gitignore` MUST appear *after* the deny-all rule. A reversed order silently negates the strategy. A CI script MAY assert this. +3. **No-orphan-tracked-files check.** Every file in `git ls-files` SHOULD have a corresponding `!`-prefixed allowlist entry. A CI script MAY enforce this; in practice it is reviewer-detectable because adding a file without allowlisting it produces `git status: nothing to commit` after `git add`. +4. **Per-directory file scope.** Per-directory `.gitignore` files MAY exist but MUST NOT contain `!`-prefixed entries that re-allow content that the top-level deny-all rule excludes. A reviewer SHOULD reject any per-directory allow that contradicts the top-level intent. +5. **PR review rule.** Pull requests that add new tracked files MUST modify `.gitignore` in the same change. A reviewer SHOULD reject a PR that introduces a new file without a corresponding allowlist edit; a CI check MAY automate the detection. +6. **Opt-out rule.** A repository that opts out of this strategy MUST record a repository-level ADR that names ADR-0003 in its `Supersedes` section (scoped to that repository) and explains why the trade-offs differ. + +Enforcement tooling is recommended but not mandatory at acceptance time. A solo-maintainer repository MAY rely on manual discipline; a team repository or a compliance-critical repository SHOULD automate at least the structural and allowlist-only-after-deny checks. + +## Consequences + +### Positive + +- New files default to ignored. Credentials, terraform state, build artifacts, and IDE files cannot enter the repository through `git add .` alone. +- Each new tracked file is an explicit allowlist edit, visible in pull-request review. +- `.gitignore` becomes the inventory of intentionally tracked content, making the repository's tracked surface auditable from a single file. +- The diagnostic for "why isn't this file showing up?" is `git check-ignore -v `, which names the rule responsible — fast and unambiguous. +- The strategy applies uniformly across the portfolio: contributors learn it once and recognise it everywhere it is adopted. + +### Negative + +- Initial allowlist setup is a one-time effort per repository. For an existing repository, this means walking `git ls-files` and producing the allowlist that matches it. +- Contributors unfamiliar with the pattern may be surprised when a new file does not appear in `git status`. This is mitigated by documentation but not eliminated. +- Each new directory requires two allowlist entries. The most-elegant single-entry form is not portable. +- Repositories that legitimately track very large generated trees may find the per-file allowlist impractical and should opt out. + +### Neutral + +- Repositories MAY adopt enforcement tooling (pre-commit hooks, CI checks). The strategy works without it but is more durable with it. +- Per-directory `.gitignore` files are not forbidden but are constrained to additive denies; they cannot re-allow content excluded at the top level. +- The dominant community convention (allow-all with denies) remains the default outside this portfolio. Contributors who work across both conventions must context-switch. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. Git's `**` and `!`-prefixed pattern semantics continue to behave as documented in `gitignore(5)`. +2. Adopting repositories' tooling (IDEs, pre-commit, CI, vendor-specific build systems) does not require allow-all `.gitignore` semantics to function. To date, none observed in this portfolio do. +3. Contributors to adopting repositories have access to this ADR and to the per-repository `.gitignore` documentation, so the unfamiliar pattern is learnable in seconds. +4. Repositories that legitimately need allow-all semantics are a minority and will opt out via repository-level ADR rather than pretending to adopt this baseline. + +## Supersedes + +None. + +## Superseded by + +None (current). + +## Implementing PRs + +This section lists downstream pull requests that implement or operationalize the decision described in this ADR. It does not need to list the pull request that introduced the ADR itself. + +`github-terraform-framework` already implements this strategy and predates the ADR; the implementation does not require a new PR. Subsequent adopting repositories will record their adoption in this section as they migrate. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) — establishes the ADR convention. ADR-0003 is governed by ADR-0001's format and lifecycle rules. +- [ADR-0002](0002-adopt-diataxis-documentation-framework.md) — establishes the Diátaxis documentation framework. Companion documentation for ADR-0003 (an explanation of how the strategy works in practice and a how-to for adding allowlist entries) belongs in adopting repositories' `docs/explanation/` and `docs/how-to/` directories per ADR-0002. + +## Compliance Notes + +This ADR establishes a source-control hygiene practice that contributes to the prevention of accidental disclosure of sensitive information. The table below indicates where evidence produced under this convention may help during reviews; it is illustrative rather than exhaustive, and is not a claim that a repository is compliant merely because the strategy is adopted. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| NIST SP 800-53 Rev. 5 | SC-28 (Protection of Information at Rest) | A deny-all `.gitignore` reduces the risk that sensitive at-rest content (terraform state, credentials, environment files) enters version control through accidental `git add` operations. | +| NIST SP 800-53 Rev. 5 | SI-12 (Information Management and Retention) | Explicitly allowlisting tracked content provides a source-of-truth inventory of what data the repository retains. | +| NIST SP 800-53 Rev. 5 | IA-5 (Authenticator Management) | The strategy contributes to authenticator-management hygiene by blocking common credential-bearing files (`.env`, `credentials.json`, `*.pem`, `*.key`) from accidental commit. | +| NIST SP 800-218 (SSDF) | PS.1 (Protect All Forms of Code from Unauthorized Access and Tampering) | The pull-request-visible `.gitignore` allowlist edit creates a reviewable trail of what content is intentionally added to the source-controlled artifact set. | +| OWASP | A02:2021 — Cryptographic Failures | Default-blocking `.env`, `*.key`, `*.pem`, and similar files reduces the most common vector for cryptographic-material disclosure: accidental commit. | + +Subsequent repository-level ADRs that scope this convention to specific compliance contexts should keep only the rows that genuinely apply to their decision. diff --git a/docs/decision-records/org/0004-use-renovate-for-dependency-updates.md b/docs/decision-records/org/0004-use-renovate-for-dependency-updates.md new file mode 100644 index 0000000..25c3b3e --- /dev/null +++ b/docs/decision-records/org/0004-use-renovate-for-dependency-updates.md @@ -0,0 +1,235 @@ +# ADR-0004: Use Renovate for Dependency Updates with Per-Template Baselines + +| Field | Value | +| ---------------- | ------------------------------------------------------------------------ | +| ID | ADR-0004 | +| Scope | Org baseline | +| Status | Accepted | +| Decision-subject | Dependency-update tooling and Renovate baseline inheritance for `NWarila`. | +| Date accepted | 2026-05-05 | +| Date | 2026-06-02 | +| Last reviewed | 2026-06-02 | +| Authors | Nick Warila (@NWarila) | +| Decision-makers | Nick Warila (sole portfolio maintainer) | +| Consulted | Renovate preset documentation and current template/consumer configs. | +| Informed | Maintainers of adopting repositories under `NWarila`. | +| Reversibility | Medium | +| Review-by | 2026-11-29 | + +## TL;DR + +All `NWarila/*` repositories track dependency updates via [Renovate](https://docs.renovatebot.com/). Each **type-template** in the portfolio (e.g. `NWarila/terraform-runner-template`, `NWarila/terraform-framework-template`, `NWarila/packer-framework-template`, `NWarila/packer-runner-template`, `NWarila/ansible-framework-template`, `NWarila/python-template`) owns a complete, self-contained `.github/renovate.json5` that is the canonical Renovate baseline for every consumer of that template. Consumers' local `.github/renovate.json5` extends only their type-template's explicit preset file (e.g. `extends: ["github>NWarila/terraform-runner-template//.github/renovate.json5"]`) and adds only the overrides genuinely specific to that consumer. The explicit path is required because Renovate resolves bare `github>owner/repo` presets to `default.json`, while these type-template baselines intentionally live at `.github/renovate.json5` so the template repository itself uses the same file Renovate evaluates. There is **no** org-level _shared_ `renovate.json5` for consumers to extend — consumers extend their type-template's baseline, never the org repo. `NWarila/.github` does carry its own `.github/renovate.json5`, but it governs only that repo's own dependencies (the `github-actions` pins in its reusable workflows) and is not a baseline any consumer extends. Renovate replaces Dependabot at the org level because Dependabot does not update Terraform's `required_version` field and has incomplete coverage of pinned tool versions in adjacent tooling. The per-template-baseline pattern keeps each stack's Renovate policy self-contained, lets stacks evolve their settings independently, and aligns with the three-tier ADR model from [ADR-0001](0001-use-architecture-decision-records.md): stack-level concerns live at the template tier, not the org tier. + +## Context and Problem Statement + +Repositories under the `NWarila` namespace track several version-pin surfaces that need automated updates: + +- **GitHub Actions** referenced by full commit SHA in workflow files, per the org's SHA-pin policy. +- **Terraform** version constraints — `required_version` on the `terraform` block, and provider versions in `required_providers`. +- **Tool versions** in adjacent tooling such as `.tool-versions` (asdf), devcontainer feature inputs, the `terraform_version:` literal in `hashicorp/setup-terraform` workflow steps, Dockerfile `FROM` lines, and pre-commit `rev:` references. +- **Other ecosystems** as repos add language-specific tooling (npm, pip, etc.). + +Dependabot supports the GitHub Actions case well. It does **not** support Terraform's `required_version` field — Dependabot's Terraform updater scans `required_providers` but ignores the constraint on Terraform itself. Dependabot also has limited and inconsistent handling of pinned tool versions in adjacent tooling. As repositories grow to include any of those, Dependabot leaves silent drift. Adopting Dependabot for every new repository accepts that gap by default, which is misaligned with the org's secure-by-default posture. + +Renovate offers native managers for every one of those surfaces (`terraform`, `terraform-version`, `github-actions`, `pre-commit`, `asdf`, `dockerfile`, `devcontainer`, `npm`, `pip`, etc.) plus a `regex` manager for arbitrary version literals. It also rewrites the trailing tag comment on SHA-pinned Actions bumps (`# v6` → `# v6.1.0`), preserving the human-readability convention enforced in the org's Actions SHA-pin policy. + +A second concern beyond manager coverage is configuration drift. If every repository hand-rolls its Renovate config, settings diverge across repos and stacks lose uniform behavior. Renovate's `extends` mechanism solves this: a single source of truth at the type-template tier is inherited by every consumer of that template, with per-repo overrides limited to repo-specific concerns. Different stacks (Terraform, Packer, Python, etc.) have legitimately different Renovate needs — `terraform.rangeStrategy`, `pip` constraints, `dockerfile` rebases — so each type-template owns the settings appropriate to its stack and stays out of the others' way. + +The previous per-repo `.github/dependabot.yml` files covered only `github-actions`, at varying schedules. That coverage no longer matches the org's actual update surface, and the cadence drift produces avoidable PR churn. + +## Decision Drivers + +The following forces shaped this decision: + +1. **Coverage of Terraform `required_version` and adjacent tooling.** Dependabot does not handle these; Renovate does. As repositories grow to pin Terraform CLI versions and other tool versions in adjacent tooling, the gap widens. +2. **SHA-pin retention on GitHub Actions.** The org's SHA-pin policy requires every `uses:` entry to be a 40-character commit SHA with a tag comment. The dependency-update tool must preserve this format on every bump. +3. **Conventional Commit emission.** Update PRs should emit Conventional Commit prefixes that release-please (where configured) categorises. +4. **Cross-repo consistency.** Common settings must be uniform across repos. Per-repo configs that drift are a maintenance liability. +5. **DRY within a stack (Inheritance over duplication).** Hand-copying the same Terraform settings into every Terraform consumer is error-prone. A type-template baseline that every consumer of that template inherits reduces maintenance to one place per stack. This aligns with the "Inheritance over duplication" principle in [ADR-0001](0001-use-architecture-decision-records.md). +6. **Stack independence.** Different stacks have legitimately different Renovate needs. A Terraform-specific change should not require touching the Packer template or vice versa. The configuration model must let stacks evolve independently. +7. **Reasonable PR cadence.** Daily PR creation produces noise; weekly cadence aligns with most repository review windows. + +## Considered Options + +1. **Stay on Dependabot org-wide.** Continue with per-repo `.github/dependabot.yml`, accepting the `required_version` gap and per-repo cadence drift. +2. **Adopt Renovate per-repo with no shared baseline at all.** Each repo maintains its own `.github/renovate.json5` from scratch. +3. **Adopt Renovate with a single shared org baseline.** All settings live in `NWarila/.github/.github/renovate.json5`; every consuming repo extends it directly. +4. **Adopt Renovate with self-contained type-template baselines.** Each type-template (Terraform, Packer, Python, etc.) owns a complete `renovate.json5`. Consumers extend their type-template only. No org-level Renovate config. +5. **Adopt Renovate with an org→template→consumer extends chain.** Truly universal settings at the org tier; stack-specific at the template tier; consumer-specific at the repo tier; consumers extend the template, which transitively extends the org. +6. **Mix Dependabot for legacy repos and Renovate for new repos.** Run both tools depending on repo age. +7. **Hand-roll a scheduled GitHub Actions workflow that opens update PRs.** Custom maintenance pipeline. + +## Decision Outcome + +Chosen option: **Option 4, Renovate with self-contained type-template baselines.** + +Each **type-template** in the portfolio owns a complete, self-contained `.github/renovate.json5` that is the single source of truth for its stack. Consumers of that template extend only the template; there is no org-level `renovate.json5` and consumers do not extend more than one config. + +The Terraform-runner template's baseline (`NWarila/terraform-runner-template/.github/renovate.json5`) is the canonical pattern. Other type-templates (`NWarila/terraform-framework-template`, `NWarila/packer-framework-template`, `NWarila/packer-runner-template`, `NWarila/ansible-framework-template`, `NWarila/python-template`, etc.) carry their own baselines tailored to their stack as they are brought online. + +Each type-template baseline configures, at minimum: + +- `extends: ["config:recommended"]` as the inherited Renovate baseline. +- `schedule: ["before 6am on monday"]` (weekly), the portfolio cadence. +- `semanticCommits: "enabled"` so PRs use Conventional Commit prefixes. +- `:dependencyDashboard` so each consumer gets a single tracking issue rather than a flood of standalone PRs. +- `prConcurrentLimit: 5` to cap noise during update bursts. +- A `packageRules` entry that maps `github-actions` updates to `ci(deps): ...` Conventional Commit prefixes with `pinDigests: true` to preserve SHA-pin format. +- Stack-specific settings — for Terraform: `terraform.rangeStrategy: "pin"` per [ADR-0005](0005-pin-terraform-and-provider-versions-exactly.md); `enabledManagers: ["github-actions", "terraform", "pip_requirements", "custom.regex"]`; the `customManagers` regex for `# renovate:` annotations in workflow comments. + +Each adopting consumer carries a minimal `.github/renovate.json5` that: + +- Inherits the template baseline via `extends: ["github>NWarila///.github/renovate.json5"]`. +- Adds only the overrides that are genuinely repo-specific (e.g. a single repo's release schedule, a single repo's automerge policy). Stack-wide overrides MUST be made in the type-template, not duplicated in every consumer. + +The `.github/dependabot.yml` file MUST NOT exist in any adopting repository. Repositories that previously contained one MUST remove it as part of their Renovate migration PR. + +Renovate enablement requires the Renovate GitHub App to be installed against each repository or against the entire org. Installation is a one-time operation outside the repo's git history and is the maintainer's responsibility. + +`NWarila/.github` carries a `.github/renovate.json5` that governs **only its own** dependencies — the `github-actions` pins in its reusable workflows — so the org repo's own supply chain stays current. This is **not** an org-level baseline: no consumer extends it, and there remains no _shared_ org Renovate config. The org tier holds ADRs and policies that apply to every repo regardless of stack; a truly universal Renovate setting (if one ever genuinely arose that applied to Terraform AND Packer AND Python AND PowerShell consumers identically) would still be added to each type-template independently rather than centralised. + +## Pros and Cons of the Options + +### Option 1: Stay on Dependabot org-wide + +- **Good, because** Dependabot is GitHub-native; no third-party app installation required. +- **Good, because** existing single-ecosystem configurations are already working for GitHub Actions in the repos that have them. +- **Bad, because** Dependabot cannot update Terraform's `required_version` field. Repositories with pinned Terraform versions accumulate untracked drift. +- **Bad, because** Dependabot's coverage of pinned tool versions in adjacent tooling is incomplete and inconsistent. +- **Bad, because** Dependabot's per-repo configuration provides no shared baseline; common settings drift across repos. + +### Option 2: Adopt Renovate per-repo with no shared baseline at all + +- **Good, because** every repo's behavior is fully self-contained and visible in one file. +- **Good, because** there is no implicit dependency on an external config repo at evaluation time. +- **Bad, because** common settings (schedule, semantic-commit prefixes, SHA-pin retention) drift across repos as new repos are bootstrapped from older templates. +- **Bad, because** changing a stack-wide setting (e.g., shifting the cadence from weekly to bi-weekly) requires a coordinated PR across every repo. +- **Bad, because** it duplicates ~80 lines of identical config into every repository, contradicting the org's "Inheritance over duplication" principle. + +### Option 3: Adopt Renovate with a single shared org baseline + +- **Good, because** truly universal settings live in exactly one place. +- **Good, because** changing such a setting takes one PR. +- **Bad, because** different stacks have legitimately different needs (`terraform.rangeStrategy`, `pip` constraints, `dockerfile` rebases). An org-level baseline either serves the lowest common denominator or ends up encoding stack-specific defaults that are wrong for some stacks. +- **Bad, because** an outage or breaking change in the shared baseline propagates to every consuming repo at once, regardless of stack. A Terraform-specific tweak should not be able to break Packer consumers. +- **Bad, because** it conflicts with the three-tier ADR model (org / template / repo): stack-specific concerns live at the template tier, and Renovate config carries the same character. + +### Option 4: Adopt Renovate with self-contained type-template baselines (chosen) + +- **Good, because** Renovate covers every update surface the org has now or is likely to grow into. +- **Good, because** `pinDigests: true` (set per-template for github-actions) preserves SHA-pin format on Action bumps and rewrites trailing tag comments in place. +- **Good, because** `semanticCommits` emits Conventional Commit prefixes that release-please categorises without per-PR rewriting. +- **Good, because** each type-template's baseline is self-contained and stack-appropriate. A Terraform-specific tweak only affects Terraform consumers; a Packer-specific tweak only affects Packer consumers. +- **Good, because** consumers remain free to override repo-specific concerns without re-declaring the entire config. +- **Good, because** the dependency-dashboard issue surfaces pending updates without flooding the PR list. +- **Good, because** the configuration model directly mirrors the three-tier ADR model: stack-level concerns live at the template tier where they belong. +- **Neutral, because** Renovate requires the GitHub App to be installed once per repository (or once per org). +- **Neutral, because** truly universal settings (cadence, dependency-dashboard, prConcurrentLimit) end up duplicated across each type-template's baseline. In practice these settings rarely change and the duplication is small (~10 lines per template); the trade-off is worth the stack independence. +- **Bad, because** the Renovate GitHub App is a third-party dependency in the supply chain (managed by Mend); operational burden of compromise is real. +- **Bad, because** the dependency-dashboard issue is opinionated; if not curated it can clutter the issue tracker. + +### Option 5: Org→template→consumer extends chain + +- **Good, because** truly universal settings live at the org tier and stack-specific at the template tier, eliminating the Option 4 duplication. +- **Good, because** the inheritance chain matches Renovate's idiomatic pattern (`extends` is transitive). +- **Bad, because** it adds a third config file to reason about per consumer (org + template + consumer's own). +- **Bad, because** an org-level Renovate config blurs the line between "org tier = universal policy" and "stack-specific config", which the three-tier ADR model carefully separates. ADRs at the org tier apply to every repo regardless of stack; Renovate config historically does not. +- **Bad, because** propagation behavior across two levels of inheritance is harder to debug when a single consumer behaves unexpectedly. +- **Bad, because** the duplication-cost in Option 4 is a small one (~10 lines per template) and Option 5's reduction is not worth the additional architectural surface area. + +### Option 6: Mix Dependabot and Renovate + +- **Good, because** legacy repos avoid the migration cost. +- **Bad, because** the org loses uniform dependency-update behavior. New contributors must learn which repo uses which tool. +- **Bad, because** the `required_version` coverage gap remains for any repo still on Dependabot, partially defeating the value of switching at all. +- **Bad, because** mixed-tool environments accumulate inconsistencies (different cadences, different commit-message formats) that erode the value of having either tool. + +### Option 7: Hand-roll a scheduled GitHub Actions workflow + +- **Good, because** it provides full control over update logic, schedule, and PR template. +- **Bad, because** it imposes disproportionate maintenance burden for a personal-account org. +- **Bad, because** features like release-notes fetching, semver diffing, and dependency-graph awareness would all be reinvented. +- **Bad, because** a hand-rolled workflow is a single point of failure with no community support. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Tool-presence check.** Every adopting repository MUST contain `.github/renovate.json5`. A `.github/dependabot.yml` file MUST NOT exist; a CI script or `pre-commit` hook MAY assert its absence. +2. **Inheritance check.** Every adopting consumer's `.github/renovate.json5` MUST include exactly one `github>NWarila///.github/renovate.json5` entry in its `extends` array, identifying the type-template the consumer derives from and the exact preset file Renovate should load. Bare `github>NWarila/` preset references are not compliant unless the template deliberately publishes a `default.json` preset; the current type-template baselines do not. A consumer that does not derive from a type-template (e.g. the type-template repos themselves, or a one-off repo with no template lineage) MUST document its exception in a repo-specific superseding ADR. +3. **SHA-pin retention check.** Every type-template's `.github/renovate.json5` MUST configure `pinDigests: true` for the `github-actions` manager (typically via a `packageRules` entry). A reviewer SHOULD reject a PR to a type-template baseline that removes or disables this setting without a superseding ADR. +4. **Schedule check.** Every type-template's `.github/renovate.json5` MUST schedule weekly or less-frequent runs. Daily or more-frequent schedules would produce avoidable PR churn across every consumer of that template. +5. **Override discipline.** Repository-local overrides MUST be limited to repo-specific concerns. Settings that should apply to every consumer of a particular type-template MUST be added to that type-template's `renovate.json5` rather than copy-pasted into every consumer. There is no org-level Renovate baseline; settings that would otherwise be "truly universal" are duplicated across each type-template independently to preserve stack independence (see Option 4 §"Neutral"). +6. **No org-level Renovate baseline.** `NWarila/.github/.github/renovate.json5` MUST NOT be referenced via `extends` by any consumer; there MUST be no _shared_ org-level Renovate baseline that consumers extend. The org repo's own `.github/renovate.json5` is permitted and governs **only** that repo's own dependencies (the `github-actions` pins in its reusable workflows); it is not a baseline any consumer extends. A maintainer who is tempted to centralise a setting "because it applies to every template" SHOULD instead add it to each template's baseline; the duplication cost is small and the stack independence is worth more. +7. **Editorial rule.** A change of dependency-update tool (back to Dependabot, or to a third option) is itself an architectural decision and MUST be recorded as a superseding ADR. Adoption or removal of a type-template's Renovate baseline is a template-tier decision and MUST be recorded as an ADR in that type-template's own `docs/decision-records/template/` directory. +8. **Minimum-release-age + strict-timestamp check.** Every type-template's `.github/renovate.json5` MUST set `minimumReleaseAge: "7 days"` and `internalChecksFilter: "strict"`. The 7-day quarantine window blocks the common compromised-package pattern where a malicious release is published and then yanked within hours or days; consumers that only consider releases at least seven days old never observe the yanked window. The `"strict"` filter mode is required because Renovate's default behavior (`"none"`) silently allows updates whose timestamps it cannot resolve, which would defeat the quarantine for any datasource that omits timestamp metadata. Strict mode treats unresolvable timestamps as failing the age check, which is the conservative choice for a supply-chain control. A template-tier baseline that omits either setting, or sets `minimumReleaseAge` below seven days, requires a superseding ADR documenting the relaxation and its rationale. A CVE-driven immediate-bump carve-out via `vulnerabilityAlerts.minimumReleaseAge: "0 days"` is permitted at the type-template tier provided the carve-out is recorded as a separate Confirmation item or a follow-up ADR in that template's `docs/decision-records/template/`. + +Enforcement tooling is recommended but not mandatory at acceptance time. A repository MAY add CI scripts that verify (1)–(3); template adoption MAY be tracked via the drift-gate workflow that mirrors ADRs from the appropriate sources. + +## Consequences + +### Positive + +- Terraform `required_version` updates are tracked automatically across the org; the Dependabot-shaped gap is closed. +- Action SHAs stay current with their tag comments rewritten in place across every repo, preserving the SHA-pin convention without manual intervention. +- Conventional Commit prefixes flow into release-please without per-PR rewriting. +- Each stack's settings live in exactly one place — its type-template — and cannot affect other stacks. Changing Terraform-specific behavior takes one PR to `NWarila/terraform-runner-template` and reaches every Terraform consumer; Packer and Python are untouched. +- New consumers of a template bootstrap with a ~6-line `renovate.json5` that inherits the template behavior automatically. +- Future managers (pre-commit, devcontainer features, mkdocs Python deps, Docker base images) can be enabled by editing the relevant type-template's baseline rather than every consuming repo. + +### Negative + +- One additional GitHub App must be installed against the org (or per-repo). +- Renovate's dependency-dashboard issue is opinionated and clutters the issue tracker if not curated. +- Each type-template's baseline is now a load-bearing artifact for its stack: an outage or breaking change in `NWarila//.github/renovate.json5` propagates to every consumer of that template on the next Renovate run. Mitigation: type-template baselines are reviewed in PR like any other template-tier change. +- Truly universal settings (cadence, prConcurrentLimit, semantic-commits, dependency-dashboard) end up duplicated across each type-template's baseline. In practice these settings rarely change; the duplication is small and the stack independence is worth more. +- Release-notes fetching adds latency to PR creation (negligible in practice). + +### Neutral + +- The `github>` extends syntax creates a runtime dependency on `NWarila/` being reachable when Renovate evaluates a consuming repo. In practice this is reliable; if it becomes unreliable, consumers MAY temporarily inline the template baseline. +- This ADR scopes the decision to the `NWarila` organization. The functionally-identical `nwarila-platform` organization carries its own ADR-0004 documenting the same per-template-baseline pattern with `nwarila-platform/*` scoping; the two ADRs are maintained in parallel per the portfolio's org-separation practice. Cross-org consumers (e.g., an `nwarila-platform/*` runner consuming `NWarila/terraform-runner-template` via `extends: ["github>NWarila/terraform-runner-template//.github/renovate.json5"]`) inherit the type-template's behavior regardless of which org's `.github` is read for community-health policy. +- Repo-specific overrides remain permitted; this ADR is not a uniformity-at-all-costs mandate. The only constraint is that overrides MUST be repo-specific concerns. Stack-wide concerns belong in the type-template tier per ADR-0001. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. The Renovate GitHub App remains free for personal-account organizations and continues to be actively maintained. +2. Renovate continues to support GitHub-hosted presets with explicit file paths, including `.json5` preset files. +3. The Renovate config schema remains compatible with the configuration shape used here. +4. The org continues to use Conventional Commits + release-please for repos that publish releases. A switch to a different release tool would require adjusting `semanticCommitType` overrides in the shared baseline. + +## Supersedes + +None — `.github/dependabot.yml` files in `NWarila/*` repos were single-ecosystem configurations with no prior ADR documenting their adoption. This ADR replaces that pattern as a new decision rather than as a formal supersession. + +## Superseded by + +None (current). + +## Implementing PRs + +Pending. Each type-template's `.github/renovate.json5` is the source of truth for its stack and is a precondition rather than an "implementing PR" of this ADR (the Terraform-runner template's baseline already exists; other type-templates' baselines will be authored or reviewed as those templates come online). Consumer migration PRs that switch from a copy-pasted Renovate config to a thin `extends: ["github>NWarila///.github/renovate.json5"]` will be listed here. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) — establishes the format and three-tier scope structure of decision records. The per-template-baseline pattern in this ADR mirrors that three-tier model: stack-level concerns live at the template tier. +- [ADR-0003](0003-use-deny-all-gitignore-strategy.md) — establishes the deny-all `.gitignore` strategy. Renovate config files are explicitly allowlisted in adopting repositories per ADR-0003. +- [ADR-0005](0005-pin-terraform-and-provider-versions-exactly.md) — pins Terraform and provider versions exactly across the org. Each Terraform-shape type-template's `renovate.json5` sets `terraform.rangeStrategy: "pin"` per that ADR. Per-template baselines mean each stack records its own analogous decisions in its own ADRs where applicable. + +## Compliance Notes + +This ADR preserves the SHA-pin policy (encoded in each type-template's baseline as `github-actions.pinDigests: true`). It does not modify branch-protection or PR-review requirements: every Renovate PR is subject to the same `main`-branch protections as a human-authored PR, including required status checks. Future ADRs that adopt additional managers (e.g., `pre-commit`, `pip`, `docker`) inherit this ADR's defaults and need only document scope-specific divergence in repo-local config. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| NIST SP 800-53 Rev. 5 | SI-2 (Flaw Remediation) | Renovate's automated update PRs contribute to the timely application of patches and security fixes across the org. | +| NIST SP 800-53 Rev. 5 | CM-3 (Configuration Change Control) | The per-template-baseline pattern records stack-wide dependency-management policy in source control with PR review history. | +| NIST SP 800-218 (SSDF) | PW.4 (Reuse Existing, Well-Secured Software When Feasible) | Tracking dependency updates with SHA-pin retention preserves the supply-chain integrity posture for reused software. | + +## Changelog + +| Date | Change | Reason | Author/Role | Body-diff? | +| ---------- | ----------------------------------------- | ------------------------------------------- | --------------------------------- | ---------- | +| 2026-06-02 | Refreshed living metadata and clarified explicit Renovate preset-path compliance. | Re-review against current Renovate preset documentation and live template/consumer configs. | Portfolio maintainer / governance | Yes | diff --git a/docs/decision-records/org/0005-pin-terraform-and-provider-versions-exactly.md b/docs/decision-records/org/0005-pin-terraform-and-provider-versions-exactly.md new file mode 100644 index 0000000..9c12568 --- /dev/null +++ b/docs/decision-records/org/0005-pin-terraform-and-provider-versions-exactly.md @@ -0,0 +1,179 @@ +# ADR-0005: Pin Terraform and Provider Versions Exactly + +| Field | Value | +| -------------- | ---------------------------------------- | +| Status | Accepted | +| Date | 2026-05-05 | +| Authors | Nick Warila (@NWarila) | +| Decision-maker | Nick Warila (sole portfolio maintainer) | +| Consulted | None. | +| Informed | None. | +| Reversibility | Medium | +| Review-by | N/A (Accepted) | + +## TL;DR + +Every repository under `NWarila` that contains Terraform configuration pins both the Terraform CLI version (via `terraform { required_version = "= X.Y.Z" }`) and every provider version in `required_providers` to an exact version using the `=` operator. Range constraints (`>=`, `~>`, etc.) are not used. Renovate keeps these exact pins current via `rangeStrategy: "pin"` configured in each Terraform-shape type-template baseline. Consumers of repos that publish Terraform modules MUST run the exact pinned Terraform CLI version; consumers running anything else hit `terraform init` failure immediately rather than discovering compatibility issues partway through `apply`. + +## Context and Problem Statement + +Terraform's `required_version` constraint on the `terraform { }` block is enforced by the CLI: every consumer of a configuration (root or child module) must satisfy the constraint, or `terraform init` aborts. The constraint uses the [HashiCorp version-constraint syntax](https://developer.hashicorp.com/terraform/language/expressions/version-constraints) and supports several operators: + +- `= X.Y.Z` — exact pin; only this version satisfies the constraint +- `>= X.Y` — minimum; any newer version satisfies +- `~> X.Y` — pessimistic; permits patch updates within X.Y +- Combinations like `>= 1.9, < 2.0` for explicit ranges + +The same operators apply to provider version constraints in `required_providers`. + +Two camps exist in the wider Terraform community: + +1. **Range constraints** (`>=`, `~>`). The argument is multi-module satisfiability: if a root module pulls in three child modules from independent authors, range constraints let all three coexist as long as one CLI version satisfies their union. This matters when consuming modules from authors you do not control. + +2. **Exact pins** (`=`). The argument is reproducibility and security: every consumer runs the exact CLI version the author tested with; behavior is deterministic; supply-chain integrity is stronger. + +The `NWarila` portfolio sits squarely in the second context. Modules in this org are consumed almost exclusively by other repositories in the same org. The maintainer controls every published module and every consumer. Multi-module satisfiability across third-party authors is not a real concern here. What matters is: + +- Reproducibility: every consumer runs the exact Terraform CLI version we tested with +- Security: known-good versions; no surprise behavior changes from a consumer using a newer CLI +- Supply-chain consistency: the SHA-pin policy on GitHub Actions extends naturally to exact-pinning Terraform versions +- Predictable failure modes: `terraform init` fails fast on version mismatch, not partway through `apply` + +The previous default ([ADR-0004](0004-use-renovate-for-dependency-updates.md) §Decision Outcome) suggested `rangeStrategy: "bump"` for child modules and `"pin"` for root modules. That distinction is suitable for the wider community but is unnecessarily permissive for this org's consumption model. + +## Decision Drivers + +The following forces shaped this decision: + +1. **Reproducibility.** Every consumer should run the exact Terraform CLI and provider versions the author tested with. Version drift is a known source of "works on my machine" incidents. +2. **Security and supply-chain consistency.** Exact pins on Terraform and providers match the SHA-pin posture on GitHub Actions. The org's overall stance is "if it can be pinned exactly, pin it exactly." +3. **Failure-mode visibility.** `terraform init` aborting with "required Terraform version is X, you have Y" is unambiguous. Compatibility issues that surface partway through `terraform plan` or `terraform apply` are harder to diagnose and may leave partial state behind. +4. **Cross-module composability within the org.** Because the maintainer controls every module, exact-pinning all of them to the same Terraform version is straightforward; multi-module satisfiability concerns do not apply. +5. **Tested-and-proven posture.** Publishing a module that says "should work with Terraform >= 1.9" makes a claim the maintainer has not actually verified. Pinning `= 1.9.8` says only what has been tested. +6. **Renovate fitness.** Renovate's `rangeStrategy: "pin"` operates correctly on exact-pinned versions: it bumps the exact version on each update, requiring an explicit author-controlled PR for each change. + +## Considered Options + +1. **Exact pins for both Terraform and providers.** `terraform { required_version = "= X.Y.Z" }` and `required_providers` entries with `version = "= X.Y.Z"`. Renovate uses `rangeStrategy: "pin"`. +2. **Range constraints with pessimistic operator (`~>`).** Permits patch-level updates without re-running tests. +3. **Range constraints with minimum (`>=`).** Permits any newer version. +4. **Hybrid: exact-pin Terraform CLI, range-pin providers.** Lock the runtime, allow provider drift. +5. **No constraint at all.** Omit `required_version` and `required_providers` constraints; let consumers choose. + +## Decision Outcome + +Chosen option: **Option 1, exact pins for both Terraform and providers.** + +In every repository that adopts this baseline: + +- The `terraform { }` block in `versions.tf` MUST set `required_version = "= X.Y.Z"` using a single exact version. Range operators (`>=`, `~>`, etc.) MUST NOT be used. +- Every `required_providers` entry MUST set `version = "= X.Y.Z"` using a single exact version. +- Every Terraform-shape type-template Renovate baseline sets `terraform.rangeStrategy: "pin"` for Terraform manager updates. Consumers inherit this via `extends: ["github>NWarila///.github/renovate.json5"]` per [ADR-0004](0004-use-renovate-for-dependency-updates.md). Repo-local Renovate configs MUST NOT override this to `"bump"`, `"replace"`, or `"widen"` without a superseding repo-level ADR. +- The README's "Provider Requirements" or equivalent table MUST display the exact pinned versions and explain that consumers must run that exact CLI version. +- When a repository updates either the Terraform CLI or a provider version, the update MUST be tested against the pinned version before merging the Renovate PR. A `terraform test` suite that runs on every PR satisfies this requirement. + +This refines the rangeStrategy guidance in [ADR-0004](0004-use-renovate-for-dependency-updates.md) §Decision Outcome by making the Terraform stack's answer unambiguous: **always pin exactly**. ADR-0004's decision to use Renovate with per-template baselines remains in force. + +## Pros and Cons of the Options + +### Option 1: Exact pins for both Terraform and providers (chosen) + +- **Good, because** every consumer runs the exact CLI and provider version the author tested with; reproducibility is total. +- **Good, because** failure modes are predictable: `terraform init` fails fast on mismatch with a clear message. +- **Good, because** supply-chain posture is consistent with the org's SHA-pin policy on GitHub Actions. +- **Good, because** Renovate's `rangeStrategy: "pin"` operates cleanly on exact pins; each version bump is an explicit, reviewable PR. +- **Good, because** authors cannot accidentally publish a "should work with anything ≥ X" claim they have not actually verified. +- **Bad, because** consumers must update their Terraform CLI when a module bumps. For consumers using `tfenv` or `asdf`, this is a one-line `.tool-versions` change. For consumers without per-project version management, it is more friction. +- **Bad, because** in a hypothetical future with cross-org module consumption, an exact-pinned dependency tree is harder to satisfy than a range-pinned one. Mitigation: this is not a concern in the current org's consumption model. +- **Neutral, because** it imposes more discipline on the maintainer (every Renovate PR requires testing against the new exact version) but the discipline matches the org's overall posture. + +### Option 2: Pessimistic operator (`~> X.Y`) + +- **Good, because** it permits patch-level CLI/provider updates without per-update testing or PRs. +- **Good, because** it is the most common pattern in the wider Terraform community. +- **Bad, because** consumers may run a slightly different version from the author's tested version; subtle behavior differences slip through. +- **Bad, because** it weakens the reproducibility argument — "tested with 1.9.8, deployed with 1.9.12" is not the same as "tested and deployed with 1.9.8". +- **Bad, because** Renovate's behavior for `~>` versions is to bump the floor, not pin to the exact version, leaving the same drift surface. + +### Option 3: Minimum constraint (`>= X.Y`) + +- **Good, because** it is the most permissive option for consumers; any newer CLI works. +- **Bad, because** it makes a claim the author has not verified ("should work with anything ≥ X"). Future Terraform versions may break this assumption silently. +- **Bad, because** it provides no upper-bound protection; a consumer running a future Terraform major version might break in unpredictable ways with no constraint to catch it. + +### Option 4: Hybrid — exact CLI, range providers + +- **Good, because** it locks the runtime (the most volatile component) while permitting provider patches. +- **Bad, because** it introduces inconsistency: why pin some things and not others? The org's stance is "pin everything that can be pinned." +- **Bad, because** providers are equally susceptible to surprise behavior changes; locking the CLI but not the provider gives a false sense of reproducibility. + +### Option 5: No constraint + +- **Good, because** it imposes nothing on consumers. +- **Bad, because** it abandons the reproducibility and supply-chain arguments entirely. +- **Bad, because** Terraform itself recommends including `required_version` and `required_providers` for any non-trivial configuration; omitting them is a documentation deficit, not a permissive choice. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Constraint operator check.** Every `required_version` and `required_providers[].version` value MUST use the `=` exact operator. A CI script or `tflint` rule MAY assert this; the regex `^=\s*[0-9]+\.[0-9]+\.[0-9]+$` is sufficient for the exact-pin shape. +2. **Renovate rangeStrategy check.** Every Terraform-shape type-template baseline MUST set `terraform.rangeStrategy: "pin"` for Terraform manager updates. Repo-local overrides MAY narrow this for a specific manager but MUST NOT widen it without a superseding repo-level ADR. A CI script MAY assert this. +3. **README documentation.** Repositories that publish Terraform modules MUST document the exact pinned Terraform CLI version and provider versions in the README's prerequisites or provider requirements section, with an explicit statement that consumers must run those exact versions. +4. **Test-before-bump rule.** A Renovate PR that bumps the Terraform CLI version or a pinned provider version SHOULD NOT be merged without the maintainer running the test suite against the new version. A CI workflow that runs `terraform test` on every PR satisfies this requirement automatically. +5. **Editorial rule.** A relaxation of the exact-pin policy (e.g., adopting `~>` for a specific repo) is an architectural decision and MUST be recorded as a repository-level superseding ADR. + +## Consequences + +### Positive + +- Reproducibility: every consumer runs the exact CLI and provider version the author tested with. +- Failure modes are predictable and fail-fast. +- Supply-chain posture is consistent across SHA-pinned Actions, exact-pinned Terraform, and exact-pinned providers. +- Each version update is an explicit, reviewable, testable Renovate PR rather than silent drift. + +### Negative + +- Consumers must update their local Terraform CLI on every CLI version bump in a depended-on module. For consumers using `tfenv` or `asdf` this is trivial; for others it is more friction. +- The org now ships modules that have a hard external dependency on a specific Terraform version. Switching modules to a newer Terraform requires a coordinated bump across every consumer. +- Renovate generates more frequent PRs against the org as Terraform and providers release. The maintainer absorbs the review burden. + +### Neutral + +- The exact-pin policy applies only inside `NWarila`. Future external consumers (if any) inherit the strictness; if their context demands range constraints they fork or pin internally. +- Repositories that today have range constraints will be migrated to exact pins via the implementing PRs. The migration is a one-time editorial pass; ongoing maintenance is a single-line edit per Renovate PR. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. The `NWarila` org continues to consume Terraform modules primarily from itself, not from third-party authors with conflicting version constraints. +2. Renovate's `rangeStrategy: "pin"` continues to behave as documented — converting ranges to exact pins on the next bump and bumping exact pins to newer exact versions thereafter. +3. Consumers of `NWarila` Terraform modules are willing to accept the discipline of running the exact pinned CLI version. + +## Supersedes + +None. This ADR refines [ADR-0004](0004-use-renovate-for-dependency-updates.md) §Decision Outcome's rangeStrategy guidance but does not supersede ADR-0004 in full; ADR-0004's choice of Renovate over Dependabot and the shared-baseline pattern remain in force. + +## Superseded by + +None (current). + +## Implementing PRs + +Pending. The first implementing PR ships in `terraform-proxmox-iso-manager-framework`, which migrates `terraform/versions.tf` from `required_version = ">= 1.9"` to `= 1.9.8` and from `version = ">= 0.98.1"` to `= 0.98.1`, simplifies `.github/renovate.json5` to inherit the org baseline (which now sets `terraform.rangeStrategy: "pin"`), and adds `terraform test` coverage to enforce the test-before-bump rule. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) — establishes the format and three-tier scope structure of decision records. +- [ADR-0004](0004-use-renovate-for-dependency-updates.md) — establishes Renovate as the org's dependency-update tool. ADR-0005 refines ADR-0004's rangeStrategy guidance to "always pin exactly". + +## Compliance Notes + +This ADR strengthens the supply-chain posture by ensuring that every Terraform configuration consumed in `NWarila` runs against a known, tested set of CLI and provider versions. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| NIST SP 800-53 Rev. 5 | CM-2 (Baseline Configuration) | Exact-pinned Terraform and provider versions are part of the baseline configuration of every infrastructure deployment. | +| NIST SP 800-53 Rev. 5 | SI-7 (Software, Firmware, and Information Integrity) | Exact pins reduce the surface for unintentional or malicious version changes between author-tested and consumer-deployed. | +| NIST SP 800-218 (SSDF) | PS.2 (Provide a Mechanism for Verifying Software Release Integrity) | Combined with SHA-pinned Actions, exact-pinned Terraform contributes to release-integrity verification across the toolchain. | diff --git a/docs/decision-records/org/0006-keep-github-control-planes-namespace-local.md b/docs/decision-records/org/0006-keep-github-control-planes-namespace-local.md new file mode 100644 index 0000000..03b86fe --- /dev/null +++ b/docs/decision-records/org/0006-keep-github-control-planes-namespace-local.md @@ -0,0 +1,155 @@ +# ADR-0006: Keep GitHub Control Planes Namespace-Local + +| Field | Value | +| ---------------- | --------------------------------------------------------------------------- | +| ID | ADR-0006 | +| Scope | Org baseline | +| Status | Accepted | +| Decision-subject | Namespace-local ownership for GitHub control-plane governance. | +| Date accepted | 2026-06-01 | +| Date | 2026-06-02 | +| Last reviewed | 2026-06-02 | +| Authors | Nick Warila (@NWarila) | +| Decision-makers | Nick Warila (sole portfolio maintainer) | +| Consulted | CI findings from repo-hygiene, ADR drift, and reusable workflow rollout. | +| Informed | Maintainers of adopting repositories under `NWarila`. | +| Reversibility | Medium | +| Review-by | 2026-11-29 | + +## TL;DR + +Repositories under `NWarila` use `NWarila/.github` as their org control plane for org-baseline ADRs, community-health files, org repo-hygiene policy, and reusable workflow callers. Repositories under another namespace, including `nwarila-platform`, use that namespace's own `.github` repository for the same control-plane concerns. Cross-namespace dependencies remain allowed for type-template repositories and explicit tools, but not for org governance that should be owned by the consuming repository's namespace. + +## Context and Problem Statement + +The portfolio has two related but distinct namespaces: `NWarila` and `nwarila-platform`. They share patterns, maintainers, and some type-template dependencies, but they are separate GitHub organizations with separate default community files, ADR baselines, repository policies, and reusable workflow control surfaces. + +Before this decision was written down, some `nwarila-platform/*` repositories called reusable workflows from `NWarila/.github`. That worked technically because the repositories are public, but it blurred ownership. A platform repository could be governed by an org control plane outside its namespace, and a change to `NWarila/.github` could affect platform repository checks without the platform namespace carrying the source policy itself. + +The same ambiguity appeared in drift-gate manifests. Files such as workflow callers and mirroring documentation have the same shape across templates, but their org-control-plane references must differ by namespace. Treating those files as byte-identical across namespaces makes the automated drift check fight the intended trust boundary. + +The portfolio needs a clear rule that keeps org governance local to the namespace while still allowing cross-namespace reuse of type-template assets where that is the deliberate architecture. + +## Decision Drivers + +1. **Ownership clarity.** A repository's org governance should be owned by the organization that owns the repository. +2. **Blast-radius control.** A change in one namespace's `.github` repository should not silently change another namespace's policy surface. +3. **Auditability.** Reviewers should be able to identify the authoritative org ADRs, community-health files, and workflow policies from the repository's owner/name. +4. **Useful automation.** Drift gates should detect real drift, not force namespace-specific files to pretend they are byte-identical. +5. **Template reuse.** Stack templates can still live in `NWarila` when they are explicitly type-template dependencies rather than org governance. + +## Considered Options + +1. Use a single shared `NWarila/.github` control plane for both namespaces. +2. Duplicate all templates and tools into each namespace, forbidding cross-namespace dependencies entirely. +3. Keep org control planes namespace-local, while allowing explicit cross-namespace type-template and tool dependencies. + +## Decision Outcome + +Chosen option: **Option 3, keep org control planes namespace-local while allowing explicit type-template and tool dependencies.** + +For repositories whose owner is `NWarila`: + +- Org-baseline ADRs are sourced from `NWarila/.github/docs/decision-records/`. +- Community-health files and org defaults are sourced from `NWarila/.github`. +- Org reusable workflow callers such as repo hygiene, CodeQL, IaC/security, Scorecard, release-please, and auto-merge call `NWarila/.github`. +- `repo-hygiene` callers set `source_ref` to the same `NWarila/.github` commit SHA used in the reusable workflow `uses:` reference. + +For repositories owned by another namespace, the same categories are sourced from that namespace's `.github` repository. A `nwarila-platform/*` repository therefore calls `nwarila-platform/.github` for org reusable workflows and mirrors org ADRs from `nwarila-platform/.github`. + +Cross-namespace references remain valid when they are not org control planes. Examples include type-template repositories, drift-gate itself, and framework reusable workflows that are intentionally published as stack templates. Those dependencies must still be pinned by full commit SHA where repo-hygiene requires it. + +Template manifests must distinguish byte-identical files from namespace-specific starter files. A file that embeds an org-control-plane repository name, such as `.github/workflows/security.yaml` or a mirroring reference document, is not byte-identical across namespaces unless the source and consumer share the same namespace. Such files belong in `scaffold_starter` or an equivalent non-byte-enforced propagation group. + +## Pros and Cons of the Options + +### Option 1: Single shared `NWarila/.github` + +- **Good, because** one repository carries all org reusable workflows and policy files. +- **Good, because** consumers have fewer source repositories to track. +- **Bad, because** `nwarila-platform/*` repositories would depend on a different namespace for org governance. +- **Bad, because** a `NWarila/.github` change could affect platform repositories without a platform-owned policy update. +- **Bad, because** it makes ADR mirrors and drift-gate source labels misleading. + +### Option 2: Duplicate all templates and tools per namespace + +- **Good, because** every dependency is namespace-local. +- **Good, because** blast radius is smallest. +- **Bad, because** type-template code and tools would be copied unnecessarily. +- **Bad, because** duplicated stack templates drift and multiply maintenance work. +- **Bad, because** it prevents deliberate reuse of mature templates. + +### Option 3: Namespace-local org control planes with explicit cross-namespace templates + +- **Good, because** org governance follows repository ownership. +- **Good, because** reusable workflow and ADR source labels match the owning namespace. +- **Good, because** drift-gate can enforce the right files exactly and allow the right files to vary. +- **Good, because** type-template reuse remains possible when that dependency is explicit. +- **Neutral, because** consumers must distinguish org-control-plane dependencies from type-template dependencies. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows RFC 2119 conventions. + +1. **Workflow namespace check.** A `NWarila/*` repository's reusable workflow calls to org governance workflows MUST use `NWarila/.github/.github/workflows/...` pinned by full commit SHA. +2. **Repo-hygiene source check.** A `NWarila/*` repo-hygiene caller MUST set `source_ref` to the same `NWarila/.github` commit SHA used in the workflow `uses:` reference. +3. **ADR mirror source check.** A `NWarila/*` repository that mirrors org ADRs MUST mirror them from `NWarila/.github`. +4. **Cross-namespace exception check.** A cross-namespace dependency MUST be recognizable as a type-template, tool, or other explicit non-org-control-plane dependency. +5. **Manifest classification check.** Type-template manifests SHOULD classify files that embed org-control-plane repository names as starter or customizable files unless the template and all consumers share the same namespace. +6. **Review rule.** Any PR that introduces a reusable workflow caller to another namespace's `.github` repository MUST explain why it is not an org-control-plane dependency, or it should be rejected. + +## Consequences + +### Positive + +- Org policy ownership is obvious from repository ownership. +- A namespace can harden its reusable workflows without unexpectedly changing another namespace's repositories. +- Drift-gate failures become more meaningful because namespace-specific files are not forced into byte identity. +- Cross-namespace type-template reuse remains available and explicit. + +### Negative + +- Equivalent reusable workflows may exist in more than one `.github` repository. +- Consumers that move between namespaces need an intentional workflow and ADR mirror repointing PR. +- Template authors must classify manifest entries more carefully. + +### Neutral + +- This ADR does not change the ADR format or the three-scope ADR model from ADR-0001. +- This ADR does not forbid `NWarila/*` repositories from consuming `nwarila-platform/*` tools if a later repository-specific decision justifies that dependency. +- This ADR documents an ownership rule that was already implicit in the separate org-baseline ADR sets. + +## Assumptions + +1. `NWarila/.github` and `nwarila-platform/.github` remain public or otherwise accessible to their consumers. +2. Each namespace continues to maintain its own org ADRs, community-health files, and reusable workflow baselines. +3. Type-template repositories may continue to live in one namespace while serving consumers in another namespace. +4. Repo-hygiene and drift-gate remain the main machine checks for workflow pinning and mirrored baseline drift. + +## Supersedes + +None. + +## Superseded by + +None (current). + +## Implementing PRs + +- [`NWarila/.github#18`](https://github.com/NWarila/.github/pull/18) removed the repo-hygiene advisory bypass and produced the current hard-fail org reusable baseline. +- [`NWarila/chiseled-application-template#13`](https://github.com/NWarila/chiseled-application-template/pull/13) pinned `NWarila` template workflow callers and org ADR sync to the current `NWarila/.github` baseline. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) establishes the org, template, and repository ADR scopes. +- [ADR-0004](0004-use-renovate-for-dependency-updates.md) establishes the same general principle for dependency-update baselines: stack-specific concerns belong to type-template baselines, not a single org-wide config. + +## Compliance Notes + +This decision supports configuration management and separation of duties. It keeps policy authority aligned with repository ownership and makes the source of CI, ADR, and community-health controls explicit in source control. It is not, by itself, a compliance claim. + +## Changelog + +| Date | Change | Reason | Author/Role | Body-diff? | +| ---------- | ----------------------------------------- | ------------------------------------------- | --------------------------------- | ---------- | +| 2026-06-02 | Refreshed metadata and removed final blank lint drift. | Apply ADR-0001 living metadata and keep Markdown lint green. | Portfolio maintainer / governance | Yes | diff --git a/docs/decision-records/org/0007-centralize-universal-ci-reusables-within-each-namespace.md b/docs/decision-records/org/0007-centralize-universal-ci-reusables-within-each-namespace.md new file mode 100644 index 0000000..99054b8 --- /dev/null +++ b/docs/decision-records/org/0007-centralize-universal-ci-reusables-within-each-namespace.md @@ -0,0 +1,156 @@ +# ADR-0007: Centralize Universal CI Reusables Within Each Namespace + +| Field | Value | +| ---------------- | --------------------------------------------------------------------------- | +| ID | ADR-0007 | +| Scope | Org baseline | +| Status | Accepted | +| Decision-subject | Namespace-local placement and calling rules for universal CI reusables. | +| Date accepted | 2026-06-02 | +| Date | 2026-06-02 | +| Last reviewed | 2026-06-02 | +| Authors | Nick Warila (@NWarila) | +| Decision-makers | Nick Warila (sole portfolio maintainer) | +| Consulted | Alignment audit findings from framework and runner-template migrations. | +| Informed | Maintainers of adopting repositories under `NWarila`. | +| Reversibility | Medium | +| Review-by | 2026-11-29 | + +## TL;DR + +Universal CI reusable workflows live once in the owning namespace's `.github` control plane and are called by full commit SHA from repositories in that namespace. For `NWarila/*` repositories, that means the universal org reusable workflows are authored in `NWarila/.github`. Stack-specific reusable workflows remain in their type-template repositories, and type-specific release-evidence workflows stay per template. This extends ADR-0006: centralization is namespace-local, not cross-namespace org governance. + +## Context and Problem Statement + +The alignment work found the same universal GitHub Actions reusable workflows copied across framework templates and consumers. Copies of CodeQL, IaC/security, Scorecard, release automation, auto-merge, and repo-hygiene logic created drift risk, multiplied review work, and made security fixes propagate only after every duplicate was updated. + +At the same time, ADR-0006 established that org control planes are namespace-local. A `NWarila/*` repository should not be governed by another namespace's `.github` repository, and repositories in another namespace should not depend on `NWarila/.github` for their org governance. + +The portfolio therefore needs a rule that removes duplicated universal CI logic while keeping policy ownership aligned with the namespace that owns the repository. + +## Decision Drivers + +1. **Single source of truth.** Universal CI behavior should be edited and reviewed in one place per namespace. +2. **Fast security response.** A reusable workflow fix should be available to every adopting repository through a reviewed SHA bump. +3. **Namespace ownership.** Org governance must follow ADR-0006 and stay in the owning namespace. +4. **Template clarity.** Type templates should carry stack-specific behavior, not duplicated org-wide behavior. +5. **Auditability.** A reviewer should be able to identify whether a workflow dependency is org governance or stack-specific reuse from its repository path. + +## Considered Options + +1. Keep duplicating universal reusable workflows into every template and consumer. +2. Use a single global `NWarila/.github` control plane for all namespaces. +3. Centralize universal reusable workflows once per namespace and keep stack-specific reusables in type templates. + +## Decision Outcome + +Chosen option: **Option 3, centralize universal reusable workflows once per namespace and keep stack-specific reusable workflows in type templates.** + +For `NWarila/*` repositories, the following reusable workflow families are universal org governance and are authored in `NWarila/.github/.github/workflows/`: + +- CodeQL analysis. +- IaC and secret scanning. +- OpenSSF Scorecard. +- Release automation. +- Auto-merge. +- Repo hygiene. + +Repositories call these reusable workflows with `uses: NWarila/.github/.github/workflows/.yaml@<40-character-sha>`. The SHA pin is reviewed like any other dependency update. A repository under another namespace follows the same pattern against that namespace's `.github` control plane. + +Stack-specific reusable workflows remain in the type-template that owns the stack. Examples include Terraform validation/deploy workflows, Packer framework build workflows, and Ansible framework run workflows. Type-specific release-evidence workflows also remain per template because their evidence gathering steps use different toolchains even when their envelope is similar. + +Templates and consumers must not reintroduce local copies of universal org reusable workflows. A local thin caller is acceptable when the caller belongs to the repository's own CI surface and invokes the namespace-local org reusable by SHA. + +## Pros and Cons of the Options + +### Option 1: Duplicate universal reusables everywhere + +- **Good, because** every repository can run without another reusable-workflow source. +- **Good, because** local review sees the full copied workflow body. +- **Bad, because** universal fixes must be copied into many repositories. +- **Bad, because** copies drift silently. +- **Bad, because** consumer manifests can balloon with files the consumer does not own. + +### Option 2: Use one global control plane + +- **Good, because** there is exactly one universal reusable source. +- **Good, because** security fixes have the smallest source footprint. +- **Bad, because** it violates ADR-0006 by making another namespace's `.github` repository govern local org policy. +- **Bad, because** blast radius crosses namespace boundaries. +- **Bad, because** ADR mirrors and drift labels become misleading for non-`NWarila` repositories. + +### Option 3: Centralize once per namespace and keep type-specific reusables in templates + +- **Good, because** duplicated universal workflow bodies disappear inside the namespace. +- **Good, because** org policy ownership remains namespace-local. +- **Good, because** type templates keep only the workflows that are genuinely stack-specific. +- **Good, because** consumers mirror fewer files and have clearer drift-gate obligations. +- **Neutral, because** equivalent reusable workflow families may exist in multiple namespaces. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows RFC 2119 conventions. + +1. **Workflow source check.** A `NWarila/*` repository's universal org workflow caller MUST call `NWarila/.github/.github/workflows/...` by full commit SHA. +2. **No-copy check.** Type templates and consumers MUST NOT carry local copies of universal org reusable workflows. +3. **Type-specific exception check.** Stack-specific reusable workflows MUST remain in their owning type-template unless a later ADR deliberately moves them. +4. **Release-evidence check.** Release-evidence workflows MAY remain per template when the evidence core depends on the template's toolchain. +5. **Manifest check.** Baseline manifests SHOULD classify universal org reusable workflow bodies as org-controlled source files, not consumer-mirrored template files. +6. **Namespace check.** A repository under another namespace MUST use that namespace's `.github` control plane for org governance unless a repository-specific ADR documents a narrow exception. + +## Consequences + +### Positive + +- Universal CI policy has one source per namespace. +- Framework and runner templates carry less duplicated code. +- Security workflow fixes are easier to review and propagate. +- Drift-gate manifests are smaller and easier to reason about. + +### Negative + +- A bad reusable workflow update can affect many repositories after they bump their pins. +- Each namespace must maintain its own `.github` control plane for org governance. +- Consumers need disciplined SHA-bump review instead of local body edits. + +### Neutral + +- This ADR does not remove stack-specific reusable workflows from type templates. +- This ADR does not require cross-namespace duplication of template repositories. +- This ADR depends on ADR-0006 for namespace ownership boundaries. + +## Assumptions + +1. Reusable workflow callers remain SHA-pinned by repo-hygiene policy. +2. Namespace `.github` repositories remain accessible to repositories in that namespace. +3. Type templates remain the home for stack-specific workflow behavior. +4. Drift-gate remains the mechanism for mirrored baseline files. + +## Supersedes + +None. + +## Superseded by + +None (current). + +## Implementing PRs + +None yet; this ADR records the accepted governance rule that existing and future alignment PRs enforce. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) defines org, template, and repository ADR scopes. +- [ADR-0006](0006-keep-github-control-planes-namespace-local.md) requires org control planes to stay namespace-local. +- [ADR-0008](0008-enforce-repo-hygiene-by-repo-type.md) defines how repositories enforce the policy around these callers. +- [ADR-0009](0009-classify-baseline-manifest-byte-identity.md) defines how reusable workflow and caller files are classified in manifests. + +## Compliance Notes + +This decision supports supply-chain governance by centralizing reusable CI logic, requiring SHA-pinned callers, and keeping policy ownership aligned with repository ownership. It is not itself a compliance certification. + +## Changelog + +| Date | Change | Reason | Author/Role | Body-diff? | +| ---------- | ----------------------------------------- | ------------------------------------------- | --------------------------------- | ---------- | +| 2026-06-02 | Accepted namespace-local centralization for universal CI reusable workflows. | Extract durable alignment doctrine from the org workflow migration program. | Portfolio maintainer / governance | Yes | diff --git a/docs/decision-records/org/0008-enforce-repo-hygiene-by-repo-type.md b/docs/decision-records/org/0008-enforce-repo-hygiene-by-repo-type.md new file mode 100644 index 0000000..37a735f --- /dev/null +++ b/docs/decision-records/org/0008-enforce-repo-hygiene-by-repo-type.md @@ -0,0 +1,146 @@ +# ADR-0008: Enforce Repo Hygiene by Repo Type + +| Field | Value | +| ---------------- | --------------------------------------------------------------------------- | +| ID | ADR-0008 | +| Scope | Org baseline | +| Status | Accepted | +| Decision-subject | Mechanisms for applying repo-hygiene policy across repository types. | +| Date accepted | 2026-06-02 | +| Date | 2026-06-02 | +| Last reviewed | 2026-06-02 | +| Authors | Nick Warila (@NWarila) | +| Decision-makers | Nick Warila (sole portfolio maintainer) | +| Consulted | Framework-template, runner-template, and consumer CI alignment findings. | +| Informed | Maintainers of adopting repositories under `NWarila`. | +| Reversibility | Medium | +| Review-by | 2026-11-29 | + +## TL;DR + +Every repository that carries GitHub workflow policy must run the same repo-hygiene rules, but the correct wiring depends on the repository type. Repositories with a local `make ci` or equivalent verification harness that evaluates the repo-hygiene policy over their own workflows do not need a standalone `repo-hygiene.yaml` caller. Repositories without that local policy path, including data-only runner templates and runner consumers, must carry a thin `repo-hygiene.yaml` caller to the namespace-local reusable workflow. + +## Context and Problem Statement + +The repo-hygiene policy enforces controls that are shared across the portfolio: workflow `uses:` pinning, privileged `pull_request_target` restrictions, and exact Terraform pinning when Terraform version files exist. The same policy applies to framework templates, runner templates, and consumers, but those repositories do not all have the same internal shape. + +Framework templates already run local CI harnesses that execute repository validators and policy checks as part of `make ci` or an equivalent verification command. Data-only runner templates and their consumers intentionally avoid carrying local tooling, tests, and policy directories. Requiring those data-only repositories to add a full local toolchain would violate their contract, while omitting repo-hygiene entirely would leave privileged workflow safety unenforced. + +The organization needs a single doctrine for enforcement that avoids both false gaps and cargo-cult tooling. + +## Decision Drivers + +1. **Uniform policy.** SHA pinning and privileged-workflow safety should be enforced everywhere. +2. **Repo-type fit.** Enforcement wiring should match the repository's contract and not force data-only repositories to carry local tooling. +3. **No duplicate gates.** A repository should not need both a local policy evaluation and a standalone caller when one already covers the current workflow tree. +4. **Review clarity.** The absence or presence of `repo-hygiene.yaml` should be explainable from the repo type. +5. **Runtime proof.** The reusable policy path must be smoke-tested in its source control plane. + +## Considered Options + +1. Require every repository to carry a standalone `repo-hygiene.yaml` caller. +2. Require every repository to carry local policy tooling and run it in `make ci`. +3. Select the enforcement mechanism by repository type while keeping the policy itself uniform. + +## Decision Outcome + +Chosen option: **Option 3, select the enforcement mechanism by repository type while keeping the policy itself uniform.** + +Repositories that already evaluate repo-hygiene through their local verification harness do not carry a standalone repo-hygiene caller. This is the expected shape for framework templates when their `make ci`, `tools/verify.py`, or equivalent command evaluates the repo-hygiene policy against the repository's own workflows. + +Repositories without that local policy path must carry `.github/workflows/repo-hygiene.yaml` as a thin caller to the namespace-local reusable repo-hygiene workflow. This is the expected shape for data-only runner templates, runner consumers, and other repositories whose contract deliberately avoids local policy tooling. + +The policy semantics remain the same. The choice is only the invocation mechanism. A repository-specific exception is allowed only when the repository has no applicable workflow surface or a documented equivalent gate that evaluates the same policy against the current tree. Exceptions must not weaken SHA pinning, privileged workflow restrictions, or Terraform exact-pin rules where applicable. + +The source `.github` repository must smoke-test the reusable repo-hygiene workflow against itself so runtime failures in the reusable are caught before consumers pin it. + +## Pros and Cons of the Options + +### Option 1: Standalone caller everywhere + +- **Good, because** every repository has the same visible workflow caller. +- **Good, because** data-only repositories stay lean. +- **Bad, because** framework templates that already run the policy would duplicate a gate. +- **Bad, because** reviewers may mistake duplicate checks for stronger assurance. + +### Option 2: Local tooling everywhere + +- **Good, because** every repository evaluates policy the same local way. +- **Good, because** policy tests can sit beside policy code. +- **Bad, because** data-only repositories would need to carry tools and policies they do not otherwise own. +- **Bad, because** runner manifests would become bloated with template-internal files. + +### Option 3: Mechanism by repo type + +- **Good, because** the same policy applies everywhere. +- **Good, because** each repository uses the invocation path that fits its contract. +- **Good, because** standalone caller absence is not misclassified as a gap when local CI already evaluates the policy. +- **Good, because** data-only repositories stay data-only. +- **Neutral, because** auditors must look at either local CI or the standalone caller depending on repository type. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows RFC 2119 conventions. + +1. **Local harness check.** A repository that omits `.github/workflows/repo-hygiene.yaml` MUST have a local CI path that evaluates the repo-hygiene policy against its own workflow tree, or it must document why the policy is not applicable. +2. **Caller check.** A repository without that local policy path MUST carry a thin `repo-hygiene.yaml` caller to the namespace-local reusable workflow. +3. **Policy equivalence check.** Alternate local wiring MUST evaluate the same repo-hygiene policy semantics rather than a weaker subset. +4. **Data-only check.** Data-only runner templates and consumers SHOULD use the reusable caller instead of local policy tooling. +5. **Smoke-test check.** The namespace `.github` repository MUST smoke-test the reusable repo-hygiene workflow against itself. +6. **Review check.** PRs that add privileged workflow behavior MUST show that repo-hygiene covers the changed workflow. + +## Consequences + +### Positive + +- Repo-hygiene enforcement matches repository shape. +- Data-only repositories avoid unnecessary local tooling. +- Framework templates avoid duplicate checks when their harness already evaluates the policy. +- Reviewers get a durable rule for deciding whether a missing caller is acceptable. + +### Negative + +- Enforcement wiring is not visually identical across every repository. +- Reviewers must know the repository type before judging the expected shape. +- Exceptions need documentation to avoid becoming quiet policy gaps. + +### Neutral + +- This ADR does not change the repo-hygiene policy body. +- This ADR does not change which workflow patterns repo-hygiene denies. +- This ADR works together with ADR-0007's namespace-local reusable placement. + +## Assumptions + +1. Repo-hygiene remains the shared policy for workflow pinning and privileged workflow safety. +2. Framework templates continue to expose a local CI harness that can evaluate policies. +3. Runner templates and many runner consumers remain intentionally lean. +4. The reusable repo-hygiene workflow remains smoke-tested in the namespace control plane. + +## Supersedes + +None. + +## Superseded by + +None (current). + +## Implementing PRs + +None yet; this ADR records the accepted enforcement doctrine that existing and future alignment PRs apply. + +## Related ADRs + +- [ADR-0006](0006-keep-github-control-planes-namespace-local.md) defines the namespace-local control plane for the reusable workflow. +- [ADR-0007](0007-centralize-universal-ci-reusables-within-each-namespace.md) defines where the universal reusable workflow lives. +- [ADR-0009](0009-classify-baseline-manifest-byte-identity.md) explains why data-only repositories should not mirror local policy tooling they do not run. + +## Compliance Notes + +This decision supports consistent CI control enforcement while preserving repository-type contracts. It provides an auditable rule for why the policy may be invoked through different mechanisms without weakening the policy itself. + +## Changelog + +| Date | Change | Reason | Author/Role | Body-diff? | +| ---------- | ----------------------------------------- | ------------------------------------------- | --------------------------------- | ---------- | +| 2026-06-02 | Accepted repo-type-specific invocation of the shared repo-hygiene policy. | Extract durable enforcement doctrine from framework and runner alignment work. | Portfolio maintainer / governance | Yes | diff --git a/docs/decision-records/org/0009-classify-baseline-manifest-byte-identity.md b/docs/decision-records/org/0009-classify-baseline-manifest-byte-identity.md new file mode 100644 index 0000000..2179d66 --- /dev/null +++ b/docs/decision-records/org/0009-classify-baseline-manifest-byte-identity.md @@ -0,0 +1,157 @@ +# ADR-0009: Classify Baseline Manifest Byte Identity + +| Field | Value | +| ---------------- | --------------------------------------------------------------------------- | +| ID | ADR-0009 | +| Scope | Org baseline | +| Status | Accepted | +| Decision-subject | Classification rules for byte-identical baseline-manifest entries. | +| Date accepted | 2026-06-02 | +| Date | 2026-06-02 | +| Last reviewed | 2026-06-02 | +| Authors | Nick Warila (@NWarila) | +| Decision-makers | Nick Warila (sole portfolio maintainer) | +| Consulted | Drift-gate findings from framework, runner, and consumer alignment PRs. | +| Informed | Maintainers of adopting repositories under `NWarila`. | +| Reversibility | Medium | +| Review-by | 2026-11-29 | + +## TL;DR + +Baseline manifests use byte identity only for files that are truly uniform across the target repositories. Fleet-canonical community files, org ADR mirrors, docs skeleton sentinels, and stable org reference docs may be byte-identical. Repo-customizable configs, namespace-specific workflow callers, repo-specific docs, and template-internal test fixtures should be starters, existence checks, or local files rather than byte-enforced mirrors. The rule is simple: consumers mirror what they actually run or inherit as governance; templates keep what only templates run. + +## Context and Problem Statement + +The alignment program found that some baseline manifests were too broad. They treated reusable workflow bodies, local tools, tests, policies, and repo-customizable configs as byte-identical consumer obligations. That made consumers mirror files they did not invoke, discouraged useful local improvements, and turned drift-gate into a cargo-cult compliance check instead of a precise governance check. + +The same problem appears at the documentation layer. Some documents are fleet-canonical and should be mirrored exactly, such as org ADRs and shared governance references. Other documents are repo-specific or namespace-specific and should not be forced into byte identity. + +The organization needs a durable classification rule so future manifests stay small, meaningful, and enforceable. + +## Decision Drivers + +1. **Meaningful drift.** Drift-gate should fail only when a repository diverges from something it truly inherits. +2. **Consumer ergonomics.** Consumers should not mirror tools, tests, or policies they never run. +3. **Local maturity.** Repositories should be able to improve configs and docs that are intentionally local. +4. **Namespace correctness.** Files that embed namespace-local control-plane paths should not be byte-identical across namespaces. +5. **Recruiter clarity.** A repository should look intentional, not like a dumped copy of a template's internals. + +## Considered Options + +1. Put every useful template file into `byte_identical`. +2. Treat all files as starter material and avoid byte identity. +3. Use byte identity only for files that are uniform governance, and classify everything else by how it is actually used. + +## Decision Outcome + +Chosen option: **Option 3, use byte identity only for files that are uniform governance, and classify everything else by how it is actually used.** + +Files belong in `byte_identical` when all of the following are true: + +- The file is intentionally the same for every target repository in the manifest scope. +- The target repository either inherits the file as governance or directly uses it in its own lifecycle. +- A local edit would be drift rather than maturity. +- The file does not embed namespace-specific or repo-specific values unless the manifest scope is limited to that namespace or repository. + +Files do not belong in `byte_identical` when any of the following are true: + +- The file is template-internal tooling, policy, tests, or fixtures that consumers do not run. +- The file is a repo-customizable config such as editor, lint, or hook configuration where mature consumers may have richer local variants. +- The file is a workflow caller that embeds a namespace-local `.github` repository path and the manifest applies across namespaces. +- The file is repo-specific documentation, diagram content, inventory, or runtime evidence. + +Those files should instead be classified as starter material, scaffold content, source-existence checks, shape checks, or local repository files, depending on how the repository uses them. + +## Pros and Cons of the Options + +### Option 1: Byte-enforce every useful template file + +- **Good, because** consumers start with a complete copy of the template's working tree. +- **Good, because** drift-gate can detect any deviation. +- **Bad, because** consumers inherit files they do not run. +- **Bad, because** mature local configs are downgraded to template defaults. +- **Bad, because** namespace-specific files are forced into false uniformity. + +### Option 2: Avoid byte identity + +- **Good, because** repositories can adapt freely. +- **Good, because** manifests are less likely to block local maturity. +- **Bad, because** inherited governance can silently drift. +- **Bad, because** org ADR mirrors and community-health files lose mechanical protection. +- **Bad, because** reviewers must manually spot differences that automation should catch. + +### Option 3: Classify by actual use + +- **Good, because** inherited governance stays exact. +- **Good, because** consumers are not forced to carry template internals. +- **Good, because** repo-specific docs and configs can mature locally. +- **Good, because** namespace-local control-plane paths are handled honestly. +- **Neutral, because** manifest authors must decide classification deliberately. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows RFC 2119 conventions. + +1. **Use test.** A manifest entry marked byte-identical MUST describe a file that the target repository inherits as governance or directly uses in its lifecycle. +2. **Uniformity test.** A byte-identical file MUST be intended to have the same bytes across the manifest's target scope. +3. **Customization test.** Repo-customizable configs SHOULD be starter or scaffold entries rather than byte-identical entries. +4. **Namespace test.** Files embedding namespace-local `.github` paths MUST NOT be byte-identical across namespaces. +5. **Docs test.** Fleet-canonical docs MAY be byte-identical; repo-specific docs and diagrams SHOULD be governed by existence, shape, or local review. +6. **Review test.** PRs that add broad byte-identical entries SHOULD explain why local divergence would be drift rather than maturity. + +## Consequences + +### Positive + +- Baseline manifests stay smaller and more truthful. +- Consumers mirror only files they actually inherit or run. +- Mature repositories can keep richer local configs. +- Drift-gate failures carry clearer signal. + +### Negative + +- Manifest authors must classify files instead of copying whole directories. +- Some starter files may drift locally without byte-level automation. +- Existing bloated manifests need cleanup PRs to comply. + +### Neutral + +- This ADR does not remove drift-gate. +- This ADR does not weaken existing byte-identical entries that are truly uniform governance. +- This ADR applies to documentation and configuration as well as workflow files. + +## Assumptions + +1. Drift-gate remains the primary byte-identity enforcement mechanism. +2. Templates continue to carry starter or scaffold categories for seed files. +3. Repositories may add local docs and configs beyond inherited governance. +4. Namespace-local control-plane doctrine from ADR-0006 remains in force. + +## Supersedes + +None. + +## Superseded by + +None (current). + +## Implementing PRs + +None yet; this ADR records the accepted classification rule that future manifest cleanups apply. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) defines mirrored org, template, and repo ADR scopes. +- [ADR-0006](0006-keep-github-control-planes-namespace-local.md) defines namespace-local control-plane ownership. +- [ADR-0007](0007-centralize-universal-ci-reusables-within-each-namespace.md) explains why universal reusable bodies do not belong in type-template consumer manifests. +- [ADR-0008](0008-enforce-repo-hygiene-by-repo-type.md) explains why data-only repositories should not mirror local policy tooling. + +## Compliance Notes + +This decision supports configuration management by making inherited baseline files explicit and limiting byte-level enforcement to files that are meant to be identical. It reduces false compliance artifacts while preserving auditable governance mirrors. + +## Changelog + +| Date | Change | Reason | Author/Role | Body-diff? | +| ---------- | ----------------------------------------- | ------------------------------------------- | --------------------------------- | ---------- | +| 2026-06-02 | Accepted byte-identity classification rules for baseline manifests. | Extract durable manifest lessons from framework, runner, and consumer alignment work. | Portfolio maintainer / governance | Yes | diff --git a/docs/decision-records/org/0010-keep-ai-attribution-out-of-version-control.md b/docs/decision-records/org/0010-keep-ai-attribution-out-of-version-control.md new file mode 100644 index 0000000..dbd4565 --- /dev/null +++ b/docs/decision-records/org/0010-keep-ai-attribution-out-of-version-control.md @@ -0,0 +1,159 @@ +# ADR-0010: Keep AI Attribution Out of Version Control + +| Field | Value | +| ---------------- | --------------------------------------------------------------------------- | +| ID | ADR-0010 | +| Scope | Org baseline | +| Status | Accepted | +| Decision-subject | Attribution hygiene for commits, code, documentation, and generated assets. | +| Date accepted | 2026-06-02 | +| Date | 2026-06-02 | +| Last reviewed | 2026-06-02 | +| Authors | Nick Warila (@NWarila) | +| Decision-makers | Nick Warila (sole portfolio maintainer) | +| Consulted | Repository cleanup findings from public portfolio polishing work. | +| Informed | Maintainers of adopting repositories under `NWarila`. | +| Reversibility | Low | +| Review-by | 2026-11-29 | + +## TL;DR + +Version-controlled artifacts in `NWarila/*` repositories must read as maintainer-owned work. Commits, code, documentation, templates, and generated assets must not include assistant-tool bylines, generated-by footers, automated coauthor trailers, or similar public attribution residue. The rule is about source-control hygiene and audience clarity; it does not forbid using tools during private drafting or implementation. A CI gate scans the tree and PR commit messages for known attribution markers. History rewrites are allowed only as a narrowly approved break-glass cleanup when public residue cannot be corrected by an ordinary follow-up commit. + +## Context and Problem Statement + +The portfolio is intended to be recruiter-readable, learner-readable, and maintainer-readable. Public source history should show the maintainer's engineering judgment, not scattered drafting-tool metadata. Tool bylines and generated footers are noisy in code review, make polished repositories look unfinished, and can create confusion about who owns the design. + +The same residue can appear in several places: commit messages, documentation footers, generated asset comments, pull request body drafts copied into files, and rewritten-history cleanup notes. Because the pattern is easy to miss during manual review, it needs both a policy decision and a small automated check. + +This decision does not hide collaboration or misrepresent authorship. It sets the standard that public version-controlled artifacts are curated maintainer outputs. Drafting transcripts, prompts, and work logs belong in temporary scratch space, not in repositories. + +## Decision Drivers + +1. **Maintainer ownership.** Public source should represent intentional maintainer-authored repository state. +2. **Recruiter clarity.** Portfolio repositories should not advertise private drafting mechanics. +3. **Review signal.** Tool attribution footers and trailers distract from the engineering change. +4. **Repeatability.** A small scan can catch common residue before it merges. +5. **Cleanup discipline.** History rewriting should be rare, explicit, and gated. + +## Considered Options + +1. Allow assistant-tool attribution residue when it is accurate. +2. Rely on manual review to remove residue. +3. Forbid residue in version control and enforce the rule with a CI scan plus a narrow break-glass cleanup runbook. + +## Decision Outcome + +Chosen option: **Option 3, forbid attribution residue in version control and enforce the rule with a CI scan plus a narrow break-glass cleanup runbook.** + +Repositories must not commit assistant-tool attribution markers in tracked files or PR commit messages. Examples of disallowed marker categories include generated-by footers, assistant-tool bylines, automated coauthor trailers, tool-branded comments, and copied prompt or transcript headers. + +The rule applies to: + +- Commit subjects and bodies in the PR range. +- Source code and scripts. +- Markdown documentation. +- Workflow files, templates, and configuration. +- Generated assets when those assets are checked into source control. + +The rule does not apply to: + +- Private scratch files outside repositories. +- Local notes that are never committed. +- Ordinary references to machine learning, automation, or artificial intelligence when they are the subject matter of a repository or decision. +- Dependency names or vulnerability reports when the term is part of the real technical artifact being handled. + +When residue is found before merge, the normal fix is to amend or replace the PR commit and remove the tracked text. When residue already exists on public protected history and cannot be corrected without leaving the residue visible, a history rewrite may be approved as a break-glass exception. Such a rewrite must be scoped to the affected repository, announced to affected maintainers, and followed by force-push recovery instructions. + +## Pros and Cons of the Options + +### Option 1: Allow attribution residue + +- **Good, because** it avoids cleanup work. +- **Good, because** it may preserve details about drafting tools. +- **Bad, because** public repositories look less curated. +- **Bad, because** the residue distracts from maintainer judgment. +- **Bad, because** copied tool metadata can leak private drafting context. + +### Option 2: Manual review only + +- **Good, because** no automation is needed. +- **Good, because** reviewers can consider context. +- **Bad, because** residue is easy to miss. +- **Bad, because** the same issue recurs across repositories. +- **Bad, because** cleanup after merge is more disruptive than pre-merge rejection. + +### Option 3: Policy plus CI gate + +- **Good, because** common residue is caught before merge. +- **Good, because** the public standard is explicit. +- **Good, because** the break-glass rewrite path is constrained. +- **Good, because** private tool use remains outside the repository standard. +- **Neutral, because** rare false positives need reviewer judgment. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows RFC 2119 conventions. + +1. **Tree scan.** CI MUST scan tracked text files for known assistant-tool attribution markers. +2. **Commit-message scan.** Pull request CI SHOULD scan commit subjects and bodies in the PR range for the same marker categories. +3. **Review check.** Reviewers SHOULD reject copied prompt logs, transcript headers, bylines, generated-by footers, and assistant coauthor trailers in repository files. +4. **Scratch-space check.** Temporary prompts, logs, and PR-body drafts SHOULD stay in scratch paths outside repositories. +5. **Break-glass check.** Any history rewrite to purge merged residue MUST be explicitly approved, narrowly scoped, and followed by recovery instructions. +6. **False-positive check.** If a repository legitimately discusses a term that resembles an attribution marker, the exception MUST be documented in the residue checker or repository-specific ADR. + +## Consequences + +### Positive + +- Public repositories read as curated maintainer-owned work. +- Common residue is caught before it merges. +- Cleanup expectations are explicit. +- The portfolio avoids advertising private drafting mechanics. + +### Negative + +- Contributors must clean commit messages and files before merge. +- The scanner may need maintenance as new residue forms appear. +- Rare already-published cleanup may require disruptive history surgery. + +### Neutral + +- This ADR does not forbid using tools privately during implementation. +- This ADR does not change commit-signing, review, or branch-protection rules. +- This ADR does not require rewriting history unless the owner approves a specific cleanup. + +## Assumptions + +1. Public repository history is part of the portfolio's presentation surface. +2. Contributors can keep temporary drafting artifacts outside repositories. +3. The initial scanner catches known marker families and can be extended. +4. Break-glass history rewrites remain rare. + +## Supersedes + +None. + +## Superseded by + +None (current). + +## Implementing PRs + +None yet; this ADR is implemented by the residue checker and CI wiring added in the same governance change. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) defines ADR traceability and living changelog rules. +- [ADR-0002](0002-adopt-diataxis-documentation-framework.md) keeps operational cleanup procedure in a runbook rather than in the ADR body. +- [ADR-0009](0009-classify-baseline-manifest-byte-identity.md) defines which shared governance docs are mirrored exactly. + +## Compliance Notes + +This decision supports source-control hygiene, authorship clarity, and public portfolio presentation. It does not make a legal authorship claim and does not replace normal review, signing, or branch-protection controls. + +## Changelog + +| Date | Change | Reason | Author/Role | Body-diff? | +| ---------- | ----------------------------------------- | ------------------------------------------- | --------------------------------- | ---------- | +| 2026-06-02 | Accepted version-control attribution hygiene and the residue scanning requirement. | Extract durable cleanup doctrine from public portfolio polishing work. | Portfolio maintainer / governance | Yes | diff --git a/docs/decision-records/repo/.gitkeep b/docs/decision-records/repo/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/decision-records/template/.gitkeep b/docs/decision-records/template/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/decision-records/0001-scripts-are-standalone-and-stdlib-only.md b/docs/decision-records/template/0001-scripts-are-standalone-and-stdlib-only.md similarity index 100% rename from docs/decision-records/0001-scripts-are-standalone-and-stdlib-only.md rename to docs/decision-records/template/0001-scripts-are-standalone-and-stdlib-only.md diff --git a/docs/decision-records/0002-pull-based-manifest-driven-template-sync.md b/docs/decision-records/template/0002-pull-based-manifest-driven-template-sync.md similarity index 100% rename from docs/decision-records/0002-pull-based-manifest-driven-template-sync.md rename to docs/decision-records/template/0002-pull-based-manifest-driven-template-sync.md diff --git a/docs/explanation/why-two-layer-pull-sync.md b/docs/explanation/why-two-layer-pull-sync.md new file mode 100644 index 0000000..3a9a8ba --- /dev/null +++ b/docs/explanation/why-two-layer-pull-sync.md @@ -0,0 +1,52 @@ +# Explanation: why two-layer pull sync + +This template is one layer in a larger organization standard. `NWarila/.github` +owns organization-wide governance, while `NWarila/python-template` owns the +Python-specific quality gate. Downstream repositories keep their own product +code, metadata, release workflows, and repo-specific docs. + +## The two layers + +The org layer provides baseline policy: community health files, org ADRs, +repo-hygiene checks, workflow templates, and non-language-specific automation. +Those files should be identical only when they are truly shared governance. + +The Python-template layer provides the Python developer experience: QA scripts, +the reusable Python CI workflow, setup scripts, sync metadata, and starter +configuration. These are stack-specific, so they live in this repository rather +than in the org control plane. + +## Why downstream repos pull + +Push-based sync would require this template to hold write credentials for every +consumer repository. That makes the template a deployment authority and turns a +standards repo into a cross-repo control point. + +Pull-based sync keeps ownership where it belongs. Each downstream repo owns a +thin workflow that calls this template's `self-update.yml`, receives a normal PR +with the synced files, reviews the change, and merges on its own cadence. + +## Why the scripts are copied + +Local development and CI should execute the same files. If CI called hidden +template logic while developers ran local copies, drift would be inevitable. +Copying `.github/scripts/` into each downstream repo makes the contract visible: +the local command, VSCode tasks, pre-commit hooks, and reusable workflow all +converge on the same script surface. + +## Why the manifest matters + +`sync-manifest.json` is the reviewable map of what the template owns. It keeps +source paths, destination paths, and merge modes out of workflow shell logic. +That makes sync changes auditable: adding a managed file, changing a target, or +switching a file from full overwrite to marker-preserving merge is a normal +diff in one machine-readable place. + +## How this repo dogfoods the model + +`scripts/` is the source implementation. `.github/scripts/` is the released +copy. This template's CI runs the released copy so the repo continuously tests +the same artifact shape downstream consumers receive. + +The flow is recorded in +[docs/diagrams/qa-template-sync-flow.mmd](../diagrams/qa-template-sync-flow.mmd). diff --git a/docs/how-to/add-a-qa-check.md b/docs/how-to/add-a-qa-check.md new file mode 100644 index 0000000..6c60230 --- /dev/null +++ b/docs/how-to/add-a-qa-check.md @@ -0,0 +1,57 @@ +# How-to: add a QA check script + +Goal: add a new `check_*.py` gate without breaking the local/CI contract. + +1. **Create the source script.** Add a standalone file under `scripts/`: + + ```text + scripts/check_example.py + ``` + + Keep it stdlib-only. Duplicate small helpers locally instead of importing + from another check script. + +2. **Give it a clear CLI.** Follow the existing shape: + + - parse arguments with `argparse` + - read standard `pyproject.toml` sections when configuration is needed + - shell out to the underlying tool with `subprocess` + - return `0` on pass and nonzero on failure + - print a GitHub Actions annotation when `GITHUB_ACTIONS=true` + +3. **Add tests.** Cover the script in `tests/`, mocking subprocess calls so the + tests exercise orchestration logic without requiring the external tool to + fail on purpose. + +4. **Wire local discovery.** `scripts/qa.py` discovers `check_*.py` files in its + own directory. A new source script will be picked up automatically when + `python scripts/qa.py` runs from this repository. + +5. **Add sync metadata.** Add the source-to-destination mapping to + `sync-manifest.json`: + + ```json + { "src": "scripts/check_example.py", "dest": ".github/scripts/check_example.py", "mode": "overwrite" } + ``` + +6. **Update CI intentionally.** If the check should be its own CI job, add that + job to `.github/workflows/python-qa.yml` and include it in the `ci-passed` + dependency list. For this template's self-dogfooding CI, keep the released + copy in `.github/scripts/` byte-identical to `scripts/` when the workflow + needs to execute the new check before the next release sync. + +7. **Validate.** + + ```bash + python scripts/qa.py + ``` + + Also run any org gate tools that cover the changed file type. + +## Notes + +- Do not add a shared helper module for check scripts; the template ADRs keep + the scripts standalone. +- Prefer configuration already owned by common tools, such as `[tool.ruff]`, + `[tool.mypy]`, or `[tool.pytest.ini_options]`, over a template-specific + config namespace. diff --git a/docs/GITHUB_TOKEN_LIMITATION.md b/docs/reference/github-token-limitation.md similarity index 100% rename from docs/GITHUB_TOKEN_LIMITATION.md rename to docs/reference/github-token-limitation.md diff --git a/docs/reference/qa-contract.md b/docs/reference/qa-contract.md new file mode 100644 index 0000000..96591fe --- /dev/null +++ b/docs/reference/qa-contract.md @@ -0,0 +1,72 @@ +# Reference: QA contract + +This template provides a Python quality gate that runs the same script surface +locally and in CI. The scripts are source-controlled, synced into downstream +repositories, and configured through ordinary `pyproject.toml` tool sections. + +## Script locations + +| Location | Role | +| --- | --- | +| `scripts/` | Canonical source for this template's QA scripts and sync helper. | +| `.github/scripts/` | Released copy used by this template's self-dogfooding CI and by downstream sync. | +| Downstream `.github/scripts/` | Synced copy that local developers and CI both execute. | + +The two script trees in this repository should contain byte-identical copies of +every script listed in `sync-manifest.json`. The deliberate extra file in +`.github/scripts/` is `.version`, which records the release tag currently +pulled into the released-copy tree. + +## Checks + +| Check | Script | Primary tool | Configuration source | +| --- | --- | --- | --- | +| Lint and format | `check_lint.py` | Ruff | `[tool.ruff]` | +| Type checking | `check_types.py` | mypy | `[tool.mypy]` and `[tool.ruff].src` | +| Tests and coverage | `check_tests.py` | pytest, pytest-cov | `[tool.pytest.ini_options]` | +| Security | `check_security.py` | pip-audit | Installed environment | +| Spelling | `check_spelling.py` | codespell | `[tool.codespell]` | +| Packaging | `check_package.py` | validate-pyproject, build, twine | `[build-system]`, `[project.scripts]` | +| Orchestration | `qa.py` | Check scripts above | CLI flags and project metadata | + +## Pyproject inference + +| Script behavior | Inferred from | Default when absent | +| --- | --- | --- | +| Source paths for lint and types | `[tool.ruff] src` | `["src"]` | +| Test paths | `[tool.pytest.ini_options] testpaths` | `["tests"]` | +| Coverage threshold | `--cov-fail-under` in pytest addopts | 90 in the reference config | +| Strict typing | `[tool.mypy] strict` | `true` in the reference config | +| Package check | `[build-system]` exists | Skip when absent | +| Entry-point smoke tests | `[project.scripts]` | Skip when absent | +| Codespell ignores | `[tool.codespell]` | No template-specific ignore list | + +## Local command + +Run all checks: + +```bash +python .github/scripts/qa.py +``` + +Run auto-fixable checks: + +```bash +python .github/scripts/qa.py --fix +``` + +Skip one or more checks for a local diagnostic run: + +```bash +python .github/scripts/qa.py --skip package --skip security +``` + +CI remains the source of truth; local skips are for diagnosis, not for lowering +the repository bar. + +## CI contract + +Downstream repositories call +`NWarila/python-template/.github/workflows/python-qa.yml@v1`. The reusable +workflow checks out the caller repository and runs the caller's synced +`.github/scripts/` files. The stable aggregate status is `ci-passed`. diff --git a/docs/runbooks/refresh-released-scripts.md b/docs/runbooks/refresh-released-scripts.md new file mode 100644 index 0000000..ab78887 --- /dev/null +++ b/docs/runbooks/refresh-released-scripts.md @@ -0,0 +1,62 @@ +# Runbook: refresh released script copies + +Use this runbook when `.github/scripts/` needs to be brought back in line with +the current `scripts/` source tree during template maintenance. + +## Purpose + +`scripts/` is the source implementation. `.github/scripts/` is the released copy +used by self-dogfooding CI. The normal path updates `.github/scripts/` through a +release and `self-update.yml`; this runbook covers manual verification or a +small repair before opening a PR. + +## Prerequisites + +- Work from a clean branch. +- Do not change toolchain pins as part of the refresh. +- Keep `.github/scripts/.version` as the only deliberate extra file in the + released-copy tree. + +## Procedure + +1. Compare script names: + + ```powershell + Compare-Object ` + (Get-ChildItem scripts -File | Sort-Object Name | ForEach-Object Name) ` + (Get-ChildItem .github/scripts -File | + Where-Object Name -ne '.version' | + Sort-Object Name | + ForEach-Object Name) + ``` + +2. Compare script content: + + ```powershell + foreach ($file in Get-ChildItem scripts -File) { + $copy = Join-Path '.github/scripts' $file.Name + if ((Get-FileHash $file.FullName).Hash -ne (Get-FileHash $copy).Hash) { + Write-Output "content drift: $($file.Name)" + } + } + ``` + +3. If a copied script is stale, copy from `scripts/` to `.github/scripts/` and + rerun the comparison. + +4. Run validation: + + ```bash + python scripts/qa.py + ``` + +## Verification + +- The comparison prints no missing files and no content drift. +- `.github/scripts/.version` still exists. +- `python scripts/qa.py` passes. + +## Rollback + +If the refresh copied the wrong source, restore `.github/scripts/` from the +branch base and rerun the comparison before continuing. diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md new file mode 100644 index 0000000..f2c9777 --- /dev/null +++ b/docs/tutorials/getting-started.md @@ -0,0 +1,86 @@ +# Tutorial: adopt the Python QA template in a new repo + +This tutorial takes a new Python repository from an empty quality-gate setup to +the same local and CI contract used by `NWarila/python-template`. Allow about 20 +minutes. + +## 1. Copy the starter files + +From a clone of `NWarila/python-template`, copy the reference files into your +new repository: + +```bash +cp reference/pyproject.toml ../your-repo/pyproject.toml +cp reference/pre-commit-config.yaml ../your-repo/.pre-commit-config.yaml +cp reference/markdownlint-cli2.jsonc ../your-repo/.markdownlint-cli2.jsonc +cp reference/repo-ci.yml ../your-repo/.github/workflows/ci.yml +cp reference/settings.json ../your-repo/.vscode/settings.json +cp reference/extensions.json ../your-repo/.vscode/extensions.json +cp reference/tasks.json ../your-repo/.vscode/tasks.json +cp reference/gitignore ../your-repo/.gitignore +cp reference/gitattributes ../your-repo/.gitattributes +``` + +Then customize `pyproject.toml` for the new project name, description, runtime +dependencies, and optional entry points. + +## 2. Sync the managed scripts + +Run the manifest-driven sync once from the template clone: + +```bash +python scripts/sync.py . ../your-repo +``` + +This writes the managed QA scripts into `../your-repo/.github/scripts/` and +copies the managed config files listed in `sync-manifest.json`. + +## 3. Add your Python package and tests + +Create the normal Python project roots: + +```text +src/ +tests/ +``` + +Keep importable code under `src/` and tests under `tests/`. The reference +`pyproject.toml` already points Ruff, mypy, pytest, and coverage at those paths. + +## 4. Create a local environment + +From the new repository root, run the setup script that was synced into +`.github/scripts/`: + +```bash +bash .github/scripts/setup.sh +``` + +On Windows PowerShell: + +```powershell +.\.github\scripts\setup.ps1 +``` + +The setup script uses `uv` when `uv.lock` exists. Otherwise it creates `.venv` +with `pip` and installs the project with the `dev` extras from `pyproject.toml`. + +## 5. Run the local quality gate + +Activate the environment and run the orchestrator: + +```bash +. .venv/bin/activate +python .github/scripts/qa.py +``` + +On Windows PowerShell: + +```powershell +.\.venv\Scripts\Activate.ps1 +python .github/scripts/qa.py +``` + +The orchestrator runs lint, packaging, security, spelling, tests, and type +checks in sequence. When all checks pass locally, open a pull request and let +the reusable workflow run the same synced scripts in CI. diff --git a/pyproject.toml b/pyproject.toml index aca8e91..5b4bb64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,4 +51,4 @@ testpaths = ["tests"] pythonpath = ["."] [tool.codespell] -skip = ".venv,dist,.git,.mypy_cache,.ruff_cache,.pytest_cache,reference" +skip = ".venv,dist,.git,.mypy_cache,.ruff_cache,.pytest_cache,reference,.\\docs\\decision-records\\org\\*,./docs/decision-records/org/*"