From ba71a8b9f2c7687c38d44828caf4ac2b237d614e Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans-personal@users.noreply.github.com> Date: Sat, 30 May 2026 18:43:42 -0400 Subject: [PATCH 1/2] feat: foundation scaffolding for org-wide config (Step 0 of 9) Sets up the no-magic-numbers convention and the directory layout for the planned expansion of terraform-github into the single source of truth for all dryvist org-level configuration (rulesets, actions settings, per-repo file content, labels). No new resources; no apply required. Changes ------- - variables.tf: add `dot_github_repository_id` (number, default 1220572589, validated positive). Canonical example of the no-magic-numbers rule. - rulesets.tf: drop the `locals { }` block; reference `var.dot_github_repository_id` instead of `local.dot_github_repository_id`. - main.tf, outputs.tf: new. Resolves the two pre-existing tflint terraform_standard_module_structure warnings. main.tf documents the file-split convention (resources organized by topic in named files); outputs.tf is intentionally empty per the check's own warning text. - config/rulesets-defaults.yml: new. Holds default thresholds (max file size, banned extensions) the upcoming `org_push_protection` ruleset will consume via yamldecode(file(...)). - templates/LICENSE.mit.tmpl: new. MIT template for Step 7's per-repo `github_repository_file.license` for_each. - AGENTS.md, README.md: document the no-magic-numbers convention, the config/ and templates/ directories, and the rule that new rulesets default to `active` enforcement. Existing `markdown_lint_enforcement` keeps its legacy `evaluate` default - changing it would silently flip enforcement on the next apply for any operator running `tofu apply` without overrides. What this does NOT do --------------------- - No new rulesets - those land in Step 1 (org_push_protection: native max_file_size + file_extension_restriction push rules) and beyond. - No apply - only `tofu validate` was run. Apply requires ORG_ADMIN. - terraform-github's own LICENSE is still Apache 2.0; Step 7 will reconcile it to MIT along with every other dryvist repo via github_repository_file. Verification ------------ - tofu init -backend=false and tofu validate -> green - pre-commit run --all-files -> all hooks pass Assisted-by: Claude --- AGENTS.md | 27 ++++++++++++++++++++++----- README.md | 29 +++++++++++++++++++++-------- config/rulesets-defaults.yml | 27 +++++++++++++++++++++++++++ main.tf | 12 ++++++++++++ outputs.tf | 5 +++++ rulesets.tf | 9 +-------- templates/LICENSE.mit.tmpl | 21 +++++++++++++++++++++ variables.tf | 19 +++++++++++++++++++ 8 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 config/rulesets-defaults.yml create mode 100644 main.tf create mode 100644 outputs.tf create mode 100644 templates/LICENSE.mit.tmpl diff --git a/AGENTS.md b/AGENTS.md index 05fd3aa..9774760 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,17 @@ tooling, not its Proxmox domain content. `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 magic numbers in `.tf`.** Every numeric or string value goes in + `variables.tf` (with type + validation + description) or in + `config/.yml` (parsed via `yamldecode(file(...))` into a local). The + canonical example is `var.dot_github_repository_id`: a GitHub repo ID with a + default and validation, referenced from `rulesets.tf` as + `var.dot_github_repository_id`. Never inline a repo ID, threshold, port, + extension list, or branch name. +- **`config/` and `templates/` hold non-`.tf` source data.** `config/*.yml` + carries ruleset defaults (`yamldecode`); `templates/*.tmpl` carries per-repo + file bodies (`templatefile()`) for resources like + `github_repository_file.license` that materialize files into every repo. - **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 +49,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..9a98b01 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,19 @@ 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 (owner = dryvist), GITHUB_TOKEN auth +variables.tf # all input variables (no magic numbers in .tf below) +rulesets.tf # org rulesets (markdown_lint, …) +config/ # YAML defaults consumed via yamldecode(file(...)) +templates/ # per-repo file templates rendered via templatefile() ``` +`config/` and `templates/` are the no-magic-numbers staging area: thresholds, +extension lists, and per-repo file bodies live here as plain data, not inlined +in `.tf`. `rulesets.tf` reads them via `yamldecode(file(...))` and +`templatefile()`. + ## Requirements - **OpenTofu** (>= 1.6) and the `integrations/github` provider, pinned in @@ -64,11 +71,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/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..efb22aa 100644 --- a/rulesets.tf +++ b/rulesets.tf @@ -1,10 +1,3 @@ -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 @@ -30,7 +23,7 @@ resource "github_organization_ruleset" "markdown_lint" { rules { required_workflows { required_workflow { - repository_id = local.dot_github_repository_id + repository_id = var.dot_github_repository_id path = ".github/workflows/markdownlint.yml" ref = "refs/heads/main" } diff --git a/templates/LICENSE.mit.tmpl b/templates/LICENSE.mit.tmpl new file mode 100644 index 0000000..22b549c --- /dev/null +++ b/templates/LICENSE.mit.tmpl @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) ${year} ${holder} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/variables.tf b/variables.tf index 48638a6..af9f9ca 100644 --- a/variables.tf +++ b/variables.tf @@ -17,3 +17,22 @@ variable "markdown_lint_enforcement" { error_message = "markdown_lint_enforcement must be one of: disabled, evaluate, active." } } + +variable "dot_github_repository_id" { + description = <<-EOT + Numeric GitHub repository ID of dryvist/.github. Stable across renames; + referenced by every org ruleset that targets a workflow living in that + repo. Look it up with: + + gh api repos/dryvist/.github --jq .id + + Override only if the .github source-of-truth repo changes. + EOT + type = number + default = 1220572589 + + validation { + condition = var.dot_github_repository_id > 0 + error_message = "dot_github_repository_id must be a positive GitHub repository ID." + } +} From 666f6257f671281de57e35cc271ad2e06f6916ef Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans-personal@users.noreply.github.com> Date: Sat, 30 May 2026 19:26:40 -0400 Subject: [PATCH 2/2] fix(scaffolding): use data source for .github repo ID; drop LICENSE template; org-agnostic prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR feedback corrections: - variables.tf: drop `dot_github_repository_id`. The literal default (1220572589) was a magic number; relocating it from a local to a variable did not solve that. - data.tf: new. Adds `data "github_repository" "dot_github"` lookup that resolves the .github repo's numeric ID at apply time via the provider's already-configured `owner`. Reference as `data.github_repository.dot_github.repo_id`. - rulesets.tf: switch `repository_id` from `var.dot_github_repository_id` to `data.github_repository.dot_github.repo_id`. Resource comment rewritten to reference the repo by role ("the org's `.github` repo") instead of by org-prefixed path. - templates/LICENSE.mit.tmpl: removed. Canonical license text belongs to its upstream; Step 7's LICENSE enforcement fetches the MIT body via `data "http"` against https://api.github.com/licenses/mit at apply time, with `${year}` / `${holder}` substituted via `replace()`. No local copy to drift from the canonical source. - AGENTS.md, README.md: prose now org-agnostic — references the repo by role ("the org's `.github` repo", "the org owner declared in the provider"). The `dryvist-only references` convention bullet is reworded to "no personal-account references" with explicit note that `providers.tf` `owner` is the single allowed mention of the org login. templates/ removed from the layout block. Mentions of `templatefile()` for bundled templates replaced by the data-http fetch pattern. Verification ------------ - tofu init -backend=false and tofu validate -> green - pre-commit run --all-files -> all hooks pass Assisted-by: Claude --- AGENTS.md | 40 ++++++++++++++++++++++++-------------- README.md | 19 +++++++++++------- data.tf | 7 +++++++ rulesets.tf | 10 +++++----- templates/LICENSE.mit.tmpl | 21 -------------------- variables.tf | 19 ------------------ 6 files changed, 49 insertions(+), 67 deletions(-) create mode 100644 data.tf delete mode 100644 templates/LICENSE.mit.tmpl diff --git a/AGENTS.md b/AGENTS.md index 9774760..d7b19fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,21 +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 magic numbers in `.tf`.** Every numeric or string value goes in - `variables.tf` (with type + validation + description) or in - `config/.yml` (parsed via `yamldecode(file(...))` into a local). The - canonical example is `var.dot_github_repository_id`: a GitHub repo ID with a - default and validation, referenced from `rulesets.tf` as - `var.dot_github_repository_id`. Never inline a repo ID, threshold, port, - extension list, or branch name. -- **`config/` and `templates/` hold non-`.tf` source data.** `config/*.yml` - carries ruleset defaults (`yamldecode`); `templates/*.tmpl` carries per-repo - file bodies (`templatefile()`) for resources like - `github_repository_file.license` that materialize files into every repo. +- **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 diff --git a/README.md b/README.md index 9a98b01..9f21d1d 100644 --- a/README.md +++ b/README.md @@ -25,17 +25,22 @@ policy, file-size limits, and repo settings can move here next. ```text versions.tf # terraform + provider pins, S3 backend (partial) -providers.tf # github provider (owner = dryvist), GITHUB_TOKEN auth +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, …) -config/ # YAML defaults consumed via yamldecode(file(...)) -templates/ # per-repo file templates rendered via templatefile() +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/` and `templates/` are the no-magic-numbers staging area: thresholds, -extension lists, and per-repo file bodies live here as plain data, not inlined -in `.tf`. `rulesets.tf` reads them via `yamldecode(file(...))` and -`templatefile()`. +`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 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/rulesets.tf b/rulesets.tf index efb22aa..4cb50f6 100644 --- a/rulesets.tf +++ b/rulesets.tf @@ -1,9 +1,9 @@ # 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" @@ -23,7 +23,7 @@ resource "github_organization_ruleset" "markdown_lint" { rules { required_workflows { required_workflow { - repository_id = var.dot_github_repository_id + repository_id = data.github_repository.dot_github.repo_id path = ".github/workflows/markdownlint.yml" ref = "refs/heads/main" } diff --git a/templates/LICENSE.mit.tmpl b/templates/LICENSE.mit.tmpl deleted file mode 100644 index 22b549c..0000000 --- a/templates/LICENSE.mit.tmpl +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) ${year} ${holder} - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/variables.tf b/variables.tf index af9f9ca..48638a6 100644 --- a/variables.tf +++ b/variables.tf @@ -17,22 +17,3 @@ variable "markdown_lint_enforcement" { error_message = "markdown_lint_enforcement must be one of: disabled, evaluate, active." } } - -variable "dot_github_repository_id" { - description = <<-EOT - Numeric GitHub repository ID of dryvist/.github. Stable across renames; - referenced by every org ruleset that targets a workflow living in that - repo. Look it up with: - - gh api repos/dryvist/.github --jq .id - - Override only if the .github source-of-truth repo changes. - EOT - type = number - default = 1220572589 - - validation { - condition = var.dot_github_repository_id > 0 - error_message = "dot_github_repository_id must be a positive GitHub repository ID." - } -}