Skip to content

fix: declarative repo-settings updates are write-blocked (422 on every update PATCH) #112

Description

@devantler

🤖 Generated by the Daily AI Engineer

Summary

No managed repository setting can currently be changed declaratively. Every update PATCH to GitHub fails, so config merged into this repo never reaches the actual repos. This has been true the whole time but was invisible, because until 2026-07-18 nothing had ever changed a managed field — settings were only ever imported.

Evidence

#110 declared topics on agent-skills / agent-plugins. Both MRs went Synced=False ~2 minutes after merge and the topics never applied:

update failed: async update failed: failed to update the resource:
PATCH https://api.github.com/repos/devantler-tech/agent-skills: 422
Commit signoff is enforced by the organization and cannot be disabled

Every other repo MR is Synced=True — not because they work, but because they have no pending diff, so no PATCH is attempted.

Why this is the first sighting: every repo adopted its settings via LateInitialize, so Crossplane had only ever observed repo settings. ksail's 20 topics were imported, never written. #110 is the first commit in this repo's history to change a managed field on an already-adopted repo — i.e. the first real exercise of the update path.

Why the obvious fix doesn't work

The shared patch pins webCommitSignoffRequired: true, with a comment reasoning the provider "must always send true" to avoid this exact 422. That reasoning was never validated, and live state disproves it — spec, repo, all siblings, and the org are already true, and the PATCH is still rejected. The field is org-controlled, so GitHub refuses a repo-level PATCH carrying it at all; the value is irrelevant.

I attempted the naive fix in #111 (drop it from the patch). Codex correctly flagged it as a no-op (P1), confirmed live:

status.atProvider.webCommitSignoffRequired = true
spec.managementPolicies = Observe,Create,Update,LateInitialize

The field is observed and LateInitialize is on, so removing it from the patch just lets Crossplane write it back from the observed value. #111 is closed.

The real scope

For agent-skills, git declares 3 fields (name, description, topics), the shared patch adds 7, and the live spec has 21 — so 11 fields exist in spec purely via LateInitialize. Any org-enforced-but-repo-observable field will be absorbed into spec and then sent on update.

Candidate approaches (needs a deliberate decision, not a guess)

  1. Drop LateInitialize from managementPolicies. Matches "declare what you manage" and stops absorbing drift. Caveat: does not retroactively strip the 11 already-written fields — those would need removing from the live specs too, and Flux/SSA will not prune fields owned by the Crossplane field manager.
  2. Provider-side field exclusion, if provider-upjet-github supports skipping a field on late-init/update. Needs verifying against the provider's actual capabilities.
  3. Upstream fix in provider-upjet-github / terraform-provider-github so the update payload omits org-controlled fields.

Acceptance criteria

  • agent-skills and agent-plugins return to Synced=True.
  • Their declared topics and descriptions from #110 appear on GitHub.
  • A subsequent settings change on some other repo also applies — proving the update path generally, not just these two.
  • The kustomization comment is corrected; the current one states a mitigation that does not hold.

Blocks devantler-tech/monorepo#2229.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    Status
    📥 Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions