diff --git a/config/repos.yml b/config/repos.yml new file mode 100644 index 0000000..4783eb2 --- /dev/null +++ b/config/repos.yml @@ -0,0 +1,122 @@ +# Inventory of repos whose per-repo GitHub settings this config manages. +# +# Decoded with yamldecode(file(...)) in locals.tf and consumed by repos.tf +# via for_each. The map key is the repo name; the org owner is implied by the +# single provider's `owner` setting (see providers.tf) and is intentionally +# NOT repeated here — an account login in config/*.yml would violate the +# no-identity-in-source rule. Repo names are governance subjects (the things +# being managed), not identities, so they belong here as structured data. +# +# Schema (per entry): +# visibility: "public" | "private". Drives the secret-scanning cost gate: +# secret scanning + push protection require GitHub Advanced +# Security on PRIVATE repos (paid), but are free on public. The +# module enables them ONLY when visibility is "public", so an +# apply never silently turns on a paid feature. Look up the live +# value (`gh repo view --json visibility`) before adding +# a repo — never assume "public". +# description: GitHub repo description (1-line, shown on GitHub). +# topics: List of GitHub topic tags. +# +# Per-repo `all`/`main` rulesets are OUT OF SCOPE: the org-level rulesets in +# rulesets.tf already enforce signed commits, linear history, and Conventional +# Commits on every repo. This config manages only repository *settings* +# (merge methods, auto-merge, branch deletion, wiki, web-commit signoff, +# Dependabot, and the public-only secret-scanning block). +# +# This is the first increment — the nix-* family ported from the retired +# `.github-tofu` scaffold. Expanding to every non-archived org repo is a +# follow-up (see the tracking issue). + +repos: + nix-ai: + visibility: public + description: "Your AI coding toolkit, declared in Nix — Claude, Gemini, Copilot, 15+ MCP servers, one flake" + topics: + - ai + - ai-tools + - claude + - claude-code + - copilot + - gemini + - home-manager + - mcp-server + - nix + - nix-flakes + - ollama + + nix-darwin: + visibility: public + description: "Flakes-based nix-darwin config for macOS — system packages, networking, security, and home-manager orchestration via Nix" + topics: + - darwin + - declarative + - home-manager + - infrastructure-as-code + - macos + - macos-configuration + - nix + - nix-darwin + - nix-flakes + - reproducible + + nix-home: + visibility: public + description: "Cross-platform dev environment in Nix — git, zsh, VS Code, tmux, declared once, reproduced everywhere" + topics: + - cross-platform + - developer-tools + - dotfiles + - git-config + - home-manager + - nix + - nix-flakes + - tmux + - vscode + - zsh + + nix-devenv: + visibility: public + description: "Reusable dev shells in Nix — Terraform, Ansible, Kubernetes, AI/ML, and more, one nix develop away" + topics: + - declarative + - developer-tools + - development-environment + - devenv + - direnv + - flake-templates + - nix + - nix-develop + - nix-flakes + - reproducible + + nix-claude-code: + visibility: public + description: "Declarative Claude Code in Nix — plugins, marketplaces, skills, hooks, MCP, and permissions as composable home-manager modules. Reproducible on macOS and Linux." + topics: + - agents + - ai + - ai-cli + - anthropic + - claude-code + - declarative + - flake-parts + - home-manager + - home-manager-module + - mcp + - nix + - nix-darwin + - nix-flake + - nix-flakes + - reproducible + - skills + + nix-pxe-bootstrap: + visibility: public + description: "NixOS-on-Pi netboot.xyz + Proxmox auto-installer for unattended bare-metal install" + topics: [] + + nix-ai-server: + visibility: public + description: "NixOS bare-metal config for the dryvist AI host (server A, standalone)" + topics: [] diff --git a/modules/repo-settings/main.tf b/modules/repo-settings/main.tf new file mode 100644 index 0000000..2297876 --- /dev/null +++ b/modules/repo-settings/main.tf @@ -0,0 +1,86 @@ +# Per-repo settings — the repository-settings half of the retired +# `.github-tofu` nix-repo module. Baseline established on the nix-* family: +# squash + rebase merges only (no merge commit), auto-merge on, branch deleted +# on merge, web commit signoff required, wiki off. +# +# Per-repo rulesets are intentionally NOT ported: the org-level rulesets in +# ../../rulesets.tf already enforce signed commits, linear history, and +# Conventional Commits on every repo. Porting the source module's per-repo +# `all` ruleset would duplicate that org-level coverage. +# +# The owner is supplied by the calling module's provider (a single +# org-scoped provider at the root); this module never names it. + +resource "github_repository" "this" { + # checkov:skip=CKV_GIT_1: These repos are intentionally public. The org cost + # policy (see AGENTS.md) depends on public visibility to keep secret scanning + # free; forcing private would invert the design and incur GHAS charges. The + # visibility variable is validated and per-repo in config/repos.yml. + # checkov:skip=CKV2_GIT_1: Branch protection is associated at the ORG level via + # the github_organization_ruleset resources in ../../rulesets.tf (signed + # commits, linear history, Conventional Commits on every repo's default + # branch). Checkov only detects per-repo branch_protection resources, not the + # org rulesets that cover these repos — so this is a false negative for it. + name = var.name + description = var.description + topics = var.topics + + visibility = var.visibility + + has_issues = true + has_wiki = false + has_projects = true + has_discussions = false + + allow_squash_merge = true + allow_merge_commit = false + allow_rebase_merge = true + allow_auto_merge = true + delete_branch_on_merge = true + web_commit_signoff_required = true + + # Secret scanning + push protection are free on public repos but require + # paid GitHub Advanced Security (Secret Protection) on private repos. Emit + # the security_and_analysis block ONLY for public repos so an apply can never + # silently enable a paid feature on a private repo. The source module + # hardcoded visibility = "public" and enabled these unconditionally — that + # would charge GHAS the moment a private repo entered the inventory. + dynamic "security_and_analysis" { + for_each = var.visibility == "public" ? [1] : [] + + content { + secret_scanning { + status = "enabled" + } + secret_scanning_push_protection { + status = "enabled" + } + } + } + + # Prevent TF from recreating or renaming existing repos on first apply. + lifecycle { + prevent_destroy = true + ignore_changes = [ + # Auto-init only matters at creation; ignore so imports don't churn. + auto_init, + gitignore_template, + license_template, + # Homepage URL is per-repo and may be set manually; don't fight it. + homepage_url, + ] + } +} + +# Dependabot alerts — notifications when CVEs are detected in dependencies. +# Free on public and private repos (dependency graph + Dependabot is not GHAS). +resource "github_repository_vulnerability_alerts" "this" { + repository = github_repository.this.name +} + +# Dependabot automatic security update PRs. Also free on public and private. +resource "github_repository_dependabot_security_updates" "this" { + repository = github_repository.this.name + enabled = true + depends_on = [github_repository_vulnerability_alerts.this] +} diff --git a/modules/repo-settings/outputs.tf b/modules/repo-settings/outputs.tf new file mode 100644 index 0000000..5d34ae3 --- /dev/null +++ b/modules/repo-settings/outputs.tf @@ -0,0 +1,9 @@ +output "full_name" { + description = "owner/name of the managed repo, from the provider-resolved live value." + value = github_repository.this.full_name +} + +output "node_id" { + description = "Provider-assigned GraphQL node id of the managed repo." + value = github_repository.this.node_id +} diff --git a/modules/repo-settings/variables.tf b/modules/repo-settings/variables.tf new file mode 100644 index 0000000..99659a2 --- /dev/null +++ b/modules/repo-settings/variables.tf @@ -0,0 +1,31 @@ +variable "name" { + description = "Repo name without owner. The owner is supplied by the calling module's GitHub provider, so it never appears here." + type = string +} + +variable "description" { + description = "Repo description (1-line, shown on GitHub)." + type = string +} + +variable "topics" { + description = "GitHub topic tags." + type = list(string) + default = [] +} + +variable "visibility" { + description = <<-EOT + Repo visibility: "public" or "private". Drives the secret-scanning cost + gate. Secret scanning and push protection require GitHub Advanced Security + on private repos (paid: Secret Protection) but are free on public repos, so + the module only sets the security_and_analysis block when this is "public". + A private repo therefore never has a paid feature enabled by an apply. + EOT + type = string + + validation { + condition = contains(["public", "private"], var.visibility) + error_message = "visibility must be one of: public, private." + } +} diff --git a/modules/repo-settings/versions.tf b/modules/repo-settings/versions.tf new file mode 100644 index 0000000..11aba7f --- /dev/null +++ b/modules/repo-settings/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + github = { + source = "integrations/github" + version = "~> 6.0" + } + } +} diff --git a/repos.tf b/repos.tf new file mode 100644 index 0000000..fff8c6f --- /dev/null +++ b/repos.tf @@ -0,0 +1,52 @@ +# Per-repo settings for the repos this config governs. The repository-settings +# half ported from the retired `.github-tofu` scaffold (its per-repo rulesets +# are dropped — the org rulesets in rulesets.tf already cover signed commits, +# linear history, and Conventional Commits on every repo). +# +# config/repos.yml is the single source of truth for which repos are managed +# and their per-repo metadata (visibility, description, topics). The owner is +# supplied by the single provider in providers.tf, never repeated per repo. + +locals { + # Inventory of managed repos, keyed by repo name. + repos = yamldecode(file("${path.module}/config/repos.yml")).repos +} + +module "repo_settings" { + source = "./modules/repo-settings" + for_each = local.repos + + name = each.key + description = each.value.description + topics = each.value.topics + visibility = each.value.visibility +} + +# Import-on-first-apply: adopt every managed repo (and its two Dependabot +# sub-resources) into Terraform state so the first apply RECONCILES the +# existing repos' settings instead of trying to create them — which +# prevent_destroy would block and a name collision would fail anyway. Mirrors +# what `.github-tofu/scripts/import.sh` imported, but as native Terraform 1.5+ +# import blocks rather than a shell script. The import id for a +# github_repository is the bare repo name (owner comes from the provider); for +# the Dependabot sub-resources it is likewise the repo name. +# +# These blocks are idempotent and only useful once. After a successful apply +# they can be removed in a follow-up PR. +import { + for_each = local.repos + to = module.repo_settings[each.key].github_repository.this + id = each.key +} + +import { + for_each = local.repos + to = module.repo_settings[each.key].github_repository_vulnerability_alerts.this + id = each.key +} + +import { + for_each = local.repos + to = module.repo_settings[each.key].github_repository_dependabot_security_updates.this + id = each.key +}