diff --git a/AGENTS.md b/AGENTS.md index c1abc28..9299060 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,10 +51,15 @@ for the architecture, the GitHub App credential setup, and the Observe-first ado the create-only `initProvider`. upjet builds the Terraform configuration from `forProvider`, an absent optional bool takes the provider's zero value of `false`, and `false` against a live `true` is a permanent diff, so every update PATCH carries `web_commit_signoff_required: false` and GitHub - rejects the whole request with 422 "Commit signoff is enforced ... and cannot be disabled". 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. `tests/repository-update-policy.sh` pins - this. Verify + rejects the whole request with 422 "Commit signoff is enforced ... and cannot be disabled". + Declaring the live value removes that field's own diff, but the deployed provider-upjet-github + v0.19.1 embeds terraform-provider-github v6.6.0 and still includes the field when another setting + needs an update; GitHub rejects the entire PATCH while organization signoff is enforced + ([upstream #2077](https://github.com/integrations/terraform-provider-github/issues/2077)). Until a + provider-upjet-github release includes the upstream v6.12.0 fix, do not introduce new Repository + drift merely for optional discovery metadata; `agent-plugins` and `agent-skills` topics are + deliberately omitted. `tests/repository-update-policy.sh` pins both the required live signoff + value and this temporary compatibility boundary. 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 diff --git a/deploy/repositories/agent-plugins.yaml b/deploy/repositories/agent-plugins.yaml index c462d6c..cba0485 100644 --- a/deploy/repositories/agent-plugins.yaml +++ b/deploy/repositories/agent-plugins.yaml @@ -10,8 +10,8 @@ metadata: crossplane.io/external-name: agent-plugins spec: # The squash-only merge policy and the org-enforced commit signoff come from - # the shared patch. Description and topics - # are declared here so the config is AUTHORITATIVE for them. Settings declared + # the shared patch. Description is declared here so the config is + # AUTHORITATIVE for it. Settings declared # nowhere here keep the value adopted from the live repo when it was first # observed. managementPolicies: @@ -25,9 +25,10 @@ spec: # that no longer exists. Also names the bundled MCP servers and agents — # the marketplace is deliberately not skills-only. description: "Tool-neutral agent-plugin marketplace — bundles curated skills, MCP servers and agents from devantler-tech/agent-skills into category plugins for VS Code, GitHub Copilot CLI, and Claude Code" - # Discovery surface — the repo carried no topics at all, so it was invisible - # to every topic-based browse and directory crawl. - topics: ["agent-plugins", "agent-skills", "ai-agents", "claude-code", "claude-code-marketplace", "claude-code-plugin", "claude-plugin", "github-copilot", "plugin-marketplace", "vscode"] + # Topics stay temporarily unmanaged. provider-upjet-github v0.19.1 embeds + # terraform-provider-github v6.6.0, whose repository update includes the + # org-enforced web-signoff field and makes GitHub reject the whole PATCH. + # Restore topics after a provider release includes the upstream v6.12 fix. providerConfigRef: kind: ProviderConfig name: default diff --git a/deploy/repositories/agent-skills.yaml b/deploy/repositories/agent-skills.yaml index 110bf18..6ecc4ad 100644 --- a/deploy/repositories/agent-skills.yaml +++ b/deploy/repositories/agent-skills.yaml @@ -11,8 +11,8 @@ metadata: crossplane.io/external-name: agent-skills spec: # The squash-only merge policy and the org-enforced commit signoff come from - # the shared patch. Description and topics - # are declared here so the config is AUTHORITATIVE for them. Settings declared + # the shared patch. Description is declared here so the config is + # AUTHORITATIVE for it. Settings declared # nowhere here keep the value adopted from the live repo when it was first # observed. managementPolicies: @@ -26,11 +26,10 @@ spec: # either `gh skill install` or `npx skills add`. The old wording undersold # that and read as Copilot-only. description: "Agent-neutral skills for Claude Code, Copilot, Cursor and Codex — install with `gh skill install` or `npx skills add`" - # Discovery surface. `gh skill publish --tag` runs non-interactively in - # cd.yaml and therefore SKIPS the interactive "add the agent-skills topic" - # step, so the canonical topic was never applied — this declares it instead. - # The rest are the terms users actually browse/search by. - topics: ["agent-skills", "agent-skill", "ai-agents", "claude-code", "claude-skill", "codex", "cursor", "github-copilot", "gitops", "kubernetes", "skills"] + # Topics stay temporarily unmanaged. provider-upjet-github v0.19.1 embeds + # terraform-provider-github v6.6.0, whose repository update includes the + # org-enforced web-signoff field and makes GitHub reject the whole PATCH. + # Restore topics after a provider release includes the upstream v6.12 fix. providerConfigRef: kind: ProviderConfig name: default diff --git a/deploy/repositories/kustomization.yaml b/deploy/repositories/kustomization.yaml index 73f8765..4a83b5a 100644 --- a/deploy/repositories/kustomization.yaml +++ b/deploy/repositories/kustomization.yaml @@ -41,18 +41,13 @@ resources: # - squash-only merge policy (merge commits + rebase disabled; auto-merge, # auto-delete head branches, always-suggest-updating-PR-branches enabled). # - webCommitSignoffRequired: true — declared precisely BECAUSE the org enforces -# it. Leaving the field unconfigured is what breaks updates, not declaring it. -# upjet builds the Terraform configuration from forProvider, an absent optional -# bool takes the provider's zero value of false, and false against a live true -# is a permanent diff — so every update PATCH carries -# web_commit_signoff_required: false and GitHub rejects the whole request with -# 422 "Commit signoff is enforced ... and cannot be disabled". The error names -# disabling, not presence. Declaring the live value leaves nothing to diff, so -# Terraform omits the field from the payload and the rest of the update -# applies. Measured: with this line present, nine write-enabled repositories -# completed update PATCHes at 2026-07-27T04:34:5xZ; two minutes after it was -# removed the same repositories began failing on that 422, and seven were -# still failing on 2026-08-06. See #112. +# it. Leaving the field unconfigured creates a permanent false-versus-live-true +# diff. Declaring true removes that field's own diff, but the deployed +# provider-upjet-github v0.19.1 still includes the unchanged field whenever +# another Repository setting needs an update; GitHub rejects that PATCH under +# organization signoff enforcement. Until a provider release includes the +# terraform-provider-github v6.12.0 fix, optional Repository drift must remain +# absent as pinned by tests/repository-update-policy.sh. See upstream #2077. # - 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 a spec holding that default diff --git a/deploy/repositories/platform-tenant-template.yaml b/deploy/repositories/platform-tenant-template.yaml index acfcde0..323912b 100644 --- a/deploy/repositories/platform-tenant-template.yaml +++ b/deploy/repositories/platform-tenant-template.yaml @@ -24,6 +24,9 @@ spec: # restoring the write path (#112) unable to change it. visibility: public hasIssues: true + # Matches live state and advances this MR's generation so Crossplane drops + # the failed asynchronous update queued before the provider workaround. + hasProjects: false description: "Template for platform tenants on the devantler-tech platform — framework-agnostic CI/CD plumbing kept current via template-sync." providerConfigRef: kind: ProviderConfig diff --git a/tests/repository-update-policy.sh b/tests/repository-update-policy.sh index fdd4747..5108007 100755 --- a/tests/repository-update-policy.sh +++ b/tests/repository-update-policy.sh @@ -59,25 +59,17 @@ unsafe_policies="$( fail "active Repository resources must not pair Update with LateInitialize, must exclude Delete, and must Observe: $unsafe_policies" # Every active repository must declare webCommitSignoffRequired: true in -# forProvider, because the org enforces commit signoff and live is therefore -# always true. Leaving the field unconfigured does NOT keep it out of the update -# payload: upjet builds the Terraform configuration from forProvider, an absent -# optional bool takes the provider's zero value of false, and false against a -# live true is a permanent diff — so every update PATCH carries -# web_commit_signoff_required: false and GitHub rejects the whole request with -# 422 "Commit signoff is enforced by the organization and cannot be disabled". -# Declaring the live value leaves nothing to diff, so Terraform omits the field -# from the payload and the rest of the update applies. +# forProvider because the org enforces commit signoff and live is therefore +# always true. Leaving the field unconfigured creates its own permanent false +# versus true diff. Declaring true removes that diff, but it does not make +# arbitrary updates safe with the deployed provider: v0.19.1 embeds +# terraform-provider-github v6.6.0, which still includes the unchanged field +# when another Repository setting changes, and GitHub rejects that PATCH. +# The compatibility guard below therefore also prevents the known optional +# topic drift until a provider release includes the upstream v6.12.0 fix. # -# The 422 names disabling, not presence, and the cluster agrees: at -# 2026-07-27T04:34:5xZ, with this field declared by the shared patch, nine -# write-enabled repositories recorded LastAsyncOperation=Success — completed -# update PATCHes. Two minutes after the declaration was removed the same -# repositories began recording AsyncUpdateFailure carrying that 422, and seven -# were still failing on 2026-08-06. See devantler-tech/.github#112. -# -# initProvider is not a substitute: Crossplane applies it only at creation, so -# forProvider stays unconfigured and the permanent diff above is unchanged. +# initProvider is not a substitute for the required live declaration: +# Crossplane applies it only at creation, so forProvider would remain false. missing_signoff="$( yq -N ' select( @@ -123,4 +115,55 @@ platform_tenant_issues="$( [[ "$platform_tenant_issues" == "true" ]] || fail "platform-tenant-template must declare forProvider.hasIssues: true so its issue roadmap remains available" -echo "repository-update-policy: OK — $active_count active repositories declare org-enforced signoff and the platform-tenant-template issue tracker" +# provider-upjet-github v0.19.1 embeds terraform-provider-github v6.6.0. +# GitHub rejects every repository PATCH containing web_commit_signoff_required +# while the organization enforces that setting, even when the requested value +# is true (integrations/terraform-provider-github#2077). Topic-only drift on +# these two resources therefore wedges the complete update. Keep the attempted +# topics absent until a provider-upjet-github release includes the upstream +# v6.12.0 fix, which omits the field when it is unchanged. +compatibility_repositories="$( + yq -N ' + select( + .kind == "Repository" and + .spec.forProvider.archived != true and + (.metadata.name == "agent-plugins" or .metadata.name == "agent-skills") + ) | + .metadata.name + ' "$render" | sort +)" +expected_compatibility_repositories="$(printf '%s\n' agent-plugins agent-skills)" +[[ "$compatibility_repositories" == "$expected_compatibility_repositories" ]] || + fail "provider compatibility guard requires both agent repositories; got: $compatibility_repositories" + +blocked_topic_updates="$( + yq -N ' + select( + .kind == "Repository" and + (.metadata.name == "agent-plugins" or .metadata.name == "agent-skills") and + (.spec.forProvider | has("topics")) + ) | + .metadata.name + ' "$render" +)" +[[ -z "$blocked_topic_updates" ]] || + fail "provider-upjet-github v0.19.1 cannot update repository topics under org-enforced signoff: $blocked_topic_updates" + +# Pin a matching live value to advance this resource's generation and cancel +# the failed async update which predates the signoff workaround. The template +# intentionally does not use GitHub Projects. +platform_tenant_projects="$( + yq -N ' + select( + .kind == "Repository" and + .metadata.name == "platform-tenant-template" + ) | + .spec.forProvider.hasProjects | + select(tag == "!!bool") | + select(. == false) + ' "$render" +)" +[[ "$platform_tenant_projects" == "false" ]] || + fail "platform-tenant-template must pin forProvider.hasProjects: false while resetting its failed async update" + +echo "repository-update-policy: OK — $active_count active repositories declare org-enforced signoff and the provider-v0.19.1 update workaround"