Skip to content

fix(repositories): pin hasDownloads so reconciliation stops 422-ing - #125

Merged
devantler merged 1 commit into
mainfrom
claude/github-config-hasdownloads-phantom-diff-123
Jul 27, 2026
Merged

fix(repositories): pin hasDownloads so reconciliation stops 422-ing#125
devantler merged 1 commit into
mainfrom
claude/github-config-hasdownloads-phantom-diff-123

Conversation

@devantler

@devantler devantler commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Why

Declarative GitHub org management has not been applying anything. 17 of the 20 managed repositories are stuck in a failing sync state, and every one of them simultaneously reports itself healthy — which is why this ran for weeks without surfacing. While it lasts, no declared setting reaches GitHub for those repositories, including the visibility pins that two repositories' comments describe as the thing stopping management from ever exposing them. That guarantee has not existed.

The cause is not the commit-signoff setting the error message names. That one is already declared correctly and has been since June, so the obvious fix would have changed nothing. The real trigger is a setting GitHub has retired: it is still declared as on, GitHub no longer reports it at all, and the two can therefore never agree. Reconciliation keeps trying to correct it, and each attempt is rejected in full — taking every other declared setting down with it.

What

Declares that retired setting as off, matching what GitHub actually reflects, so the mismatch disappears.

Evidence: across all 20 managed repositories, agreement on this one setting predicts sync state exactly — all 17 that disagree are failing, all 3 that agree are healthy. No other setting divides the two groups.

Confidence, stated honestly: the correlation is exact, but the mechanism is inferred rather than observed — the live mirror these resources publish is demonstrably incomplete, so it cannot be used to prove which comparison reconciliation actually acts on. This is the best-supported single explanation, and it is cheap and reversible; if it is wrong it is a no-op rather than a regression. Confirmation is that the failing repositories return to a healthy sync state after this deploys, and I will verify that and report back on the issue.

Scope is the active repository set only. The archived repository is deliberately excluded — it is the one case where this value genuinely agrees today, so pinning it there would create the very mismatch this removes.

Needs your call, not fixed here: two repositories are declared private while live public. This change cannot flip them, but it does not resolve them either — and which of the two is the mistake is a judgement about intent on two live public sites. Evidence is on the issue.

Part of #123

GitHub removed the downloads feature and no longer returns has_downloads, but
the Terraform provider still defaults it to true. Repos that adopted that
default through LateInitialize therefore hold a spec value the API can never
report back, producing an unresolvable diff that makes the provider issue an
update PATCH on every reconcile. Those PATCHes are rejected wholesale with 422
on the commit-signoff field, so no declared setting reaches GitHub at all.

Pinning the field to the value the API actually reflects removes the diff.

Measured on the live cluster: the correlation is exact across all 20 managed
Repository resources - every resource with hasDownloads true in spec and absent
in atProvider is Synced=False (17/17), and every resource where the two agree
is Synced=True (3/3).

Scope is deploy/repositories/ only. reusable-workflows lives in
archived-repositories/ and deliberately gets no shared patch; its live value is
true and already matches, so pinning it false would create the very diff this
change removes.

Signed-off-by: Nikolai Emil Damm <nikolaiemildamm@icloud.com>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

CI is green at this head and the change is self-reviewed. Requesting a current-head review.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

@devantler: Starting a current-head review, with attention to the active repository scope and the hasDownloads reconciliation rationale.

✅ 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

📝 Walkthrough

Walkthrough

Updated the repository Kustomize configuration with a comment and JSON6902 patch that pins spec.forProvider.hasDownloads to false for targeted GitHub Repository resources.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main change: pinning hasDownloads to stop reconciliation 422 errors.
Description check ✅ Passed The description is clearly related to the change and explains the hasDownloads reconciliation issue.

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.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Readiness evidence at 0e3bd09b86a0bd06bd689e202c29648d21fa8ba6.

Axis State
Required checks all green, 0 failing
Unresolved threads 0
Non-thread review findings 0 — CodeRabbit's only section is 🔇 Additional comments (2), the excluded informational one
Conflict with base none — CLEAN
Green review at head cr@0e3bd09b — substantive summary naming this head, no rate-limit or service marker

Programmatically tested. The repository's three required commands all pass: kubectl kustomize deploy/, tests/admin-team-policy.sh, tests/declarative-coverage.sh (OK — 22 repositories across 4 rendered dimensions).

Exercised — what I actually observed, and its limit. This is configuration consumed by Flux and Crossplane in-cluster, so it has no locally-runnable surface. I verified the rendered artifact rather than reasoning about it: the build emits 20 Repository resources and exactly 19 carry the new pin. The 20th is reusable-workflows, which is correct — it renders from archived-repositories/, which deliberately takes no shared patch, and its live value already agrees. Pinning it there would have manufactured the mismatch this change removes. I checked that specific count precisely because "19 of 20" is what a silently-missed target looks like.

I also confirmed on the live cluster that the field this change stops sending is genuinely the discriminator: agreement on it predicts sync state across all 20 resources with no exceptions, and no other field separates the healthy from the failing.

What is NOT yet proven, stated plainly: that reconciliation actually recovers. The mirror these resources publish is demonstrably incomplete — one resource reports a field mismatch while sync succeeds — so the provider's internal comparison cannot be observed from outside. The correlation is exact and this is the best-supported explanation, but the mechanism is inferred.

That is acceptable to merge because the failure mode is a no-op, not a regression: every update attempt is already rejected in full, so this cannot make any repository worse, and it is one line to revert. Note also that cd.yaml publishes on v* tags only, so this does not reach the cluster on merge.

Verification is therefore post-merge and I will report it on #123: the failing resources returning to a healthy sync state, checked against the GitHub API rather than the mirror.

@devantler
devantler marked this pull request as ready for review July 27, 2026 04:32
@devantler
devantler merged commit 696dbb2 into main Jul 27, 2026
11 checks passed
@devantler
devantler deleted the claude/github-config-hasdownloads-phantom-diff-123 branch July 27, 2026 04:37
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.

1 participant