Skip to content

fix(repositories): stop declaring org-enforced signoff so updates can apply - #128

Merged
devantler merged 2 commits into
mainfrom
claude/repository-signoff-omit-field-112
Jul 27, 2026
Merged

fix(repositories): stop declaring org-enforced signoff so updates can apply#128
devantler merged 2 commits into
mainfrom
claude/repository-signoff-omit-field-112

Conversation

@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Why

Our declarative GitHub configuration has not been able to change any repository setting since it
was set up. Every update is rejected by GitHub, so declared settings that differ from the live repo
simply never take effect — two repos have had their declared topics sitting unapplied for weeks, and
the discovery surface they were written for has never existed.

The previous two attempts at this both narrowed the problem without fixing it. This one is based on a
direct measurement against the GitHub API rather than an assumption about how the setting behaves.

What

Stops declaring the commit-signoff setting in our configuration at all.

GitHub refuses the whole update whenever that setting is mentioned, even when we send the exact
value the repository already has
. Our organisation already enforces signoff on every repository —
including every new one — so declaring it bought us nothing and was the very thing blocking the write
path. Removing it lets everything else we declare actually apply, and loses no protection.

The existing safety check is updated to match: it now fails if the setting reappears anywhere, so
this cannot silently regress.

Fixes #112

… apply

GitHub rejects a repository PATCH with 422 whenever
web_commit_signoff_required appears in it while the org enforces signoff --
measured directly against the API, sending the field at its own current value
of true still fails. The Terraform provider omits the field only while it is
left unconfigured, and upjet builds that configuration from forProvider and
initProvider alike, so the create-only seed kept the field in every update
payload and the write path stayed blocked.

Nothing is lost by omitting it: the org setting is what applies signoff to a
new repository, and it is the same setting that makes the field unwritable.

Fixes #112

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Verification record

Root cause, measured (not inferred). Two no-op PATCHes against agent-plugins, each sending a
field at the value it already holds:

Probe Payload Result
Control has_issues=true (already true) 200 OK
Signoff web_commit_signoff_required=true (already true) 422

So the trigger is the field's presence, and the "cannot be disabled" wording is misleading — nothing
was being disabled. This is what falsifies #127's premise: initProvider is not create-only in effect,
because upjet feeds both blocks into the Terraform configuration.

RED/GREEN.

  • Control run on the unmodified branch: GREEN (19 active repositories) — proves the harness reproduces.
  • Old test against the new manifest: RED, listing all 19 repositories — proves the test binds to the
    manifest rather than passing incidentally.
  • New test, new manifest: GREEN.
  • Both branches of the new assertion RED-probed independently: re-declaring in initProvider → fail;
    declaring in forProvider → fail. Neither branch is vacuous.
  • Rendered output contains zero occurrences of the field.

Redundancy of the removed seed. Every repository in this org already carries
web_commit_signoff_required: true live: doggy-countdown (created 07-26), aws, kyverno-policies,
provider-upjet-unifi. The org setting applies it — the same setting that makes the field unwritable.

Open verification gap — stated plainly

The deployed provider embeds terraform-provider-github v6.6.0 (read from the running provider
binary's Go build info). Upstream fixed this by only sending the field when it is explicitly
configured, but I could not confirm from public non-repository documentation which v6 minor first
carried that fix, so I cannot prove pre-merge that v6.6.0 includes it.

I attempted to settle it by temporarily removing the field from the live managed resource and watching
the reconcile; that action was blocked by this runtime's permission layer, and I did not work around it.

This does not change whether the PR is correct — the removed declaration is provably redundant and is
demonstrably in the failing payload today — but it does mean the fix's efficacy is confirmed after
merge, not before
. The reconcile retries continuously, so the check is fast:

  • agent-plugins and agent-skills reach Synced=True
  • their declared topics appear on the GitHub API (not atProvider, which is not a drift oracle)

If they do not, the field is unconditionally sent by v6.6.0 and #112 is upstream-blocked rather than
config-blocked — which is itself the next thing worth knowing, and I will report it on the issue either way.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

@devantler: I’ll review the changes and verification coverage.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@devantler, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f76880ee-6bba-4a4b-88ee-d46b4b17c7eb

📥 Commits

Reviewing files that changed from the base of the PR and between 0c1f555 and 45d7e23.

📒 Files selected for processing (6)
  • deploy/archived-repositories/kustomization.yaml
  • deploy/repositories/agent-plugins.yaml
  • deploy/repositories/agent-skills.yaml
  • deploy/repositories/aws.yaml
  • deploy/repositories/kyverno-policies.yaml
  • deploy/repositories/provider-upjet-unifi.yaml
📝 Walkthrough

Walkthrough

Repository guidance now states that organization-enforced fields must be absent from both forProvider and initProvider. Repository patches remove the webCommitSignoffRequired initialization and pin hasDownloads to false. The update-policy test rejects active repositories declaring signoff in either provider block and updates its success message.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: removing org-enforced signoff declarations so repo updates can proceed.
Description check ✅ Passed The description matches the changeset and explains the signoff-removal fix plus the updated safety check.
Linked Issues check ✅ Passed The PR removes webCommitSignoffRequired from config and updates the guard test, matching issue #112's core fix.
Out of Scope Changes check ✅ Passed No clearly unrelated changes stand out; the comment and test updates support the signoff-removal objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@deploy/repositories/kustomization.yaml`:
- Around line 39-47: Update the comment in agent-plugins.yaml near the
webCommitSignoffRequired guidance to state that the field is declared nowhere in
the repository configuration and signoff is inherited from the organization.
Remove the outdated reference to the shared patch while preserving the
surrounding configuration guidance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1c04ffb7-ff83-41e4-b7aa-5aae96b825c5

📥 Commits

Reviewing files that changed from the base of the PR and between 993d404 and 0c1f555.

📒 Files selected for processing (3)
  • AGENTS.md
  • deploy/repositories/kustomization.yaml
  • tests/repository-update-policy.sh
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
AGENTS.md

📄 CodeRabbit inference engine (CLAUDE.md)

Follow the instructions defined in AGENTS.md.

Keep this repository's AGENTS.md synchronized with the actual deploy/ layout and shared engineering contract; do not maintain a redundant .github/copilot-instructions.md file.

Files:

  • AGENTS.md
**/AGENTS.md

📄 CodeRabbit inference engine (GEMINI.md)

Follow the coding guidelines and instructions defined in AGENTS.md.

Files:

  • AGENTS.md
tests/*.sh

📄 CodeRabbit inference engine (AGENTS.md)

Before every pull request, run tests/admin-team-policy.sh, tests/declarative-coverage.sh, and tests/repository-update-policy.sh; all must pass.

Files:

  • tests/repository-update-policy.sh
deploy/{repositories,labels,teams}/**/*.yaml

📄 CodeRabbit inference engine (AGENTS.md)

When adopting an existing repository, issue-label set, or team, set crossplane.io/external-name to the live name and use a management policy that excludes Delete to observe and late-initialize without recreate or deletion risk.

Files:

  • deploy/repositories/kustomization.yaml
deploy/repositories/**/*.yaml

📄 CodeRabbit inference engine (AGENTS.md)

After adoption, active Repository resources must use Observe/Create/Update without LateInitialize; do not declare organization-enforced fields such as webCommitSignoffRequired in either forProvider or initProvider.

Files:

  • deploy/repositories/kustomization.yaml
deploy/**/*.yaml

📄 CodeRabbit inference engine (AGENTS.md)

deploy/**/*.yaml: Verify every new Crossplane resource kind and field against the authoritative provider CRDs and generated examples; local schema validation is unavailable, so do not rely solely on local kustomize validation.
Preserve the documented deployment layout: repositories, teams, team memberships, team-repository grants, labels, provider configuration, external secret, and top-level kustomization belong in their corresponding paths.
Before every pull request, ensure the rendered deployment manifests build cleanly with kubectl kustomize deploy/.

Files:

  • deploy/repositories/kustomization.yaml
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: devantler-tech/.github

Timestamp: 2026-07-27T14:26:44.160Z
Learning: Manage GitHub organization, repository, team, and label configuration declaratively by editing `deploy/` and shipping a PR; never write managed configuration through the GitHub UI or `gh api`. Reading via `gh api` is allowed, and applying an existing label is triage rather than configuration.
Learnt from: CR
Repo: devantler-tech/.github

Timestamp: 2026-07-27T14:26:44.160Z
Learning: Use GitHub Issues as the roadmap of record, triage incoming issues into the roadmap/epic structure, and have implementation pull requests use `Fixes `#N``.
📚 Learning: 2026-07-10T14:42:56.622Z
Learnt from: devantler
Repo: devantler-tech/.github PR: 90
File: deploy/archived-repositories/kustomization.yaml:11-16
Timestamp: 2026-07-10T14:42:56.622Z
Learning: In this repo’s deployment setup, `deploy/` kustomize manifests are rendered unconditionally (no feature-flag/conditional-render/apply primitive) and Flux applies from `main`. For irreversible or ordering-sensitive rollouts in `deploy/` (e.g., changes under `deploy/archived-repositories/` that require prerequisites handled in separate PRs), reviewers should not expect a mechanical CI guard. Instead, require the PR to remain in draft until the required prerequisite one-click actions (e.g., org `required-workflow` ruleset repointing) are completed, with the required rollout ordering explicitly documented in the PR body (per the documented bold line). Only then should the PR be promoted/merged.

Applied to files:

  • deploy/repositories/kustomization.yaml
📚 Learning: 2026-07-27T11:40:06.755Z
Learnt from: devantler
Repo: devantler-tech/.github PR: 127
File: deploy/repositories/agent-plugins.yaml:14-15
Timestamp: 2026-07-27T11:40:06.755Z
Learning: When reviewing Crossplane GitHub `Repository` resources under `deploy/repositories/*.yaml`, note that removing `LateInitialize` prevents newly observed/undeclared fields from being copied into `spec.forProvider`. As a result, those newly added settings become unmanaged until explicitly declared. However, fields that were late-initialized before this policy change may already exist in `spec.forProvider`; they are then owned by the provider field manager (not Flux `kustomize-controller`) and may remain in provider update payloads. Require comments to clearly distinguish these two cases so reviewers know whether a field is newly unmanaged vs. pre-existing provider-owned data.

Applied to files:

  • deploy/repositories/kustomization.yaml
🔇 Additional comments (5)
AGENTS.md (1)

47-50: 🗄️ Data Integrity & Integration

Verify the deployed provider behavior before relying on this rule.

The fix depends on terraform-provider-github v6.6.0 omitting webCommitSignoffRequired when both provider blocks are absent, but the PR context says this is still unconfirmed. Validate the installed provider’s update path and one real repository update; otherwise existing 422 loops may persist despite clean manifests.

deploy/repositories/kustomization.yaml (2)

58-80: LGTM!


81-83: 🗄️ Data Integrity & Integration

Verify hasDownloads against the installed Repository CRD.

This adds spec.forProvider.hasDownloads to every rendered Repository. As per coding guidelines, verify the field in the authoritative provider CRD and generated namespaced examples; local kubectl kustomize rendering is insufficient.

Source: Coding guidelines

tests/repository-update-policy.sh (2)

61-83: 🗄️ Data Integrity & Integration

Check live CR state in addition to rendered manifests.

This assertion proves only that Kustomize output no longer declares the key. A field previously late-initialized into live spec.forProvider can remain provider-owned after manifest removal and still be included in update payloads, so existing 422 failures may continue. Verify that the reconciled live resources no longer contain the field before treating this test as proof of recovery.

Based on learnings, previously provider-owned fields can remain in live spec.forProvider after a manifest stops declaring them.

Source: Learnings


85-85: LGTM!

Comment thread deploy/repositories/kustomization.yaml
Five repository files and the archived-repositories kustomization still
described webCommitSignoffRequired as supplied by the shared patch. That patch
no longer sets it, so the guidance would have invited the rejected field back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

@devantler: Reviewing the latest changes and verification coverage now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Readiness

Pentad at 45d7e23c: CI green · 0 unresolved threads · 0 non-thread review findings (newest
CodeRabbit review at head carries no finding sections) · MERGEABLE/CLEAN · green review
cr@45d7e23c.

Blast-radius check before merging a write-path repair. Restoring the write path makes every
declared-vs-live difference enactable, not just the topics this issue is about — so I checked what the
config becomes free to do:

  • The two repositories that are declared private but live publicascoachingogvaner and
    wedding-app — are on Observe only. They cannot write, so this change cannot unpublish them.
    That parking is what Repository reconciliation is fully broken: every PATCH 422s on org-enforced commit signoff #123 put in place and it still holds.
  • world-at-ruin is also Observe only.
  • For all 17 writable repositories, declared visibility now equals live visibility — including
    fleet-gitops (private/private). No repository flips.

So the only pending writes this unblocks are the intended ones: the declared topics on agent-plugins
and agent-skills.

User evaluation. These manifests have no locally exercisable runtime surface — they are consumed by
Flux and Crossplane in the cluster — so per the contract's carve-out I state that explicitly rather than
claiming a run I did not perform. What I did exercise:

  • rendered the full kustomization and confirmed zero occurrences of the field reach any spec
  • ran the contract test through control → RED → GREEN, RED-probed both branches of the new
    assertion, and added a negative control proving an unrelated initProvider key does not trip it
  • measured the underlying API behaviour directly with the paired no-op PATCH probe
  • verified every changed YAML file parses

The one thing I could not settle pre-merge is whether terraform-provider-github v6.6.0 carries the
upstream "only set when explicitly configured" change. That is stated in the verification record above
and is the first thing checked after deploy.

Promoting on that basis.

@devantler
devantler marked this pull request as ready for review July 27, 2026 14:42
@devantler
devantler merged commit af5578f into main Jul 27, 2026
11 checks passed
@devantler
devantler deleted the claude/repository-signoff-omit-field-112 branch July 27, 2026 14:43
devantler added a commit that referenced this pull request Aug 6, 2026
Seven of the twenty Repository resources have been failing every update with
422 "Commit signoff is enforced by the organization and cannot be disabled",
so declared repository configuration has not reached GitHub for them.

The accepted explanation was that the field is rejected whenever it appears in
an update, so #128 removed the declaration entirely. The live cluster shows the
opposite. upjet builds the Terraform configuration from forProvider; an absent
optional bool takes the provider's zero value of false; false against a live
true is a permanent diff, so the payload carries
web_commit_signoff_required: false and GitHub rejects it. The error names
disabling, not presence. Declaring the live value leaves nothing to diff, so
Terraform omits the field from the payload and the update applies.

Measured, at two-minute resolution:

  2026-07-27T04:33Z  #125 merges, shared patch still declares the field
  2026-07-27T04:34:5x  nine write-enabled repos record LastAsyncOperation=Success
  2026-07-27T14:42Z  #128 removes the declaration
  2026-07-27T14:44:4x  the same repos begin recording AsyncUpdateFailure (422)

Ten repositories still reconcile today only because the provider had already
late-initialized the value into their spec, where Flux's removal could not
reach it. The seven that never accumulated that residue are exactly the seven
failing, and six of them differ from a working repository by this one field.
So the write path currently depends on undeclared residue rather than on
anything in this repository.

platform-tenant-template is the one active resource holding no adopted values,
so it cannot fall back on a previously observed visibility once updates apply
again; its live value is pinned explicitly.

tests/repository-update-policy.sh now pins the inverted invariant.

Fixes #112

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
devantler added a commit that referenced this pull request Aug 6, 2026
…136)

* chore: claim #123

* fix(repositories): declare org-enforced signoff so updates can apply

Seven of the twenty Repository resources have been failing every update with
422 "Commit signoff is enforced by the organization and cannot be disabled",
so declared repository configuration has not reached GitHub for them.

The accepted explanation was that the field is rejected whenever it appears in
an update, so #128 removed the declaration entirely. The live cluster shows the
opposite. upjet builds the Terraform configuration from forProvider; an absent
optional bool takes the provider's zero value of false; false against a live
true is a permanent diff, so the payload carries
web_commit_signoff_required: false and GitHub rejects it. The error names
disabling, not presence. Declaring the live value leaves nothing to diff, so
Terraform omits the field from the payload and the update applies.

Measured, at two-minute resolution:

  2026-07-27T04:33Z  #125 merges, shared patch still declares the field
  2026-07-27T04:34:5x  nine write-enabled repos record LastAsyncOperation=Success
  2026-07-27T14:42Z  #128 removes the declaration
  2026-07-27T14:44:4x  the same repos begin recording AsyncUpdateFailure (422)

Ten repositories still reconcile today only because the provider had already
late-initialized the value into their spec, where Flux's removal could not
reach it. The seven that never accumulated that residue are exactly the seven
failing, and six of them differ from a working repository by this one field.
So the write path currently depends on undeclared residue rather than on
anything in this repository.

platform-tenant-template is the one active resource holding no adopted values,
so it cannot fall back on a previously observed visibility once updates apply
again; its live value is pinned explicitly.

tests/repository-update-policy.sh now pins the inverted invariant.

Fixes #112

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repositories): point signoff rationale at the tracking issue

* docs(agents): bound LateInitialize residue to the adoption phase

CodeRabbit: the previous wording let 'late-initialized values land in
forProvider' read as an ongoing behaviour. Once LateInitialize is removed no
newly observed field is copied in again, which is precisely why a resource
adopted without a field can never acquire it and the config must supply it.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
devantler added a commit that referenced this pull request Aug 6, 2026
…epos (#138)

* chore: claim #123

* fix(repositories): declare org-enforced signoff so updates can apply

Seven of the twenty Repository resources have been failing every update with
422 "Commit signoff is enforced by the organization and cannot be disabled",
so declared repository configuration has not reached GitHub for them.

The accepted explanation was that the field is rejected whenever it appears in
an update, so #128 removed the declaration entirely. The live cluster shows the
opposite. upjet builds the Terraform configuration from forProvider; an absent
optional bool takes the provider's zero value of false; false against a live
true is a permanent diff, so the payload carries
web_commit_signoff_required: false and GitHub rejects it. The error names
disabling, not presence. Declaring the live value leaves nothing to diff, so
Terraform omits the field from the payload and the update applies.

Measured, at two-minute resolution:

  2026-07-27T04:33Z  #125 merges, shared patch still declares the field
  2026-07-27T04:34:5x  nine write-enabled repos record LastAsyncOperation=Success
  2026-07-27T14:42Z  #128 removes the declaration
  2026-07-27T14:44:4x  the same repos begin recording AsyncUpdateFailure (422)

Ten repositories still reconcile today only because the provider had already
late-initialized the value into their spec, where Flux's removal could not
reach it. The seven that never accumulated that residue are exactly the seven
failing, and six of them differ from a working repository by this one field.
So the write path currently depends on undeclared residue rather than on
anything in this repository.

platform-tenant-template is the one active resource holding no adopted values,
so it cannot fall back on a previously observed visibility once updates apply
again; its live value is pinned explicitly.

tests/repository-update-policy.sh now pins the inverted invariant.

Fixes #112

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repositories): point signoff rationale at the tracking issue

* docs(agents): bound LateInitialize residue to the adoption phase

CodeRabbit: the previous wording let 'late-initialized values land in
forProvider' read as an ongoing behaviour. Once LateInitialize is removed no
newly observed field is copied in again, which is precisely why a resource
adopted without a field can never acquire it and the config must supply it.

* fix(repositories): correct stale private declarations before restoring writes

ascoachingogvaner and wedding-app are public and serve live sites, but both
declared visibility: private — carried in from the template they were written
against, never true of either repository. While updates were write-blocked the
divergence was inert; restoring the write path is what would make it act, so the
declaration is corrected first and the two stay Observe-only until promoted
separately.

The kustomization grouped them under '# Private repos.', which described neither
their visibility nor their management policy. fleet-gitops, which is genuinely
private and fully managed, moves in with the actively-managed set.

Part of #123

* docs(repositories): state the current management rationale, not the migration

The two observe-only declarations described the change this PR makes rather
than the state it leaves behind. Both now say why write management is still
disabled and where enabling it is tracked.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant