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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ organization and Actions axes:
(who is an owner, the all-members team, ruleset tuning) is an input, not a baked-in
constant.

See [`docs/PLAN.md`](docs/PLAN.md) for the design rationale, a comparison with the
See [`docs/DESIGN.md`](docs/DESIGN.md) for the design rationale, a comparison with the
existing ecosystem modules, and the feature roadmap.

## Architecture
Expand Down Expand Up @@ -106,14 +106,14 @@ root).
| Name | Description | Type | Default | Required |
| ---- | ----------- | ---- | ------- | :------: |
| <a name="input_all_members_team_name"></a> [all\_members\_team\_name](#input\_all\_members\_team\_name) | Name of the all-members team. | `string` | `"everyone"` | no |
| <a name="input_baseline_ruleset"></a> [baseline\_ruleset](#input\_baseline\_ruleset) | Baseline org-wide branch-protection ruleset. See the organization submodule for the object schema; set enabled = false to disable. | <pre>object({<br/> enabled = optional(bool, true)<br/> block_deletion = optional(bool, true)<br/> block_force_pushes = optional(bool, false)<br/> required_approving_review_count = optional(number, 1)<br/> require_code_owner_review = optional(bool, true)<br/> require_last_push_approval = optional(bool, false)<br/> dismiss_stale_reviews_on_push = optional(bool, false)<br/> required_review_thread_resolution = optional(bool, true)<br/> })</pre> | `{}` | no |
| <a name="input_create_all_members_team"></a> [create\_all\_members\_team](#input\_create\_all\_members\_team) | Create an all-members team that grants default read access to non-restricted repositories. | `bool` | `true` | no |
| <a name="input_enabled_presets"></a> [enabled\_presets](#input\_enabled\_presets) | Names of built-in ruleset presets to enable. See modules/organization/main.tf local.presets. | `list(string)` | <pre>[<br/> "require_pull_request_reviews",<br/> "restrict_deletions",<br/> "require_signed_commits"<br/>]</pre> | no |
| <a name="input_organization_rulesets"></a> [organization\_rulesets](#input\_organization\_rulesets) | Organization rulesets, keyed by name. Merged with (and overridden by name by)<br/>the presets selected via enabled\_presets. bypass\_actors uses raw numeric<br/>actor\_id (OrganizationAdmin needs none); team-name bypass is per-repo only. | <pre>map(object({<br/> enforcement = optional(string, "active")<br/> target = optional(string, "branch")<br/> include_refs = optional(list(string), ["~ALL"])<br/> exclude_refs = optional(list(string), [])<br/> include_repositories = optional(list(string), ["~ALL"])<br/> exclude_repositories = optional(list(string), [])<br/> bypass_actors = optional(list(object({<br/> actor_type = string<br/> actor_id = optional(number, 0)<br/> bypass_mode = optional(string, "always")<br/> })), [])<br/> rules = object({<br/> creation = optional(bool, false)<br/> update = optional(bool, false)<br/> deletion = optional(bool, false)<br/> non_fast_forward = optional(bool, false)<br/> required_signatures = optional(bool, false)<br/> required_linear_history = optional(bool, false)<br/> pull_request = optional(object({<br/> required_approving_review_count = optional(number, 0)<br/> require_code_owner_review = optional(bool, false)<br/> require_last_push_approval = optional(bool, false)<br/> dismiss_stale_reviews_on_push = optional(bool, false)<br/> required_review_thread_resolution = optional(bool, false)<br/> }))<br/> })<br/> }))</pre> | `{}` | no |
| <a name="input_organization_secrets"></a> [organization\_secrets](#input\_organization\_secrets) | Visibility config for org-level shared secrets. Values come from var.secrets.org. | <pre>map(object({<br/> visibility = optional(string, "private")<br/> repositories = optional(list(string), [])<br/> }))</pre> | `{}` | no |
| <a name="input_organization_variables"></a> [organization\_variables](#input\_organization\_variables) | Org-level shared Actions variables (plaintext value + visibility). | <pre>map(object({<br/> value = string<br/> visibility = optional(string, "private")<br/> repositories = optional(list(string), [])<br/> }))</pre> | `{}` | no |
| <a name="input_owners"></a> [owners](#input\_owners) | Explicit set of owner usernames (mapped to GitHub's `admin` role). Ignored when `owners_team` is set. | `set(string)` | `[]` | no |
| <a name="input_owners_team"></a> [owners\_team](#input\_owners\_team) | Convenience: derive organization owners from the members of this team, by<br/>name. `null` (default) means use the explicit `owners` set instead. There is<br/>no hardcoded owners team — the consumer chooses. | `string` | `null` | no |
| <a name="input_repositories"></a> [repositories](#input\_repositories) | Repositories and which teams may access them. See the repository submodule for the full object schema and validations. | <pre>list(object({<br/> name = string<br/> description = string<br/> visibility = optional(string, "private")<br/> readers = optional(list(string), [])<br/> writers = optional(list(string), [])<br/> admins = optional(list(string), [])<br/> restricted = optional(bool, false)<br/><br/> topics = optional(list(string), [])<br/> homepage_url = optional(string, null)<br/> has_issues = optional(bool, true)<br/> has_wiki = optional(bool, false)<br/> has_projects = optional(bool, false)<br/> has_downloads = optional(bool, true)<br/> is_template = optional(bool, false)<br/> gitignore_template = optional(string, null)<br/> license_template = optional(string, null)<br/> archived = optional(bool, false)<br/> archive_on_destroy = optional(bool, false)<br/> default_branch = optional(string, null)<br/> enable_pages = optional(bool, false)<br/> signed_commits = optional(bool, true)<br/><br/> allow_merge_commit = optional(bool, true)<br/> allow_squash_merge = optional(bool, false)<br/> allow_rebase_merge = optional(bool, false)<br/> allow_auto_merge = optional(bool, true)<br/> delete_branch_on_merge = optional(bool, true)<br/> merge_commit_message = optional(string, "PR_BODY")<br/> merge_commit_title = optional(string, "PR_TITLE")<br/><br/> template = optional(object({<br/> owner = string<br/> repository = string<br/> include_all_branches = optional(bool, false)<br/> }))<br/><br/> variables = optional(map(string), {})<br/> environments = optional(map(object({<br/> variables = optional(map(string), {})<br/> reviewers = optional(list(string), [])<br/> wait_timer = optional(number, 0)<br/> deployment_branch_policy = optional(object({<br/> protected_branches = optional(bool, false)<br/> custom_branch_policies = optional(list(string), [])<br/> }))<br/> })), {})<br/> }))</pre> | `[]` | no |
| <a name="input_require_signed_commits"></a> [require\_signed\_commits](#input\_require\_signed\_commits) | Require signed commits on all branches of all repositories (except repositories with signed\_commits = false). | `bool` | `true` | no |
| <a name="input_repositories"></a> [repositories](#input\_repositories) | Repositories and which teams may access them. See the repository submodule for the full object schema and validations. | <pre>list(object({<br/> name = string<br/> description = string<br/> visibility = optional(string, "private")<br/> readers = optional(list(string), [])<br/> writers = optional(list(string), [])<br/> admins = optional(list(string), [])<br/> restricted = optional(bool, false)<br/><br/> topics = optional(list(string), [])<br/> homepage_url = optional(string, null)<br/> has_issues = optional(bool, true)<br/> has_wiki = optional(bool, false)<br/> has_projects = optional(bool, false)<br/> has_downloads = optional(bool, true)<br/> is_template = optional(bool, false)<br/> gitignore_template = optional(string, null)<br/> license_template = optional(string, null)<br/> archived = optional(bool, false)<br/> archive_on_destroy = optional(bool, false)<br/> default_branch = optional(string, null)<br/> enable_pages = optional(bool, false)<br/> signed_commits = optional(bool, true)<br/><br/> allow_merge_commit = optional(bool, true)<br/> allow_squash_merge = optional(bool, false)<br/> allow_rebase_merge = optional(bool, false)<br/> allow_auto_merge = optional(bool, true)<br/> delete_branch_on_merge = optional(bool, true)<br/> merge_commit_message = optional(string, "PR_BODY")<br/> merge_commit_title = optional(string, "PR_TITLE")<br/><br/> template = optional(object({<br/> owner = string<br/> repository = string<br/> include_all_branches = optional(bool, false)<br/> }))<br/><br/> rulesets = optional(map(object({<br/> enforcement = optional(string, "active")<br/> target = optional(string, "branch")<br/> include_refs = optional(list(string), ["~ALL"])<br/> exclude_refs = optional(list(string), [])<br/> bypass_actors = optional(list(object({<br/> actor_type = string<br/> actor_id = optional(number, 0)<br/> team = optional(string)<br/> bypass_mode = optional(string, "always")<br/> })), [])<br/> rules = object({<br/> creation = optional(bool, false)<br/> update = optional(bool, false)<br/> deletion = optional(bool, false)<br/> non_fast_forward = optional(bool, false)<br/> required_signatures = optional(bool, false)<br/> required_linear_history = optional(bool, false)<br/> pull_request = optional(object({<br/> required_approving_review_count = optional(number, 0)<br/> require_code_owner_review = optional(bool, false)<br/> require_last_push_approval = optional(bool, false)<br/> dismiss_stale_reviews_on_push = optional(bool, false)<br/> required_review_thread_resolution = optional(bool, false)<br/> }))<br/> })<br/> })), {})<br/><br/> variables = optional(map(string), {})<br/> environments = optional(map(object({<br/> variables = optional(map(string), {})<br/> reviewers = optional(list(string), [])<br/> wait_timer = optional(number, 0)<br/> deployment_branch_policy = optional(object({<br/> protected_branches = optional(bool, false)<br/> custom_branch_policies = optional(list(string), [])<br/> }))<br/> })), {})<br/> }))</pre> | `[]` | no |
| <a name="input_secrets"></a> [secrets](#input\_secrets) | Decrypted secret values keyed by scope. Supplied already-decrypted by the caller; this module never performs decryption. | <pre>object({<br/> org = optional(map(string), {})<br/> repos = optional(map(object({<br/> actions = optional(map(string), {})<br/> environments = optional(map(map(string)), {})<br/> })), {})<br/> })</pre> | <pre>{<br/> "org": {},<br/> "repos": {}<br/>}</pre> | no |
| <a name="input_teams"></a> [teams](#input\_teams) | Teams and their membership. A team may hold predefined organization-level<br/>GitHub roles via `org_roles`. Repository access is granted per repository via<br/>readers/writers/admins, not through org roles. | <pre>list(object({<br/> name = string<br/> description = string<br/> members = optional(list(string), [])<br/> org_roles = optional(list(string), [])<br/> }))</pre> | `[]` | no |
| <a name="input_users"></a> [users](#input\_users) | Organization members. `fullname`/`email` are informational; membership is keyed on `username`. | <pre>list(object({<br/> username = string<br/> fullname = optional(string, "")<br/> email = optional(string, "")<br/> }))</pre> | `[]` | no |
Expand Down
20 changes: 13 additions & 7 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ maintained suite that additionally covers ground none of them do:
| Actions variables (repo + org) | ✅ |
| Actions secrets (repo + org + environment) | ✅ |
| Environments (reviewers, wait timers, branch policies) | ✅ |
| Organization rulesets (baseline + signed commits) | ✅ |
| Organization + per-repo rulesets (generic, preset-driven) | ✅ |
| Organization roles assigned to teams | ✅ |

## Architecture
Expand All @@ -41,15 +41,17 @@ modules/organization → modules/teams → modules/repository
```

- **`organization`** — org membership (owners mapped to `admin`, everyone else
`member`) and org rulesets (baseline branch protection + signed commits). Outputs
the membership map.
`member`) and generic organization rulesets: a `organization_rulesets` map
merged with curated, opt-in `enabled_presets` (PR reviews, deletion/force-push
protection, signed commits, linear history). Outputs the membership map.
- **`teams`** — teams, team memberships, the optional all-members team, and org-role
assignments (resolved by name against the org's predefined roles). Consumes the
membership map + owner set; outputs a `name → {id, slug}` team map and the
all-members team id.
- **`repository`** — repositories, team↔repo grants, the default all-members read
grant, repository/environment Actions variables & secrets, and environments.
Consumes the team map; outputs a repository-id map.
grant, per-repo `rulesets` (same generic shape as the org rulesets, plus a
team-name bypass convenience), repository/environment Actions variables &
secrets, and environments. Consumes the team map; outputs a repository-id map.

The **root** additionally manages organization-level Actions secrets and variables.
These need both decrypted secret values and repository ids to resolve `selected`
Expand All @@ -64,7 +66,9 @@ repository module (which would create a dependency cycle).
convenience that derives owners from a named team. No owners team is assumed.
- The **all-members team** is controlled by `create_all_members_team` /
`all_members_team_name`.
- The **baseline ruleset** is a tunable object (and can be disabled).
- **Rulesets** are a generic `organization_rulesets` / per-repo `rulesets` map
input; curated presets are opt-in via `enabled_presets` rather than hardcoded
defaults baked into a single "baseline" object.
- **Org roles** validate at plan time against the roles GitHub actually exposes,
not a static allowlist.
2. **Secrets are never decrypted here.** The caller passes already-decrypted values
Expand All @@ -76,7 +80,9 @@ repository module (which would create a dependency cycle).
**Tier 1 — per-repo depth (`modules/repository`)**

- Issue labels (`github_issue_label`), optional merge-with-github-defaults
- Per-repo `github_repository_ruleset` (composes with the org baseline ruleset)
- ~~Per-repo `github_repository_ruleset`~~ — done: `repositories[].rulesets` is a
generic map (same `rules`/`bypass_actors` shape as `organization_rulesets`,
plus a `team` bypass convenience), composing alongside org-level presets.
- Expose currently-fixed repo settings: `topics`, `homepage_url`, merge-strategy
toggles, `has_wiki`/`has_downloads`, `is_template`, template source,
`gitignore`/`license_template`, `archived`/`archive_on_destroy`, `default_branch`
Expand Down
20 changes: 17 additions & 3 deletions examples/complete/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ module "orgkit" {
writers = ["developers"]
admins = ["owners"]
variables = { NODE_ENV = "production" }
rulesets = {
"Protect main" = {
include_refs = ["~DEFAULT_BRANCH"]
bypass_actors = [{ actor_type = "Team", team = "owners" }]
rules = { required_linear_history = true }
}
}
environments = {
# protected_branches style: only protected branches may deploy.
production = {
Expand Down Expand Up @@ -122,8 +129,15 @@ module "orgkit" {
}
}

# The baseline ruleset is tunable; here we require two approvals.
baseline_ruleset = {
required_approving_review_count = 2
# Enable a curated subset of built-in ruleset presets.
enabled_presets = ["require_pull_request_reviews", "restrict_deletions", "require_signed_commits", "block_force_pushes"]

# A custom org ruleset alongside the presets.
organization_rulesets = {
"No Tag Deletes" = {
target = "tag"
bypass_actors = [{ actor_type = "OrganizationAdmin" }]
rules = { deletion = true }
}
}
}
4 changes: 2 additions & 2 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ module "organization" {

members = local.usernames
owners = local.owners
baseline_ruleset = var.baseline_ruleset
require_signed_commits = var.require_signed_commits
organization_rulesets = var.organization_rulesets
enabled_presets = var.enabled_presets
signed_commits_excluded_repositories = local.signed_commits_excluded
}

Expand Down
Loading
Loading