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: 8 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ out-of-band changes made in the GitHub UI.
every repo); each `<repo>.yaml` adds only that repo's Dependabot/Renovate
ecosystem extras. Authoritative β€” out-of-band label drift is reverted. This is
the Crossplane replacement for the old EndBug/label-sync workflow.
- `organization-rulesets/` β€” one `OrganizationRuleset` per file (org-wide branch/tag
protection). 10 existing org rulesets are adopted **Observe-first** (read-only) + 1
net-new `v*` tag-protection ruleset is managed. The 10 org rulesets the provider
can't yet express stay UI-managed β€” see
[`organization-rulesets/README.md`](organization-rulesets/README.md) for the full
importability matrix and the push/tag/Actions-policy analysis.
- `repository-rulesets/` β€” one `RepositoryRuleset` per file (`<verb>-on-<repo>.yaml`),
each a repo-scoped rule adopted Observe-first.

See the platform repo's
[`docs/github-management.md`](https://github.com/devantler-tech/platform/blob/main/docs/github-management.md)
Expand Down
2 changes: 2 additions & 0 deletions deploy/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ resources:
- repositories/
- teams/
- labels/
- organization-rulesets/
- repository-rulesets/
90 changes: 90 additions & 0 deletions deploy/organization-rulesets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# `organization-rulesets/` β€” org rulesets as code

Declarative management of the devantler-tech org's **`OrganizationRuleset`** resources
(org-wide branch/tag protection and policies) as Crossplane managed resources, via
[provider-upjet-github](https://github.com/crossplane-contrib/provider-upjet-github)
(from the `integrations/github` Terraform provider). Repo-scoped `RepositoryRuleset`
resources live in the sibling [`../repository-rulesets/`](../repository-rulesets/).
Reconciled by the platform `github-config` tenant like the rest of `deploy/`.

## How adoption works

- **Observe-first (read-only).** Existing rulesets are bound with
`managementPolicies: ["Observe"]` β€” Crossplane mirrors live GitHub state into
`status.atProvider` and **never writes, reverts, or deletes**. This is pure GitOps
*visibility*, with zero behaviour change (the same flow `repositories/` and `teams/`
used). `Delete` is omitted everywhere, so a CR/Flux prune can never delete a real
ruleset.
- **external-name = the import id.** `OrganizationRuleset` β†’ the numeric ruleset id
(`gh api orgs/devantler-tech/rulesets`); `RepositoryRuleset` β†’ `<repo>:<id>`.
- **forProvider is identity-only** on the Observe imports (`name`/`target`/
`enforcement`). ⚠️ **Do not** promote an import past `Observe` without first
backfilling its full `rules`/`conditions`/`bypassActors` from the observed
`status.atProvider` β€” the provider's round-trip is lossy, so a partial `forProvider`
under `Update` would wipe live rules.

## What is managed here

**One `OrganizationRuleset` per file**, named after the rule it enforces (an active
verb β€” e.g. `require-pull-request.yaml`). Repo-scoped rulesets live next door in
[`../repository-rulesets/`](../repository-rulesets/) as `<verb>-on-<repo>.yaml`.

| Files | Rulesets | Policy |
|---|---|---|
| 10 `OrganizationRuleset` files | the 10 org rulesets below | Observe (read-only import) |
| `protect-release-tags.yaml` | **Protect release tags** (net-new) | Managed (Create) β€” block tag delete + force-move + require `v<semver>` |
| (in `../repository-rulesets/`) `restrict-deletions-on-ksail.yaml`, `require-merge-queue-on-platform.yaml` | `ksail` "Restrict deletions", `platform` "Require merge queue" | Observe (read-only import) |

The 10 imported org rulesets: Block force pushes Β· Require a pull request before
merging Β· Require conversation resolution before merging Β· Require linear history Β·
Require signed commits Β· Require status checks to pass Β· Restrict deletions Β· Restrict
branch names Β· Restrict commit metadata Β· Require workflows (DependencyReview).

## What stays UI-managed, and why

`provider-upjet-github` v0.19.1 has a **narrower** ruleset schema than GitHub's API.
Verified against the live CRDs, it does **not** support:

- **`repository_property` conditions** (custom-property scoping) β€” only `refName`,
`repositoryName`, `repositoryId`.
- **Rule types** `code_quality`, `copilot_code_review`, `repository_transfer`,
`repository_name`, and the push-file rules (`file_path_restriction`, `max_file_size`,
`file_extension_restriction`, `max_file_path_length`).
- **Target `repository`** (only `branch`, `tag`, `push`).
- **Bypass actor `EnterpriseOwner`**.

So **10 of the 20 org rulesets cannot be faithfully expressed** and remain UI-managed:

| Ruleset (org) | Blocked by |
|---|---|
| Require code scanning results | `repository_property` condition (custom property `Type`) |
| Require workflows … EnableAutoMerge | `repository_property` condition |
| Require workflows … LintDocumentation | `repository_property` condition |
| Require workflows … ScanGitHubActions | `repository_property` condition |
| Require workflows for .NET | `repository_property` condition (`language`) |
| Require workflows for Go | `repository_property` condition (`language`) |
| Require code quality results | rule type `code_quality` unsupported |
| Automatically request Copilot code review | rule type `copilot_code_review` unsupported (also disabled) |
| restrict-names | target `repository` unsupported |
| restrict-transfers | target `repository` unsupported (+ `EnterpriseOwner` bypass) |

These are tracked for re-adoption as the provider gains support in
[#69](https://github.com/devantler-tech/.github/issues/69) (`roadmap`). Re-home each
here Observe-first once expressible.

## Push / tag / Actions-policy considerations

- **Push rulesets** β€” none exist, and **not adoptable**: the provider supports
`target: push` (beta) but none of the push-file rule types above, so a push ruleset
can't be expressed. Tracked in [#69](https://github.com/devantler-tech/.github/issues/69).
- **Tag rulesets** β€” none existed; **added** here (`protect-release-tags.yaml`). Makes
release tags immutable (block delete + force-move) and well-formed (`v<semver>`). See
that file's header for the team-vs-enterprise tier caveat on the name-pattern rule and
its fallback.
- **Actions policies** β€” the 2026-06-18
[workflow execution protections](https://github.blog/changelog/2026-06-18-control-who-and-what-triggers-github-actions-workflows/)
(actor + event allow-lists controlling who/what triggers workflows, delivered as org
rulesets scoped by **custom properties**) are **not adoptable**: the new rule types
aren't in the provider and the feature relies on the `repository_property` scoping the
provider lacks. Tracked in [#69](https://github.com/devantler-tech/.github/issues/69);
revisit when the provider catches up.
18 changes: 18 additions & 0 deletions deploy/organization-rulesets/block-force-pushes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: block-force-pushes
annotations:
crossplane.io/external-name: "6986186" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Block force pushes
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
21 changes: 21 additions & 0 deletions deploy/organization-rulesets/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# `OrganizationRuleset` resources β€” one per file, named after the rule (an active verb).
# 10 existing org rulesets are adopted Observe-first (read-only) and 1 net-new tag ruleset
# is managed (Create). The 10 org rulesets the provider cannot express remain UI-managed β€”
# see ./README.md for the importability matrix and the push/tag/Actions-policy analysis.
# Repo-scoped rulesets live in ../repository-rulesets/. Included by ../kustomization.yaml.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
# Adopted Observe-first (read-only).
- block-force-pushes.yaml
- require-pull-request.yaml
- require-conversation-resolution.yaml
- require-linear-history.yaml
- require-signed-commits.yaml
- require-status-checks.yaml
- restrict-deletions.yaml
- restrict-branch-names.yaml
- restrict-commit-metadata.yaml
- require-workflows-dependency-review.yaml
# Net-new, managed (Create).
- protect-release-tags.yaml
62 changes: 62 additions & 0 deletions deploy/organization-rulesets/protect-release-tags.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Protect release tags β€” the ONE net-new, actually-managed ruleset in deploy/rulesets/.
#
# The org ships every product by tagging a `v*` release (each repo's cd.yaml triggers
# on `v*`), yet had NO tag ruleset β€” release tags could be deleted or force-moved.
# This `OrganizationRuleset` (target: tag, ~ALL repos, ~ALL tags) makes release tags
# immutable and well-formed:
# - deletion: block deleting a tag
# - nonFastForward: block force-updating (re-pointing) a tag
# - tagNamePattern: require `v<semver>` (e.g. v1.2.3, v1.2.3-rc.1)
# Creating a NEW v-semver tag is still allowed (deletion/nonFastForward only restrict
# delete/move) so normal releases are unaffected; only tampering is blocked.
#
# Unlike everything else in this directory this is NOT an import β€” it has no
# external-name and is managed (Observe + Create + Update + LateInitialize, never
# Delete), so Crossplane CREATES it on first reconcile and then keeps it in sync.
#
# No bypassActors on purpose: clean immutability, consistent with the org's other
# protective rulesets ("Block force pushes", "Restrict branch names", which also have
# none), and it avoids the OrganizationAdmin bypass actor_id quirk. An org admin can
# still edit/disable the ruleset itself. If release automation ever needs to move or
# delete a tag, add a bypassActors entry here (declaratively) rather than in the UI.
#
# TIER CAVEAT (tagNamePattern): provider/GitHub docs historically flag the tag name
# pattern rule as "enterprise only". devantler-tech is on the `team` plan, NOT an
# enterprise β€” BUT this same org already runs ACTIVE org-level branch_name_pattern
# ("Restrict branch names") and commit_message_pattern ("Restrict commit metadata")
# rulesets, so pattern rules demonstrably work here and tagNamePattern is expected to
# as well. FALLBACK: if GitHub rejects this ruleset on apply because of the tag name
# pattern, delete just the `tagNamePattern` block below β€” `deletion` + `nonFastForward`
# still deliver immutable release tags and have no tier requirement.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: protect-release-tags
spec:
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: Protect release tags
target: tag
enforcement: active
conditions:
- refName:
- include: ["~ALL"]
exclude: []
repositoryName:
- include: ["~ALL"]
exclude: []
rules:
- deletion: true
nonFastForward: true
tagNamePattern:
- operator: regex
pattern: '^v\d+\.\d+\.\d+(-.+)?$'
negate: false
name: "Tags must be v<semver> (e.g. v1.2.3 or v1.2.3-rc.1)"
providerConfigRef:
kind: ProviderConfig
name: default
18 changes: 18 additions & 0 deletions deploy/organization-rulesets/require-conversation-resolution.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: require-conversation-resolution
annotations:
crossplane.io/external-name: "6986190" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Require conversation resolution before merging
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
18 changes: 18 additions & 0 deletions deploy/organization-rulesets/require-linear-history.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: require-linear-history
annotations:
crossplane.io/external-name: "6986179" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Require linear history
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
18 changes: 18 additions & 0 deletions deploy/organization-rulesets/require-pull-request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: require-pull-request
annotations:
crossplane.io/external-name: "4134020" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Require a pull request before merging
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
18 changes: 18 additions & 0 deletions deploy/organization-rulesets/require-signed-commits.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: require-signed-commits
annotations:
crossplane.io/external-name: "5397812" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Require signed commits
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
18 changes: 18 additions & 0 deletions deploy/organization-rulesets/require-status-checks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: require-status-checks
annotations:
crossplane.io/external-name: "14974342" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Require status checks to pass
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
#
# Newer "Require workflows to pass" rule (API type `workflows`, exposed by the
# provider as `requiredWorkflows`). Only this DependencyReview variant is
# importable β€” it conditions on repository_name `~ALL`. The other "Require
# workflows …" rulesets condition on repository_property and stay UI-managed.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: require-workflows-dependency-review
annotations:
crossplane.io/external-name: "17213449" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Require workflows to pass before merging - DependencyReview
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
18 changes: 18 additions & 0 deletions deploy/organization-rulesets/restrict-branch-names.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: restrict-branch-names
annotations:
crossplane.io/external-name: "4133993" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Restrict branch names
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
18 changes: 18 additions & 0 deletions deploy/organization-rulesets/restrict-commit-metadata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: restrict-commit-metadata
annotations:
crossplane.io/external-name: "4134015" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Restrict commit metadata
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
18 changes: 18 additions & 0 deletions deploy/organization-rulesets/restrict-deletions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Existing org ruleset, adopted Observe-first (read-only). Identity-only
# forProvider β€” don't promote past Observe without backfilling from
# status.atProvider (lossy round-trip). See ./README.md for the convention.
apiVersion: enterprise.github.m.upbound.io/v1alpha1
kind: OrganizationRuleset
metadata:
name: restrict-deletions
annotations:
crossplane.io/external-name: "6986171" # numeric ruleset id
spec:
managementPolicies: ["Observe"]
forProvider:
name: Restrict deletions
target: branch
enforcement: active
providerConfigRef:
kind: ProviderConfig
name: default
Loading