diff --git a/README.md b/README.md index 12cd6eb..13e8ac0 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,21 @@ root). - [`examples/complete`](examples/complete) — manage an entire organization. - [`examples/repository`](examples/repository) — use only the repository submodule. +## Stability + +orgkit is **pre-1.0** and the input interface may still change between minor +releases. Pin a version (`version = "~> 0.2"`) and review the changelog before +upgrading. Breaking input changes so far, since the initial release: + +- **`unsafe_merges` removed** — use `allow_squash_merge` / `allow_rebase_merge` + (and the other granular merge controls) instead. +- **`baseline_ruleset` and `require_signed_commits` removed** — use + `enabled_presets` (defaults reproduce the previous behavior) and + `organization_rulesets`; per-repo rules via `repositories[].rulesets`. +- **Default merge strategy is now squash-only** (`allow_squash_merge = true`, + `allow_merge_commit = false`). Set the `allow_*` fields per repository to + change it. + ## Requirements @@ -113,7 +128,7 @@ root). | [organization\_variables](#input\_organization\_variables) | Org-level shared Actions variables (plaintext value + visibility). |
map(object({
value = string
visibility = optional(string, "private")
repositories = optional(list(string), [])
})) | `{}` | no |
| [owners](#input\_owners) | Explicit set of owner usernames (mapped to GitHub's `admin` role). Ignored when `owners_team` is set. | `set(string)` | `[]` | no |
| [owners\_team](#input\_owners\_team) | Convenience: derive organization owners from the members of this team, bylist(object({
name = string
description = string
visibility = optional(string, "private")
readers = optional(list(string), [])
writers = optional(list(string), [])
admins = optional(list(string), [])
restricted = optional(bool, false)
topics = optional(list(string), [])
homepage_url = optional(string, null)
has_issues = optional(bool, true)
has_wiki = optional(bool, false)
has_projects = optional(bool, false)
has_downloads = optional(bool, true)
is_template = optional(bool, false)
gitignore_template = optional(string, null)
license_template = optional(string, null)
archived = optional(bool, false)
archive_on_destroy = optional(bool, false)
default_branch = optional(string, null)
enable_pages = optional(bool, false)
signed_commits = optional(bool, true)
allow_merge_commit = optional(bool, true)
allow_squash_merge = optional(bool, false)
allow_rebase_merge = optional(bool, false)
allow_auto_merge = optional(bool, true)
delete_branch_on_merge = optional(bool, true)
merge_commit_message = optional(string, "PR_BODY")
merge_commit_title = optional(string, "PR_TITLE")
template = optional(object({
owner = string
repository = string
include_all_branches = optional(bool, false)
}))
rulesets = optional(map(object({
enforcement = optional(string, "active")
target = optional(string, "branch")
include_refs = optional(list(string), ["~ALL"])
exclude_refs = optional(list(string), [])
bypass_actors = optional(list(object({
actor_type = string
actor_id = optional(number, 0)
team = optional(string)
bypass_mode = optional(string, "always")
})), [])
rules = object({
creation = optional(bool, false)
update = optional(bool, false)
deletion = optional(bool, false)
non_fast_forward = optional(bool, false)
required_signatures = optional(bool, false)
required_linear_history = optional(bool, false)
pull_request = optional(object({
required_approving_review_count = optional(number, 0)
require_code_owner_review = optional(bool, false)
require_last_push_approval = optional(bool, false)
dismiss_stale_reviews_on_push = optional(bool, false)
required_review_thread_resolution = optional(bool, false)
}))
})
})), {})
variables = optional(map(string), {})
environments = optional(map(object({
variables = optional(map(string), {})
reviewers = optional(list(string), [])
wait_timer = optional(number, 0)
deployment_branch_policy = optional(object({
protected_branches = optional(bool, false)
custom_branch_policies = optional(list(string), [])
}))
})), {})
})) | `[]` | no |
+| [repositories](#input\_repositories) | Repositories and which teams may access them. See the repository submodule for the full object schema and validations. | list(object({
name = string
description = string
visibility = optional(string, "private")
readers = optional(list(string), [])
writers = optional(list(string), [])
admins = optional(list(string), [])
restricted = optional(bool, false)
topics = optional(list(string), [])
homepage_url = optional(string, null)
has_issues = optional(bool, true)
has_wiki = optional(bool, false)
has_projects = optional(bool, false)
has_downloads = optional(bool, true)
is_template = optional(bool, false)
gitignore_template = optional(string, null)
license_template = optional(string, null)
archived = optional(bool, false)
archive_on_destroy = optional(bool, false)
default_branch = optional(string, null)
enable_pages = optional(bool, false)
signed_commits = optional(bool, true)
allow_merge_commit = optional(bool, false)
allow_squash_merge = optional(bool, true)
allow_rebase_merge = optional(bool, false)
allow_auto_merge = optional(bool, true)
delete_branch_on_merge = optional(bool, true)
merge_commit_message = optional(string, "PR_BODY")
merge_commit_title = optional(string, "PR_TITLE")
template = optional(object({
owner = string
repository = string
include_all_branches = optional(bool, false)
}))
rulesets = optional(map(object({
enforcement = optional(string, "active")
target = optional(string, "branch")
include_refs = optional(list(string), ["~ALL"])
exclude_refs = optional(list(string), [])
bypass_actors = optional(list(object({
actor_type = string
actor_id = optional(number, 0)
team = optional(string)
bypass_mode = optional(string, "always")
})), [])
rules = object({
creation = optional(bool, false)
update = optional(bool, false)
deletion = optional(bool, false)
non_fast_forward = optional(bool, false)
required_signatures = optional(bool, false)
required_linear_history = optional(bool, false)
pull_request = optional(object({
required_approving_review_count = optional(number, 0)
require_code_owner_review = optional(bool, false)
require_last_push_approval = optional(bool, false)
dismiss_stale_reviews_on_push = optional(bool, false)
required_review_thread_resolution = optional(bool, false)
}))
})
})), {})
variables = optional(map(string), {})
environments = optional(map(object({
variables = optional(map(string), {})
reviewers = optional(list(string), [])
wait_timer = optional(number, 0)
deployment_branch_policy = optional(object({
protected_branches = optional(bool, false)
custom_branch_policies = optional(list(string), [])
}))
})), {})
})) | `[]` | no |
| [secrets](#input\_secrets) | Decrypted secret values keyed by scope. Supplied already-decrypted by the caller; this module never performs decryption. | object({
org = optional(map(string), {})
repos = optional(map(object({
actions = optional(map(string), {})
environments = optional(map(map(string)), {})
})), {})
}) | {
"org": {},
"repos": {}
} | no |
| [teams](#input\_teams) | Teams and their membership. A team may hold predefined organization-levellist(object({
name = string
description = string
members = optional(list(string), [])
org_roles = optional(list(string), [])
})) | `[]` | no |
| [users](#input\_users) | Organization members. `fullname`/`email` are informational; membership is keyed on `username`. | list(object({
username = string
fullname = optional(string, "")
email = optional(string, "")
})) | `[]` | no |
diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index e2a3bbc..9aa3493 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -107,11 +107,9 @@ repository module (which would create a dependency cycle).
manageable in the provider) and the deprecated `branch_protection_v3` resource
(rulesets supersede it).
-## Planned default changes
+## Merge strategy default
-Defaults intended to change in a future major release. They preserve today's
-behavior for now, so existing configs see no plan churn until then:
-
-- **Merge strategy** — switch the default from merge commits to squash-only
- (`allow_squash_merge = true`, `allow_merge_commit = false`). The module currently
- defaults to merge commits.
+Repositories default to **squash-only** merges (`allow_squash_merge = true`,
+`allow_merge_commit = false`, `allow_rebase_merge = false`). Override per
+repository via the `allow_merge_commit` / `allow_squash_merge` /
+`allow_rebase_merge` fields (at least one must be enabled).
diff --git a/modules/repository/README.md b/modules/repository/README.md
index 237f9d6..99f6261 100644
--- a/modules/repository/README.md
+++ b/modules/repository/README.md
@@ -47,7 +47,7 @@ No modules.
| Name | Description | Type | Default | Required |
| ---- | ----------- | ---- | ------- | :------: |
| [all\_members\_team\_id](#input\_all\_members\_team\_id) | ID of the all-members team that receives default read access on non-restricted repositories. null disables the default read grant. | `string` | `null` | no |
-| [repositories](#input\_repositories) | Repositories to manage and which teams may access them. `readers`, `writers`list(object({
name = string
description = string
visibility = optional(string, "private")
readers = optional(list(string), [])
writers = optional(list(string), [])
admins = optional(list(string), [])
restricted = optional(bool, false)
topics = optional(list(string), [])
homepage_url = optional(string, null)
has_issues = optional(bool, true)
has_wiki = optional(bool, false)
has_projects = optional(bool, false)
has_downloads = optional(bool, true)
is_template = optional(bool, false)
gitignore_template = optional(string, null)
license_template = optional(string, null)
archived = optional(bool, false)
archive_on_destroy = optional(bool, false)
default_branch = optional(string, null)
enable_pages = optional(bool, false)
signed_commits = optional(bool, true)
allow_merge_commit = optional(bool, true)
allow_squash_merge = optional(bool, false)
allow_rebase_merge = optional(bool, false)
allow_auto_merge = optional(bool, true)
delete_branch_on_merge = optional(bool, true)
merge_commit_message = optional(string, "PR_BODY")
merge_commit_title = optional(string, "PR_TITLE")
template = optional(object({
owner = string
repository = string
include_all_branches = optional(bool, false)
}))
rulesets = optional(map(object({
enforcement = optional(string, "active")
target = optional(string, "branch")
include_refs = optional(list(string), ["~ALL"])
exclude_refs = optional(list(string), [])
bypass_actors = optional(list(object({
actor_type = string
actor_id = optional(number, 0)
team = optional(string)
bypass_mode = optional(string, "always")
})), [])
rules = object({
creation = optional(bool, false)
update = optional(bool, false)
deletion = optional(bool, false)
non_fast_forward = optional(bool, false)
required_signatures = optional(bool, false)
required_linear_history = optional(bool, false)
pull_request = optional(object({
required_approving_review_count = optional(number, 0)
require_code_owner_review = optional(bool, false)
require_last_push_approval = optional(bool, false)
dismiss_stale_reviews_on_push = optional(bool, false)
required_review_thread_resolution = optional(bool, false)
}))
})
})), {})
variables = optional(map(string), {})
environments = optional(map(object({
variables = optional(map(string), {})
reviewers = optional(list(string), [])
wait_timer = optional(number, 0)
deployment_branch_policy = optional(object({
protected_branches = optional(bool, false)
custom_branch_policies = optional(list(string), [])
}))
})), {})
})) | `[]` | no |
+| [repositories](#input\_repositories) | Repositories to manage and which teams may access them. `readers`, `writers`list(object({
name = string
description = string
visibility = optional(string, "private")
readers = optional(list(string), [])
writers = optional(list(string), [])
admins = optional(list(string), [])
restricted = optional(bool, false)
topics = optional(list(string), [])
homepage_url = optional(string, null)
has_issues = optional(bool, true)
has_wiki = optional(bool, false)
has_projects = optional(bool, false)
has_downloads = optional(bool, true)
is_template = optional(bool, false)
gitignore_template = optional(string, null)
license_template = optional(string, null)
archived = optional(bool, false)
archive_on_destroy = optional(bool, false)
default_branch = optional(string, null)
enable_pages = optional(bool, false)
signed_commits = optional(bool, true)
allow_merge_commit = optional(bool, false)
allow_squash_merge = optional(bool, true)
allow_rebase_merge = optional(bool, false)
allow_auto_merge = optional(bool, true)
delete_branch_on_merge = optional(bool, true)
merge_commit_message = optional(string, "PR_BODY")
merge_commit_title = optional(string, "PR_TITLE")
template = optional(object({
owner = string
repository = string
include_all_branches = optional(bool, false)
}))
rulesets = optional(map(object({
enforcement = optional(string, "active")
target = optional(string, "branch")
include_refs = optional(list(string), ["~ALL"])
exclude_refs = optional(list(string), [])
bypass_actors = optional(list(object({
actor_type = string
actor_id = optional(number, 0)
team = optional(string)
bypass_mode = optional(string, "always")
})), [])
rules = object({
creation = optional(bool, false)
update = optional(bool, false)
deletion = optional(bool, false)
non_fast_forward = optional(bool, false)
required_signatures = optional(bool, false)
required_linear_history = optional(bool, false)
pull_request = optional(object({
required_approving_review_count = optional(number, 0)
require_code_owner_review = optional(bool, false)
require_last_push_approval = optional(bool, false)
dismiss_stale_reviews_on_push = optional(bool, false)
required_review_thread_resolution = optional(bool, false)
}))
})
})), {})
variables = optional(map(string), {})
environments = optional(map(object({
variables = optional(map(string), {})
reviewers = optional(list(string), [])
wait_timer = optional(number, 0)
deployment_branch_policy = optional(object({
protected_branches = optional(bool, false)
custom_branch_policies = optional(list(string), [])
}))
})), {})
})) | `[]` | no |
| [repository\_secrets](#input\_repository\_secrets) | Per-repository Actions and environment secret values (plaintext), keyed bymap(object({
actions = optional(map(string), {})
environments = optional(map(map(string)), {})
})) | `{}` | no |
| [teams](#input\_teams) | Map of team name => { id, slug } for teams that may be granted repository access or set as environment reviewers. Supplied by the teams module. | map(object({
id = string
slug = string
})) | `{}` | no |
diff --git a/modules/repository/tests/repository.tftest.hcl b/modules/repository/tests/repository.tftest.hcl
index 60f7207..ec1db39 100644
--- a/modules/repository/tests/repository.tftest.hcl
+++ b/modules/repository/tests/repository.tftest.hcl
@@ -134,7 +134,7 @@ run "rejects_env_secret_for_undeclared_environment" {
expect_failures = [github_actions_environment_secret.internal]
}
-run "defaults_preserve_current_behavior" {
+run "default_merge_strategy_is_squash_only" {
command = plan
variables {
@@ -144,15 +144,14 @@ run "defaults_preserve_current_behavior" {
assert {
condition = alltrue([
github_repository.internal["app"].has_issues,
- github_repository.internal["app"].allow_merge_commit,
+ github_repository.internal["app"].allow_squash_merge,
github_repository.internal["app"].allow_auto_merge,
github_repository.internal["app"].delete_branch_on_merge,
!github_repository.internal["app"].has_wiki,
- !github_repository.internal["app"].allow_squash_merge,
+ !github_repository.internal["app"].allow_merge_commit,
!github_repository.internal["app"].allow_rebase_merge,
- github_repository.internal["app"].merge_commit_message == "PR_BODY",
])
- error_message = "Repository defaults must reproduce the previous hardcoded behavior."
+ error_message = "Repositories must default to squash-only merges (squash on; merge-commit and rebase off)."
}
}
diff --git a/modules/repository/variables.tf b/modules/repository/variables.tf
index d54f304..7e4bba6 100644
--- a/modules/repository/variables.tf
+++ b/modules/repository/variables.tf
@@ -29,8 +29,8 @@ variable "repositories" {
enable_pages = optional(bool, false)
signed_commits = optional(bool, true)
- allow_merge_commit = optional(bool, true)
- allow_squash_merge = optional(bool, false)
+ allow_merge_commit = optional(bool, false)
+ allow_squash_merge = optional(bool, true)
allow_rebase_merge = optional(bool, false)
allow_auto_merge = optional(bool, true)
delete_branch_on_merge = optional(bool, true)
diff --git a/release-please-config.json b/release-please-config.json
index cef6429..bc7637d 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -4,6 +4,8 @@
"include-v-in-tag": true,
"tag-separator": "/",
"release-type": "terraform-module",
+ "bump-minor-pre-major": true,
+ "bump-patch-for-minor-pre-major": false,
"packages": {
".": {}
},
diff --git a/variables.tf b/variables.tf
index 1a00cf7..3897c7b 100644
--- a/variables.tf
+++ b/variables.tf
@@ -78,8 +78,8 @@ variable "repositories" {
enable_pages = optional(bool, false)
signed_commits = optional(bool, true)
- allow_merge_commit = optional(bool, true)
- allow_squash_merge = optional(bool, false)
+ allow_merge_commit = optional(bool, false)
+ allow_squash_merge = optional(bool, true)
allow_rebase_merge = optional(bool, false)
allow_auto_merge = optional(bool, true)
delete_branch_on_merge = optional(bool, true)