diff --git a/AGENTS.md b/AGENTS.md index 05fd3aa..d7b19fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,10 +14,31 @@ tooling, not its Proxmox domain content. ## Conventions -- **dryvist-only references.** Every owner reference in this repo — provider - `owner`, `uses:`, renovate presets, remotes, links — is `dryvist`. Do not - introduce any personal-account owner references; this repo manages the - `dryvist` org and must point only at it. +- **No personal-account references.** Any owner reference that must exist — + `providers.tf` `owner`, `uses:`, renovate presets, remotes, links — points + at the org this repo manages, never at a personal account. (See also the + org-agnostic-code rule below: the org login appears in `providers.tf` and + documentation strings only, never in `.tf` resource bodies or variable + descriptions.) +- **No magic numbers in `.tf`. No specific identities in `.tf` either.** + Numeric or string values land in `variables.tf` (with type + validation + + description) or `config/.yml` (parsed via `yamldecode(file(...))` + into a local). For identifiers that GitHub already knows about — repo IDs, + the org's own login, account IDs — use a `data` source in `data.tf` and + reference the live value at apply time, never a literal default. The + canonical example is `data.github_repository.dot_github`: looks up the + org's `.github` repo by name (org is implied by the provider's `owner`), + exposes its numeric `repo_id` to org rulesets. +- **Code stays org-agnostic.** Don't bake the org's name into variable + descriptions, comments, or documentation strings. Write by role: "the + org's `.github` repo", "the org owner declared in the provider". The + `providers.tf` `owner = "dryvist"` is the single allowed mention of the + org login; everything else references roles. +- **`config/` holds non-`.tf` source data.** YAML thresholds, lists, and + any structured input the rulesets read at apply time. Canonical text the + org doesn't author (MIT LICENSE body, CODE_OF_CONDUCT, etc.) is fetched + from a trustworthy upstream via `data "http"` — never committed as a + local template. - **No local markdownlint config.** This repo *defines* the org-wide markdownlint ruleset (`github_organization_ruleset.markdown_lint`), whose single source of truth is the workflow + `.markdownlint-cli2.yaml` in @@ -38,11 +59,17 @@ Org ruleset changes require the **ORG_ADMIN** token tier (`gh-claude-org-admin`) — the provider needs `admin:org`. The default `DRYVIST` tier is read-only on org rulesets and will `403` on apply. -Roll out enforcement safely with the `evaluate` → `active` path: new org-wide -rules default to `evaluate` (dry-run, reports in Rulesets / Insights without -blocking merges). Confirm the fleet is green in Insights, then flip to `active`. +**New rulesets default to `active`.** Rules added going forward — push +protection, branch protection, commit format, etc. — set their +`_enforcement` variable's default to `"active"` and apply enabled +directly. No dry-run gate. The variable still exists so a misbehaving rule +can be disabled with `-var _enforcement=disabled` without a code +change. + +**The existing `markdown_lint_enforcement` keeps its legacy `evaluate` +default** (changing it would silently flip enforcement on the next apply for +any operator who runs `tofu apply` without overrides). Enforce explicitly: ```bash -tofu apply # evaluate (default) -tofu apply -var markdown_lint_enforcement=active # enforce +tofu apply -var markdown_lint_enforcement=active ``` diff --git a/README.md b/README.md index 30f9587..9f21d1d 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,24 @@ policy, file-size limits, and repo settings can move here next. ## Layout ```text -versions.tf # terraform + provider pins, S3 backend (partial) -providers.tf # github provider (owner = dryvist), GITHUB_TOKEN auth -variables.tf # markdown_lint_enforcement (evaluate | active | disabled) -rulesets.tf # org rulesets +versions.tf # terraform + provider pins, S3 backend (partial) +providers.tf # github provider, GITHUB_TOKEN auth +variables.tf # all input variables (no magic numbers in .tf below) +data.tf # live lookups: repo IDs, org metadata — never literals +rulesets.tf # org rulesets (markdown_lint, …) +main.tf # multi-file entrypoint stub (resources organized by topic) +outputs.tf # intentionally empty — see file header +config/ # YAML thresholds + lists consumed via yamldecode(file(...)) ``` +`config/` holds plain-data thresholds, extension lists, label sets — read +into Terraform via `yamldecode(file(...))` and exposed as locals, never +inlined as `.tf` literals. `data.tf` holds live lookups (repo IDs, org +metadata, future repo enumerations) so no specific identity values are +baked into the code. Canonical text the org doesn't author — MIT LICENSE +body, CODE_OF_CONDUCT, etc. — is fetched at apply time from a trustworthy +upstream via `data "http"`, not committed as a local template. + ## Requirements - **OpenTofu** (>= 1.6) and the `integrations/github` provider, pinned in @@ -64,11 +76,17 @@ tofu init -backend=false && tofu validate ``` **Rolling out a rule safely.** Org-wide enforcement can block merges everywhere -at once. Default the enforcement to `evaluate` (dry-run — reports in -**Rulesets / Insights** without blocking), confirm the fleet is green, then flip -to `active`: +at once. For `markdown_lint_enforcement` (legacy default `evaluate`), use the +dry-run gate before enforcing: ```bash -tofu apply # evaluate (default) +tofu apply # evaluate (legacy default) tofu apply -var markdown_lint_enforcement=active # enforce ``` + +**New rulesets default to `active`.** The `evaluate` dry-run gate above is +specific to `markdown_lint_enforcement`'s legacy default. Rulesets added going +forward — push protection, branch protection, commit format, etc. — default +their `_enforcement` variable to `"active"` and are applied enabled +directly. The variable still exists so a misbehaving rule can be disabled with +`-var _enforcement=disabled` without a code change. diff --git a/config/rulesets-defaults.yml b/config/rulesets-defaults.yml new file mode 100644 index 0000000..6ecf963 --- /dev/null +++ b/config/rulesets-defaults.yml @@ -0,0 +1,27 @@ +# Default thresholds and value sets for org-wide rulesets defined in +# rulesets.tf. Read with yamldecode(file(...)) and expose as locals. No +# literal numbers, lists, or thresholds in any .tf file — keep them here so +# they're tunable without editing Terraform code, and reviewable in a single +# place. + +# Consumed by github_organization_ruleset.org_push_protection (added in a +# follow-up commit). Native GitHub push rules — enforced at the git layer +# without any workflow involvement. +push_protection: + # Hard ceiling on individual file size. Any push containing a single file + # larger than this is rejected by GitHub before it lands. Repos with a + # legitimate need for larger files get added to the bypass actor list + # rather than raising the org-wide ceiling. + max_file_size_mb: 1 + + # Banned file extensions. These extensions usually carry credentials or + # keys; pushes containing files with these extensions are rejected at the + # git layer. Repos that legitimately need to commit one of these (e.g. a + # test fixture) put them in a bypass-allowed path or add the actor to + # bypass. + banned_file_extensions: + - env + - pem + - key + - p12 + - pfx diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..1219aba --- /dev/null +++ b/data.tf @@ -0,0 +1,7 @@ +# Resolve the numeric repository ID of the org's `.github` repo at apply +# time, so org rulesets that reference workflows living in it never carry +# a literal ID. The provider's `owner` setting determines the org; this +# data source just names the repo. +data "github_repository" "dot_github" { + name = ".github" +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..27f3f95 --- /dev/null +++ b/main.tf @@ -0,0 +1,12 @@ +# Multi-file root configuration. Resources are organized by topic in named +# .tf files rather than concentrated here: +# +# rulesets.tf — github_organization_ruleset.* +# (future) — org_settings.tf (github_actions_organization_*), +# repo_files.tf (github_repository_file.*), +# labels.tf (github_issue_labels.*) +# +# main.tf is the entrypoint required by tflint's standard-module-structure +# check. As top-level orchestration grows (e.g. a shared data lookup the +# topical files reference), it lands here. Until then, it's intentionally +# resource-free. diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..6d6da74 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,5 @@ +# No outputs exposed at the root level. The side effects of `tofu apply` are +# the org-level rulesets, repo files, and settings created on GitHub +# directly — they're not consumable as Terraform outputs by callers. The +# standard-module-structure check requires this file to exist; per the +# check's own warning text, an empty outputs.tf is valid. diff --git a/rulesets.tf b/rulesets.tf index 43266b6..4cb50f6 100644 --- a/rulesets.tf +++ b/rulesets.tf @@ -1,16 +1,9 @@ -locals { - # dryvist/.github holds the single source of truth for org CI: the reusable - # markdownlint workflow AND the canonical .markdownlint-cli2.yaml config. - # This numeric id is stable for the life of the repo (rename-safe). - dot_github_repository_id = 1220572589 -} - # Org-wide markdown linting, enforced as a Required Workflow. # -# Every repo's default-branch PRs must pass dryvist/.github's markdownlint -# workflow. The rule references ONE workflow + ONE config (both in -# dryvist/.github), so there are no per-repo markdownlint files to drift — -# this is the org-native replacement for per-repo `uses:` wiring. +# Every repo's default-branch PRs must pass the markdownlint workflow that +# lives in the org's `.github` repo (resolved at apply time via +# data.github_repository.dot_github). One workflow + one config — no +# per-repo markdownlint files to drift, no per-repo `uses:` wiring. resource "github_organization_ruleset" "markdown_lint" { name = "org-markdown-lint" target = "branch" @@ -30,7 +23,7 @@ resource "github_organization_ruleset" "markdown_lint" { rules { required_workflows { required_workflow { - repository_id = local.dot_github_repository_id + repository_id = data.github_repository.dot_github.repo_id path = ".github/workflows/markdownlint.yml" ref = "refs/heads/main" }