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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
- name: 🧪 Test declarative coverage
run: bash tests/declarative-coverage.sh

- name: 🧪 Test repository update policy
run: bash tests/repository-update-policy.sh

ci-required-checks:
name: CI - Required Checks
runs-on: ubuntu-latest
Expand Down
10 changes: 8 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ for the architecture, the GitHub App credential setup, and the Observe-first ado
- **Observe-first when adopting an existing resource.** A new `Repository`/`IssueLabels`/team CR for
an already-live object must adopt it without risk of recreate/delete: set the
`crossplane.io/external-name` annotation to the live name and use a management policy that
**excludes `Delete`** (observe/late-initialize), per platform's `docs/github-management.md`. Verify
**excludes `Delete`** (observe/late-initialize), per platform's `docs/github-management.md`. Once
adopted, an active `Repository` runs on `Observe`/`Create`/`Update` **without `LateInitialize`**:
late-initialized values land in `forProvider`, and everything in `forProvider` is sent on every
subsequent update PATCH. An org-enforced create default such as `webCommitSignoffRequired` belongs
in `initProvider`, which Crossplane applies only at creation — GitHub rejects the whole PATCH with
422 whenever that field appears in an update. Verify
the provider kind/field schema against the authoritative source
([crossplane-contrib/provider-upjet-github `package/crds/`](https://github.com/crossplane-contrib/provider-upjet-github)
+ `examples-generated/namespaced/`) — the CRs cannot be schema-validated locally (no cluster; CI
Expand Down Expand Up @@ -79,9 +84,10 @@ structure; implementing PRs use `Fixes #N`.
kubectl kustomize deploy/ > /dev/null # must build clean
bash tests/admin-team-policy.sh # Admins policy invariants
bash tests/declarative-coverage.sh # every repo declared in every rendered dimension
bash tests/repository-update-policy.sh # active Repository update invariants
```

Those three commands are exactly what `ci.yaml` runs.
Those four commands are exactly what `ci.yaml` runs.

`kubectl` (with built-in kustomize) is preinstalled on CI runners. A clean build proves the manifests
are well-formed; the Crossplane CRDs themselves are applied/validated **on-cluster** (the
Expand Down
5 changes: 2 additions & 3 deletions deploy/repositories/agent-plugins.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ metadata:
spec:
# Squash-only merge policy + webCommitSignoffRequired come from the shared
# patch; description and topics are declared here so the config is
# AUTHORITATIVE for them (description was previously LateInitialized, topics
# were unset); everything else is adopted via LateInitialize.
# AUTHORITATIVE for them. Settings declared nowhere here keep the value adopted
# from the live repo when it was first observed.
Comment thread
devantler marked this conversation as resolved.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: agent-plugins
# Fixes a stale repo reference: `devantler-tech/skills` was renamed to
Expand Down
5 changes: 2 additions & 3 deletions deploy/repositories/agent-skills.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ metadata:
spec:
# Squash-only merge policy + webCommitSignoffRequired come from the shared
# patch; description and topics are declared here so the config is
# AUTHORITATIVE for them (description was previously LateInitialized, topics
# were unset); everything else is adopted via LateInitialize.
# AUTHORITATIVE for them. Settings declared nowhere here keep the value adopted
# from the live repo when it was first observed.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: agent-skills
# De-Copilot-branded: these skills are agent-neutral (agentskills.io spec)
Expand Down
15 changes: 7 additions & 8 deletions deploy/repositories/ascoachingogvaner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ metadata:
annotations:
crossplane.io/external-name: ascoachingogvaner
spec:
# Actively managed (everything except Delete). Merge policy from the shared
# patch. PRIVATE repo: visibility is pinned private explicitly (never left to
# inference) so management can never expose it, and archived false so it can't
# be archived. description is declared so the config is AUTHORITATIVE for it
# (previously LateInitialized); LateInitialize adopts every other setting.
# Observe-only: this file declares `visibility: private` while the live
# repository is public and serving a site, so acting on the declaration would
# take the site off the public internet. Which side is wrong is the
# maintainer's call, tracked on devantler-tech/.github#123 — until it is made,
# Crossplane mirrors this repo read-only and writes nothing. Restoring
# Create/Update is what enacts the decision, once `visibility` below has been
# set to the answer.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: ascoachingogvaner
visibility: private
Expand Down
1 change: 0 additions & 1 deletion deploy/repositories/aws.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ spec:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: aws
description: "Declarative AWS infrastructure for the devantler-tech platform — Crossplane managed resources in deploy/, published as a cosign-signed OCI artifact and reconciled by the platform's aws tenant."
Expand Down
7 changes: 4 additions & 3 deletions deploy/repositories/doggy-countdown.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ metadata:
annotations:
crossplane.io/external-name: doggy-countdown
spec:
# Actively managed (everything except Delete). Merge policy from the shared
# Managed (Observe/Create/Update). Merge policy from the shared
# patch. PUBLIC repo: visibility is pinned public explicitly (never left to
# inference) so a reconcile can never take the published site private, and
# archived false so it can't be archived. description is declared so the
# config is AUTHORITATIVE for it; LateInitialize adopts every other setting.
# config is AUTHORITATIVE for it. Settings in neither
# are unmanaged: nothing new is copied into the spec, and values adopted
# earlier stay.
Comment on lines +12 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify the legacy late-initialized-field behavior.

“Values adopted earlier stay” obscures the material distinction: newly observed undeclared fields are unmanaged, while fields late-initialized before this change may already remain in spec.forProvider, be provider-owned, and still reach update payloads.

  • deploy/repositories/doggy-countdown.yaml#L12-L14: state the legacy provider-owned-field behavior explicitly.
  • deploy/repositories/dotnet-template.yaml#L11-L13: state the legacy provider-owned-field behavior explicitly.
  • deploy/repositories/fleet-gitops.yaml#L12-L14: state the legacy provider-owned-field behavior explicitly.
  • deploy/repositories/go-template.yaml#L11-L13: state the legacy provider-owned-field behavior explicitly.
  • deploy/repositories/homebrew-tap.yaml#L10-L12: state the legacy provider-owned-field behavior explicitly.
  • deploy/repositories/platform-template.yaml#L11-L13: state the legacy provider-owned-field behavior explicitly.
  • deploy/repositories/platform.yaml#L14-L16: state the legacy provider-owned-field behavior explicitly.
  • deploy/repositories/unifi.yaml#L11-L13: state the legacy provider-owned-field behavior explicitly.

Based on learnings: removal of LateInitialize affects newly observed fields differently from previously provider-owned values.

📍 Affects 8 files
  • deploy/repositories/doggy-countdown.yaml#L12-L14 (this comment)
  • deploy/repositories/dotnet-template.yaml#L11-L13
  • deploy/repositories/fleet-gitops.yaml#L12-L14
  • deploy/repositories/go-template.yaml#L11-L13
  • deploy/repositories/homebrew-tap.yaml#L10-L12
  • deploy/repositories/platform-template.yaml#L11-L13
  • deploy/repositories/platform.yaml#L14-L16
  • deploy/repositories/unifi.yaml#L11-L13
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/repositories/doggy-countdown.yaml` around lines 12 - 14, Clarify the
comments describing post-LateInitialize behavior: newly observed undeclared
fields are unmanaged, while fields late-initialized before this change may
remain in spec.forProvider as provider-owned values and continue reaching update
payloads. Apply this explicit legacy-behavior wording at
deploy/repositories/doggy-countdown.yaml lines 12-14,
deploy/repositories/dotnet-template.yaml lines 11-13,
deploy/repositories/fleet-gitops.yaml lines 12-14,
deploy/repositories/go-template.yaml lines 11-13,
deploy/repositories/homebrew-tap.yaml lines 10-12,
deploy/repositories/platform-template.yaml lines 11-13,
deploy/repositories/platform.yaml lines 14-16, and
deploy/repositories/unifi.yaml lines 11-13.

Source: Learnings

managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: doggy-countdown
visibility: public
Expand Down
12 changes: 6 additions & 6 deletions deploy/repositories/dotnet-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ metadata:
annotations:
crossplane.io/external-name: dotnet-template
spec:
# Managed (all except Delete). forProvider now declares the discoverability
# metadata (description/topics) so the config is AUTHORITATIVE for it
# (previously LateInitialized from live); the org-wide squash-only merge policy
# still comes from the shared patch in kustomization.yaml, and every other
# setting is still adopted unchanged from the live repo via LateInitialize.
# Managed (Observe/Create/Update). forProvider declares the discoverability
# metadata (description/topics), so the config is AUTHORITATIVE for it; the
# org-wide squash-only merge policy comes from the shared patch in
# kustomization.yaml. Settings in neither
# are unmanaged: nothing new is copied into the spec, and values adopted
# earlier stay.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: dotnet-template
description: "A simple .NET template for new projects."
Expand Down
9 changes: 5 additions & 4 deletions deploy/repositories/fleet-gitops.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ metadata:
annotations:
crossplane.io/external-name: fleet-gitops
spec:
# Actively managed (everything except Delete). Merge policy from the shared
# Managed (Observe/Create/Update). Merge policy from the shared
# patch. PRIVATE repo: visibility is pinned private explicitly (never left to
# inference) so management can never expose it, and archived false so it can't
# be archived. description is declared so the config is AUTHORITATIVE for it
# (previously LateInitialized); LateInitialize adopts every other setting.
# be archived. description is declared so the config is AUTHORITATIVE for it.
# Settings in neither
# are unmanaged: nothing new is copied into the spec, and values adopted
# earlier stay.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: fleet-gitops
visibility: private
Expand Down
12 changes: 6 additions & 6 deletions deploy/repositories/gitops-tenant-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ metadata:
annotations:
crossplane.io/external-name: gitops-tenant-template
spec:
# Managed (all except Delete). forProvider now declares the discoverability
# metadata (description/topics) so the config is AUTHORITATIVE for it
# (previously LateInitialized from live); the org-wide squash-only merge policy
# still comes from the shared patch in kustomization.yaml, and every other
# setting is still adopted unchanged from the live repo via LateInitialize.
# Managed (Observe/Create/Update). forProvider declares the discoverability
# metadata (description/topics), so the config is AUTHORITATIVE for it; the
# org-wide squash-only merge policy comes from the shared patch in
# kustomization.yaml. Settings in neither
# are unmanaged: nothing new is copied into the spec, and values adopted
# earlier stay.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: gitops-tenant-template
description: "Template for GitOps tenants on the devantler-tech platform — framework-agnostic CI/CD plumbing kept current via template-sync."
Expand Down
12 changes: 6 additions & 6 deletions deploy/repositories/go-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ metadata:
annotations:
crossplane.io/external-name: go-template
spec:
# Managed (all except Delete). forProvider now declares the discoverability
# metadata (description/topics) so the config is AUTHORITATIVE for it
# (previously LateInitialized from live); the org-wide squash-only merge policy
# still comes from the shared patch in kustomization.yaml, and every other
# setting is still adopted unchanged from the live repo via LateInitialize.
# Managed (Observe/Create/Update). forProvider declares the discoverability
# metadata (description/topics), so the config is AUTHORITATIVE for it; the
# org-wide squash-only merge policy comes from the shared patch in
# kustomization.yaml. Settings in neither
# are unmanaged: nothing new is copied into the spec, and values adopted
# earlier stay.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: go-template
description: "A simple Go template for new projects."
Expand Down
8 changes: 4 additions & 4 deletions deploy/repositories/homebrew-tap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ metadata:
annotations:
crossplane.io/external-name: homebrew-tap
spec:
# Managed (all except Delete). forProvider now declares the discoverability
# Managed (Observe/Create/Update). forProvider now declares the discoverability
# metadata so the config is AUTHORITATIVE for it; the org-wide squash-only
# merge policy still comes from the shared patch in kustomization.yaml, and
# every other setting is still adopted unchanged via LateInitialize.
# merge policy comes from the shared patch in kustomization.yaml. Settings in neither
# are unmanaged: nothing new is copied into the spec, and values adopted
# earlier stay.
# NB: this is the one repo whose live description was empty — declaring it here
# is a real (intentional) Update, not a no-op cutover like the other repos.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: homebrew-tap
description: "Homebrew tap for devantler-tech CLIs (e.g. ksail)."
Expand Down
6 changes: 3 additions & 3 deletions deploy/repositories/ksail.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ metadata:
# (owner comes from the ProviderConfig credentials).
crossplane.io/external-name: ksail
spec:
# Actively managed (everything except Delete). Merge policy from the shared
# Managed (Observe/Create/Update). Merge policy from the shared
# patch; visibility pinned public + archived false so management can never
# flip visibility or archive this repo. Discoverability metadata
# (description/topics/homepageUrl) is declared so the config is AUTHORITATIVE
# for it (previously LateInitialized from live); LateInitialize adopts the rest.
# for it. Settings declared nowhere here keep the value adopted from the live
# repo when it was first observed.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: ksail
visibility: public
Expand Down
58 changes: 37 additions & 21 deletions deploy/repositories/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# One file per managed repository. Adoption is Observe-first: a repo starts with
# managementPolicies: ["Observe"] (read-only — Crossplane mirrors live state into
# status.atProvider and never writes), then forProvider is set and the policy
# promoted to the full set EXCEPT Delete. Namespaced managed resources have no
# deletionPolicy; omitting Delete is what guarantees Crossplane can never delete
# a real GitHub repository. See devantler-tech/platform docs/github-management.md.
# promoted to Observe/Create/Update by the shared patch below. Namespaced managed
# resources have no deletionPolicy; omitting Delete is what guarantees Crossplane
# can never delete a real GitHub repository. LateInitialize is omitted too: it
# copies live-only values back into forProvider, and every field that lands there
# is sent on every subsequent update PATCH. See devantler-tech/platform
# docs/github-management.md.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
# Actively managed (managementPolicies all-except-Delete). Every non-archived
# org repo; the shared merge-policy patch below applies to all of them.
# Actively managed (Observe/Create/Update). Every non-archived org repo; the
# shared policy patch below applies to all of them.
- platform.yaml
- ksail.yaml
- maintenance.yaml
Expand All @@ -33,21 +36,21 @@ resources:
# (auto-applies to every future repo):
# - squash-only merge policy (merge commits + rebase disabled; auto-merge,
# auto-delete head branches, always-suggest-updating-PR-branches enabled).
# - webCommitSignoffRequired: true — the org ENFORCES commit signoff and it
# can't be disabled per-repo, so the provider must always send true (else an
# update PATCH gets 422 "Commit signoff is enforced ... and cannot be
# disabled"). The 12 already-adopted repos had this true via LateInitialize;
# pinning it here makes every repo's update self-consistent and robust.
# - webCommitSignoffRequired: true, in initProvider — CREATE-ONLY. The org
# enforces commit signoff, and GitHub rejects the whole PATCH with 422
# "Commit signoff is enforced ... and cannot be disabled" whenever the field
# appears in a repository update, whatever value it carries. initProvider is
# applied at creation and never included in an update payload, so a new repo
# still gets the secure default while existing repos stay updatable.
# - hasDownloads: false — GitHub has removed the downloads feature and no
# longer returns the field, so status.atProvider never carries it. The
# Terraform provider still defaults it to true, so any repo whose spec picked
# that default up through LateInitialize holds a value the API can never
# report back. That is an unresolvable diff: the provider sees spec != live on
# every reconcile and issues an update PATCH forever. Those PATCHes are then
# rejected wholesale (422, on the commit-signoff field), so NO declared
# setting reaches GitHub for that repository — visibility included. Pinning
# the field to the value the API actually reflects is what stops the diff, and
# therefore what keeps the rest of this file enforceable.
# Terraform provider still defaults it to true, so a spec holding that default
# carries a value the API can never report back. That is an unresolvable diff:
# the provider sees spec != live on every reconcile and issues an update PATCH
# forever. Pinning the field to the value the API actually reflects is what
# stops the diff.
# managementPolicies stays per repo: adoption is Observe-first, and a shared
# override would promote a repo to writing the moment it is added.
patches:
- target:
group: repo.github.m.upbound.io
Expand All @@ -72,9 +75,22 @@ patches:
- op: add
path: /spec/forProvider/deleteBranchOnMerge
value: true
- op: add
path: /spec/forProvider/webCommitSignoffRequired
value: true
- op: add
path: /spec/forProvider/hasDownloads
value: false
# A merge patch, not a JSON Patch op: it creates spec.initProvider when absent
# and merges into it when a repository declares its own create-only settings.
# A JSON Patch `add` on that path would replace the whole map and silently drop
# them.
- target:
group: repo.github.m.upbound.io
version: v1alpha1
kind: Repository
patch: |-
apiVersion: repo.github.m.upbound.io/v1alpha1
kind: Repository
metadata:
name: placeholder
spec:
initProvider:
webCommitSignoffRequired: true
1 change: 0 additions & 1 deletion deploy/repositories/kyverno-policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ spec:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: kyverno-policies
description: "Shared Kyverno policy library for the devantler-tech platforms — the single source the platform and platform-template consume instead of vendoring per-repo copies."
Expand Down
6 changes: 3 additions & 3 deletions deploy/repositories/maintenance.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# First repo brought under ACTIVE management (everything except Delete). The
# First repo brought under ACTIVE management (Observe/Create/Update). The
# merge policy (squash-only + auto-merge + auto-delete branches + suggest-update)
# now comes from the shared patch in kustomization.yaml, so it is not repeated
# here. `archived` is pinned false so management can never accidentally archive
# the repo; `visibility` is intentionally left to LateInitialize (never hard-set).
# the repo; `visibility` is intentionally never declared here, so management can
# neither open nor close it.
apiVersion: repo.github.m.upbound.io/v1alpha1
kind: Repository
metadata:
Expand All @@ -14,7 +15,6 @@ spec:
- Observe
- Create
- Update
- LateInitialize
forProvider:
# Required once managementPolicies includes Create/Update (Observe-only
# didn't enforce it); must equal the external-name / repo name.
Expand Down
12 changes: 6 additions & 6 deletions deploy/repositories/platform-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ metadata:
annotations:
crossplane.io/external-name: platform-template
spec:
# Managed (all except Delete). forProvider now declares the discoverability
# metadata (description/topics) so the config is AUTHORITATIVE for it
# (previously LateInitialized from live); the org-wide squash-only merge policy
# still comes from the shared patch in kustomization.yaml, and every other
# setting is still adopted unchanged from the live repo via LateInitialize.
# Managed (Observe/Create/Update). forProvider declares the discoverability
# metadata (description/topics), so the config is AUTHORITATIVE for it; the
# org-wide squash-only merge policy comes from the shared patch in
# kustomization.yaml. Settings in neither
# are unmanaged: nothing new is copied into the spec, and values adopted
# earlier stay.
managementPolicies:
- Observe
- Create
- Update
- LateInitialize
forProvider:
name: platform-template
description: "Opinionated, batteries-included Kubernetes platform (Flux GitOps + KSail + Talos) you instantiate from a template — fully automated bootstrap from GitHub Secrets. Derived from devantler-tech/platform."
Expand Down
Loading
Loading