Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions config/repos.yml
Original file line number Diff line number Diff line change
@@ -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 <repo> --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: []
86 changes: 86 additions & 0 deletions modules/repo-settings/main.tf
Original file line number Diff line number Diff line change
@@ -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]
}
9 changes: 9 additions & 0 deletions modules/repo-settings/outputs.tf
Original file line number Diff line number Diff line change
@@ -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
}
31 changes: 31 additions & 0 deletions modules/repo-settings/variables.tf
Original file line number Diff line number Diff line change
@@ -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."
}
}
10 changes: 10 additions & 0 deletions modules/repo-settings/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.6.0"

required_providers {
github = {
source = "integrations/github"
version = "~> 6.0"
}
}
}
52 changes: 52 additions & 0 deletions repos.tf
Original file line number Diff line number Diff line change
@@ -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
}
Loading