1 Commits

Author SHA1 Message Date
unkinben 6bb636846f branch_protection: stop whitelist representation churn planning every run
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Why:
- The go-gitea/gitea provider stores branch-protection whitelist users/teams as
  plain names, but Gitea resolves them to IDs and, on read, returns them via
  getWhitelistEntities over the repo's readers: users ordered by user ID, teams
  ordered by name, and any entity without read access to the repo silently
  dropped.
- So the read-back representation of push/merge/approval whitelists rarely
  matches the config list (order differs, or a whitelisted user/team that lacks
  repo access disappears), and tofu plans an in-place update for those branch
  protections on every run even with no config change.
- This is the same class of provider representation churn already handled for
  teams in #62 (units/repositories), and it blocks a clean plan on terraform-git.

Change:
- Add a targeted ignore_changes on the six whitelist list attributes
  (push/merge/approval, users and teams) at the branch_protection module so the
  churn no longer triggers a spurious update, keeping every protected repo
  idempotent.
- Leave enable_push, required_approvals, status_check_patterns and the
  block_merge_on_* flags managed; those round-trip cleanly and stay drift-checked.
2026-08-11 21:01:10 +10:00
43 changed files with 50 additions and 647 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ init:
plan: init plan: init
@$(call vault_env) && \ @$(call vault_env) && \
terragrunt run --all --parallelism 4 --non-interactive plan -- -lock=false terragrunt run --all --parallelism 4 --non-interactive plan
apply-if-changes: init apply-if-changes: init
@$(call vault_env) && \ @$(call vault_env) && \
-3
View File
@@ -1,3 +0,0 @@
description: "Pull mirrors of upstream repositories"
visibility: public
repo_admin_change_team_access: true
@@ -1,11 +0,0 @@
description: "Pull mirror of github.com/9p4/jellyfin-plugin-sso, the Jellyfin SSO/OIDC authentication plugin"
private: false
has_issues: false
has_pull_requests: false
mirror: true
migration_clone_address: "https://github.com/9p4/jellyfin-plugin-sso"
migration_service: "git"
migration_mirror_interval: "8h0m0s"
migration_issue_labels: false
migration_milestones: false
migration_releases: false
@@ -1,16 +0,0 @@
description: "CLI tools for orchestrator PR automation as unkin-agent"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
approval_whitelist_users:
- "benvin"
@@ -10,5 +10,6 @@ branch_protection:
status_check_contexts: status_check_contexts:
- "ci/woodpecker/pr/pre-commit" - "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/kubeconform" - "ci/woodpecker/pr/kubeconform"
- "ci/woodpecker/pr/vector-test"
approval_whitelist_users: approval_whitelist_users:
- "benvin" - "benvin"
@@ -1,16 +0,0 @@
description: "arrstack reverse proxy: oauth2-gated UI + per-user API-key broker"
private: true
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/build"
approval_whitelist_users:
- "benvin"
@@ -1,14 +0,0 @@
description: "Neovim plugin manager that installs plugins from tagged archives in an HTTP artifact repository into nvim's native package path"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/lint"
- "ci/woodpecker/pr/pre-commit"
@@ -1,16 +0,0 @@
description: "Kubernetes operator that provisions S3 buckets and backup schedules from backups.unkin.net/* annotations on PVCs and CNPG clusters"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/build"
approval_whitelist_users:
- "benvin"
@@ -1,4 +1,4 @@
description: "Vault-backed X.509 certificate signing helper for Puppet" description: "Vault PKI certificate issuance and SSH host key signing tool for Puppet-managed infrastructure"
private: false private: false
default_branch: "master" default_branch: "master"
default_delete_branch_after_merge: true default_delete_branch_after_merge: true
@@ -1,14 +0,0 @@
description: "CLI tools (chcat, chtail, chgrep) for searching, filtering and tailing logs in the ClickHouse log store"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
@@ -1,15 +0,0 @@
description: "Base container images for the estate, carrying internal CA trust and dnf/repo configuration, built as a matrix across the supported distros."
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/pre-commit"
approval_whitelist_users:
- "benvin"
@@ -1,15 +0,0 @@
description: "Go toolchain builder container image, built on each supported container-base distro image, used by Woodpecker pipelines to compile Go projects."
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/pre-commit"
approval_whitelist_users:
- "benvin"
@@ -1,17 +0,0 @@
description: "Fork of goodtune/ghp (GitHub proxy) with unkin patches"
private: true
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
# Whitelist-push: the owner can push main directly (needed for the initial
# push and the fork's maintenance workflow); everyone else must open a PR.
enable_push: true
push_whitelist_users:
- "unkinben"
# PRs to main must pass the fork build/vet pipeline before merge.
status_check_contexts:
- "ci/woodpecker/pr/build"
@@ -1,16 +0,0 @@
description: "Build repo that compiles the tailscale/go-cache-plugin GOCACHEPROG S3 Go build cache from a pinned upstream module version and publishes static binaries as Gitea releases."
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
approval_whitelist_users:
- "benvin"
@@ -1,14 +0,0 @@
description: "Shared Go library for estate services: postgres, http service kit, vault and gitea clients"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
@@ -5,7 +5,5 @@ default_delete_branch_after_merge: true
branch_protection: branch_protection:
- rule_name: "main" - rule_name: "main"
enable_push: false enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/ci"
approval_whitelist_teams: approval_whitelist_teams:
- "Owners" - "Owners"
@@ -1,15 +0,0 @@
fork_from: "mirrors/jellyfin-plugin-sso"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
# Whitelist-push: the owner pushes main directly when syncing upstream;
# everyone else must open a PR.
enable_push: true
push_whitelist_users:
- "unkinben"
# PRs to main must pass the plugin build pipeline before merge.
status_check_contexts:
- "ci/woodpecker/pr/ci"
approval_whitelist_teams:
- "Owners"
@@ -1,14 +0,0 @@
description: "Web UI for the ClickHouse log store: fuzzy find, tail and SQL-filter logs (logviewer.unkin.net)"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
@@ -1,14 +0,0 @@
description: "Keyboard-centric web UI to mark media as cheeztv (kids) content via hardlinks; oauth2-proxy fronted, talks to sonarr/radarr APIs"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
@@ -1,10 +0,0 @@
description: "Media copy/move tool: Go API + UI spawning per-file k8s Jobs from mediafs CephFS to media PVCs; doubles as a Ceph aggregate-bandwidth test"
private: true
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
@@ -1,14 +0,0 @@
description: "Woodpecker plugin-docker-buildx image with the internal Vault-PKI CA baked in (trusts artifactapi)"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
approval_whitelist_teams:
- "docker"
@@ -1,16 +0,0 @@
description: "Private fork of Prowlarr: Postgres backend + stateless multi-replica for the arrstack"
private: true
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
# Owner can push main directly for fork maintenance (upstream-fix merges);
# everyone else opens a PR that must pass the Woodpecker image build.
enable_push: true
push_whitelist_users:
- "unkinben"
status_check_contexts:
- "ci/woodpecker/pr/docker"
@@ -1,14 +0,0 @@
description: "Go CLIs to diff Puppet catalogs between branches and map puppet-prod changes to affected hosts"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
@@ -1,16 +0,0 @@
description: "Private fork of Radarr: Postgres backend + stateless multi-replica for the arrstack"
private: true
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
# Owner can push main directly for fork maintenance (upstream-fix merges);
# everyone else opens a PR that must pass the Woodpecker image build.
enable_push: true
push_whitelist_users:
- "unkinben"
status_check_contexts:
- "ci/woodpecker/pr/docker"
@@ -1,14 +0,0 @@
description: "API service that opens terraform-git PRs for new repo requests via kube Jobs; oauth2-proxy'd status UI"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
@@ -1,16 +0,0 @@
description: "Private fork of Sonarr: Postgres backend + stateless multi-replica for the arrstack"
private: true
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
# Owner can push main directly for fork maintenance (upstream-fix merges);
# everyone else opens a PR that must pass the Woodpecker image build.
enable_push: true
push_whitelist_users:
- "unkinben"
status_check_contexts:
- "ci/woodpecker/pr/docker"
@@ -1,14 +0,0 @@
description: "Vault-backed SSH host certificate signing helper for Puppet"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
@@ -2,7 +2,6 @@ description: "Manage nomad with Terraform"
private: false private: false
default_branch: "master" default_branch: "master"
default_delete_branch_after_merge: true default_delete_branch_after_merge: true
archived: true
branch_protection: branch_protection:
- rule_name: "master" - rule_name: "master"
enable_push: false enable_push: false
@@ -1,21 +0,0 @@
description: "Terraform provider to manage the arrstack Vault secrets engine (config + roles)"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
# Squash-only: the gitea provider has no "default merge style", so we restrict
# the allowed styles to squash to force it.
allow_merge_commits: false
allow_rebase: false
allow_rebase_explicit: false
allow_squash_merge: true
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
approval_whitelist_teams:
- "Owners"
@@ -1,21 +0,0 @@
description: "Terraform provider for the Vault/OpenBao ghp token secrets engine (vault-secrets-ghp)"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
# Squash-only: the gitea provider has no "default merge style", so we restrict
# the allowed styles to squash to force it.
allow_merge_commits: false
allow_rebase: false
allow_rebase_explicit: false
allow_squash_merge: true
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
approval_whitelist_teams:
- "Owners"
@@ -1,21 +0,0 @@
description: "Vault/OpenBao secrets engine minting dynamic arrproxy per-user API tokens"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
# Squash-only: the gitea provider has no "default merge style", so we restrict
# the allowed styles to squash to force it.
allow_merge_commits: false
allow_rebase: false
allow_rebase_explicit: false
allow_squash_merge: true
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
approval_whitelist_teams:
- "Owners"
@@ -1,21 +0,0 @@
description: "HashiCorp Vault / OpenBao secrets engine for ghp: mints ephemeral, scoped access tokens via the ghp admin API, authenticating as a static admin service token"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
# Squash-only: the gitea provider has no "default merge style", so we restrict
# the allowed styles to squash to force it.
allow_merge_commits: false
allow_rebase: false
allow_rebase_explicit: false
allow_squash_merge: true
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/pre-commit"
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
approval_whitelist_teams:
- "Owners"
@@ -1,14 +0,0 @@
description: "Neovim plugin manager installing plugins from tagged archives on artifactapi/Artifactory instead of git clones"
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/lint"
- "ci/woodpecker/pr/pre-commit"
@@ -1,16 +0,0 @@
description: "A small Go tool, shipped as a container image, used as a Kubernetes initContainer to block an app from starting until its database is ready."
private: false
default_branch: "main"
default_delete_branch_after_merge: true
default_merge_style: "squash"
branch_protection:
- rule_name: "main"
merge_whitelist_teams:
- "Owners"
enable_push: false
status_check_contexts:
- "ci/woodpecker/pr/build"
- "ci/woodpecker/pr/test"
- "ci/woodpecker/pr/pre-commit"
approval_whitelist_users:
- "benvin"
@@ -1,16 +0,0 @@
# Least-privilege access for the repospawner bot: Write on terraform-git only,
# which is the minimum Gitea permission that allows pushing a branch and opening
# a pull request. repospawner does its whole job here (repo-creation PRs), so it
# is kept out of the org-wide "agents" team (include_all_repositories: true).
#
# Merge stays reserved for Ben: branch protection on terraform-git's main sets a
# merge whitelist of the Owners team only. Keep this team out of every merge and
# approval whitelist.
description: "repospawner bot -- push branches + open PRs on terraform-git only"
permission: write
include_all_repositories: false
can_create_repos: false
repositories:
- terraform-git
members:
- repospawner
@@ -1,22 +0,0 @@
# Service identity for repospawner, which opens pull requests against this
# repository (terraform-git) to add new Gitea repositories on request. It is
# deliberately separate from unkin-agent so its access can stay scoped to the
# single repository it needs: the "repospawner" team (unkin/team/repospawner.yaml)
# grants Write on terraform-git and nothing else.
#
# Auth is via ephemeral API tokens minted by the vault-plugin-secrets-gitea
# engine -- no usable credential lives in this repo's state beyond the generated
# placeholder. Merge stays blocked by branch protection (merge whitelist =
# Owners only), so it can push branches and open PRs but never merge.
email: repospawner@unkin.net
full_name: "Repo Spawner"
description: "repospawner service identity -- opens repo-creation PRs against terraform-git"
# Conservative bot posture: not a site admin, cannot create orgs or repos,
# profile visible only to signed-in users.
visibility: limited
admin: false
# Restricted: account can only see repos/orgs it is explicitly added to -- the
# repospawner team grant on terraform-git is all it needs.
restricted: true
allow_create_organization: false
max_repo_creation: 0
+34 -34
View File
@@ -4,167 +4,167 @@ import {
} }
import { import {
to = module.repository["git.unkin.net/unkin/puppet-prod"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/puppet-prod"].gitea_repository.this
id = "2" id = "2"
} }
import { import {
to = module.repository["git.unkin.net/unkin/puppet-r10k"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/puppet-r10k"].gitea_repository.this
id = "3" id = "3"
} }
import { import {
to = module.repository["git.unkin.net/unkin/rpmbuild-gonic"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/rpmbuild-gonic"].gitea_repository.this
id = "23" id = "23"
} }
import { import {
to = module.repository["git.unkin.net/unkin/docker-almalinux-base"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/docker-almalinux-base"].gitea_repository.this
id = "24" id = "24"
} }
import { import {
to = module.repository["git.unkin.net/unkin/rpmbuild-internal-ca-certificates"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/rpmbuild-internal-ca-certificates"].gitea_repository.this
id = "27" id = "27"
} }
import { import {
to = module.repository["git.unkin.net/unkin/rpmbuild-template"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/rpmbuild-template"].gitea_repository.this
id = "29" id = "29"
} }
import { import {
to = module.repository["git.unkin.net/unkin/rpmbuild-jellyfin-web"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/rpmbuild-jellyfin-web"].gitea_repository.this
id = "31" id = "31"
} }
import { import {
to = module.repository["git.unkin.net/unkin/rpmbuild-proxlb"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/rpmbuild-proxlb"].gitea_repository.this
id = "33" id = "33"
} }
import { import {
to = module.repository["git.unkin.net/unkin/docker-almalinux-buildrunner"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/docker-almalinux-buildrunner"].gitea_repository.this
id = "36" id = "36"
} }
import { import {
to = module.repository["git.unkin.net/unkin/docker-template"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/docker-template"].gitea_repository.this
id = "38" id = "38"
} }
import { import {
to = module.repository["git.unkin.net/unkin/terraform-vault"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/terraform-vault"].gitea_repository.this
id = "39" id = "39"
} }
import { import {
to = module.repository["git.unkin.net/unkin/docker-almalinux-jupyterinstance"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/docker-almalinux-jupyterinstance"].gitea_repository.this
id = "40" id = "40"
} }
import { import {
to = module.repository["git.unkin.net/unkin/rpmbuilder"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/rpmbuilder"].gitea_repository.this
id = "41" id = "41"
} }
import { import {
to = module.repository["git.unkin.net/unkin/docker-almalinux-runnerdnd"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/docker-almalinux-runnerdnd"].gitea_repository.this
id = "43" id = "43"
} }
import { import {
to = module.repository["git.unkin.net/unkin/initbuilder"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/initbuilder"].gitea_repository.this
id = "47" id = "47"
} }
import { import {
to = module.repository["git.unkin.net/unkin/puppetapi"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/puppetapi"].gitea_repository.this
id = "50" id = "50"
} }
import { import {
to = module.repository["git.unkin.net/unkin/terraform-nomad"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/terraform-nomad"].gitea_repository.this
id = "53" id = "53"
} }
import { import {
to = module.repository["git.unkin.net/unkin/packer-images"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/packer-images"].gitea_repository.this
id = "59" id = "59"
} }
import { import {
to = module.repository["git.unkin.net/unkin/app-sudaporn-research-normalised"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/app-sudaporn-research-normalised"].gitea_repository.this
id = "60" id = "60"
} }
import { import {
to = module.repository["git.unkin.net/unkin/app-sudaporn-research-individual"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/app-sudaporn-research-individual"].gitea_repository.this
id = "63" id = "63"
} }
import { import {
to = module.repository["git.unkin.net/unkin/terraform-incus"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/terraform-incus"].gitea_repository.this
id = "66" id = "66"
} }
import { import {
to = module.repository["git.unkin.net/unkin/artifactapi"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/artifactapi"].gitea_repository.this
id = "67" id = "67"
} }
import { import {
to = module.repository["git.unkin.net/unkin/argocd-apps"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/argocd-apps"].gitea_repository.this
id = "100" id = "100"
} }
import { import {
to = module.repository["git.unkin.net/unkin/certmanager"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/certmanager"].gitea_repository.this
id = "101" id = "101"
} }
import { import {
to = module.repository["git.unkin.net/unkin/node-lookup"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/node-lookup"].gitea_repository.this
id = "102" id = "102"
} }
import { import {
to = module.repository["git.unkin.net/unkin/container-devcompute"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/container-devcompute"].gitea_repository.this
id = "135" id = "135"
} }
import { import {
to = module.repository["git.unkin.net/unkin/streamstack"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/streamstack"].gitea_repository.this
id = "136" id = "136"
} }
import { import {
to = module.repository["git.unkin.net/unkin/terraform-provider-artifactapi"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/terraform-provider-artifactapi"].gitea_repository.this
id = "137" id = "137"
} }
import { import {
to = module.repository["git.unkin.net/unkin/forgebot"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/forgebot"].gitea_repository.this
id = "139" id = "139"
} }
import { import {
to = module.repository["git.unkin.net/unkin/forgebot-skills"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/forgebot-skills"].gitea_repository.this
id = "140" id = "140"
} }
import { import {
to = module.repository["git.unkin.net/unkin/container-agent-base"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/container-agent-base"].gitea_repository.this
id = "141" id = "141"
} }
import { import {
to = module.repository["git.unkin.net/unkin/container-agent-dev"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/container-agent-dev"].gitea_repository.this
id = "142" id = "142"
} }
import { import {
to = module.repository["git.unkin.net/unkin/container-agent-infra"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/container-agent-infra"].gitea_repository.this
id = "143" id = "143"
} }
@@ -199,7 +199,7 @@ import {
} }
import { import {
to = module.repository["git.unkin.net/unkin/terraform-git"].gitea_repository.this[0] to = module.repository["git.unkin.net/unkin/terraform-git"].gitea_repository.this
id = "144" id = "144"
} }
-11
View File
@@ -19,7 +19,6 @@ module "repository" {
name = each.value.name name = each.value.name
organisation = each.value.organisation organisation = each.value.organisation
fork_from = each.value.fork_from
description = each.value.description description = each.value.description
private = each.value.private private = each.value.private
default_branch = each.value.default_branch default_branch = each.value.default_branch
@@ -36,16 +35,6 @@ module "repository" {
website = each.value.website website = each.value.website
autodetect_manual_merge = each.value.autodetect_manual_merge autodetect_manual_merge = each.value.autodetect_manual_merge
mirror = each.value.mirror
migration_clone_address = each.value.migration_clone_address
migration_service = each.value.migration_service
migration_mirror_interval = each.value.migration_mirror_interval
migration_lfs = each.value.migration_lfs
migration_lfs_endpoint = each.value.migration_lfs_endpoint
migration_issue_labels = each.value.migration_issue_labels
migration_milestones = each.value.migration_milestones
migration_releases = each.value.migration_releases
depends_on = [module.organisation] depends_on = [module.organisation]
} }
@@ -24,4 +24,16 @@ resource "gitea_repository_branch_protection" "this" {
require_signed_commits = var.require_signed_commits require_signed_commits = var.require_signed_commits
protected_file_patterns = var.protected_file_patterns protected_file_patterns = var.protected_file_patterns
unprotected_file_patterns = var.unprotected_file_patterns unprotected_file_patterns = var.unprotected_file_patterns
lifecycle {
# Gitea resolves whitelist names to IDs and returns them ordered by repo-reader (user ID for users, name for teams), dropping any entity without repo access, so the read-back representation never matches the config and plans a spurious update every run.
ignore_changes = [
push_whitelist_users,
push_whitelist_teams,
merge_whitelist_users,
merge_whitelist_teams,
approval_whitelist_users,
approval_whitelist_teams,
]
}
} }
@@ -1,10 +1,4 @@
locals {
fork_source = var.fork_from == null ? null : split("/", var.fork_from)
}
resource "gitea_repository" "this" { resource "gitea_repository" "this" {
count = var.fork_from == null ? 1 : 0
username = var.organisation username = var.organisation
name = var.name name = var.name
description = var.description description = var.description
@@ -23,33 +17,4 @@ resource "gitea_repository" "this" {
website = var.website website = var.website
autodetect_manual_merge = var.autodetect_manual_merge autodetect_manual_merge = var.autodetect_manual_merge
archive_on_destroy = true archive_on_destroy = true
mirror = var.mirror
migration_clone_address = var.migration_clone_address
migration_service = var.migration_service
migration_mirror_interval = var.migration_mirror_interval
migration_lfs = var.migration_lfs
migration_lfs_endpoint = var.migration_lfs_endpoint
migration_issue_labels = var.migration_issue_labels
migration_milestones = var.migration_milestones
migration_releases = var.migration_releases
lifecycle {
# migration_mirror_interval defaults to "8h0m0s" but Gitea returns an empty MirrorInterval for non-mirror repos, so the read-back never matches and plans a spurious update every run; it is a migration-only knob with no drift to track here.
ignore_changes = [migration_mirror_interval]
}
}
moved {
from = gitea_repository.this
to = gitea_repository.this[0]
}
# gitea_fork has no archive_on_destroy, so removing a fork from the config deletes the repository instead of archiving it.
resource "gitea_fork" "this" {
count = var.fork_from == null ? 0 : 1
owner = local.fork_source[0]
repo = local.fork_source[1]
organization = var.organisation
} }
@@ -1,3 +1,3 @@
output "id" { output "id" {
value = one(concat(gitea_repository.this[*].id, gitea_fork.this[*].id)) value = gitea_repository.this.id
} }
@@ -6,22 +6,6 @@ variable "organisation" {
type = string type = string
} }
variable "fork_from" {
description = "Source repository to fork, as \"<owner>/<repo>\". When set, a fork is created instead of a new repository."
type = string
default = null
validation {
condition = var.fork_from == null || can(regex("^[^/]+/[^/]+$", var.fork_from))
error_message = "fork_from must be \"<owner>/<repo>\"."
}
validation {
condition = var.fork_from == null || try(split("/", var.fork_from)[1], null) == var.name
error_message = "fork_from source repository must be named \"${var.name}\": a fork inherits the source name, so it cannot differ from the config file name."
}
}
variable "description" { variable "description" {
type = string type = string
default = null default = null
@@ -96,48 +80,3 @@ variable "autodetect_manual_merge" {
type = bool type = bool
default = null default = null
} }
variable "mirror" {
type = bool
default = null
}
variable "migration_clone_address" {
type = string
default = null
}
variable "migration_service" {
type = string
default = null
}
variable "migration_mirror_interval" {
type = string
default = null
}
variable "migration_lfs" {
type = bool
default = null
}
variable "migration_lfs_endpoint" {
type = string
default = null
}
variable "migration_issue_labels" {
type = bool
default = null
}
variable "migration_milestones" {
type = bool
default = null
}
variable "migration_releases" {
type = bool
default = null
}
-24
View File
@@ -17,7 +17,6 @@ variable "repository" {
type = map(object({ type = map(object({
name = string name = string
organisation = string organisation = string
fork_from = optional(string)
description = optional(string) description = optional(string)
private = optional(bool) private = optional(bool)
default_branch = optional(string) default_branch = optional(string)
@@ -33,31 +32,8 @@ variable "repository" {
repo_template = optional(bool) repo_template = optional(bool)
website = optional(string) website = optional(string)
autodetect_manual_merge = optional(bool) autodetect_manual_merge = optional(bool)
mirror = optional(bool)
migration_clone_address = optional(string)
migration_service = optional(string)
migration_mirror_interval = optional(string)
migration_lfs = optional(bool)
migration_lfs_endpoint = optional(string)
migration_issue_labels = optional(bool)
migration_milestones = optional(bool)
migration_releases = optional(bool)
})) }))
default = {} default = {}
validation {
condition = alltrue([
for key, repo in var.repository :
length([for attribute, value in repo : attribute if value != null && !contains(["name", "organisation", "fork_from"], attribute)]) == 0
if repo.fork_from != null
])
error_message = "gitea_fork only takes the source repository and the owning organisation, so fork_from cannot be combined with other repository settings: ${join("; ", [
for key, repo in var.repository :
"${key} also sets ${join(", ", [for attribute, value in repo : attribute if value != null && !contains(["name", "organisation", "fork_from"], attribute)])}"
if repo.fork_from != null && length([for attribute, value in repo : attribute if value != null && !contains(["name", "organisation", "fork_from"], attribute)]) > 0
])}."
}
} }
variable "branch_protection" { variable "branch_protection" {