From dfb83b0429bf4d03d8a3e290e3aed37723819884 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Thu, 22 Oct 2026 20:58:22 +0200 Subject: [PATCH 1/4] fix(config): withdraw legacy Actions management --- .github/workflows/ci.yaml | 3 + AGENTS.md | 3 +- deploy/README.md | 15 +++++ deploy/labels/actions.yaml | 32 ---------- deploy/labels/kustomization.yaml | 1 - deploy/repository-permissions/actions.yaml | 27 -------- .../repository-permissions/kustomization.yaml | 1 - .../grant-admins-on-actions.yaml | 18 ------ .../grant-maintainers-on-actions.yaml | 20 ------ deploy/team-repositories/kustomization.yaml | 2 - scripts/validate-actions-retirement.sh | 36 +++++++++++ tests/actions-retirement.sh | 62 +++++++++++++++++++ tests/admin-team-policy.sh | 16 ++--- tests/declarative-coverage-fail-closed.sh | 12 ++-- tests/declarative-coverage.sh | 24 +------ 15 files changed, 134 insertions(+), 138 deletions(-) delete mode 100644 deploy/labels/actions.yaml delete mode 100644 deploy/repository-permissions/actions.yaml delete mode 100644 deploy/team-repositories/grant-admins-on-actions.yaml delete mode 100644 deploy/team-repositories/grant-maintainers-on-actions.yaml create mode 100755 scripts/validate-actions-retirement.sh create mode 100755 tests/actions-retirement.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e287cb3f..fb5dd5b5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6156,6 +6156,9 @@ jobs: - name: ๐Ÿงช Test declarative coverage failure handling run: bash tests/declarative-coverage-fail-closed.sh + - name: Test legacy Actions management stays withdrawn + run: bash tests/actions-retirement.sh + - name: ๐Ÿงช Test repository update policy run: bash tests/repository-update-policy.sh diff --git a/AGENTS.md b/AGENTS.md index 50650e6d..5bcc330d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -156,6 +156,7 @@ 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/declarative-coverage-fail-closed.sh # rendered-label reads fail closed +bash tests/actions-retirement.sh # retired catalogue has no rendered management bash tests/repository-update-policy.sh # active Repository update invariants bash tests/signing-rule-retirement.sh # retained signing-rule identity and safe lifecycle bash tests/release-contract.sh # deploy/ changes must trigger a release @@ -168,7 +169,7 @@ bash tests/workflow-execution-policies.sh # reviewed execution policies are vali bash tests/apply-workflow-execution-policies.sh # the policy reconciler, against an offline API stand-in ``` -Those fourteen commands are the baseline checks that `ci.yaml`'s `validate-manifests` job runs; the +Those fifteen commands are the baseline checks that `ci.yaml`'s `validate-manifests` job runs; the same workflow also runs the catalogue's tests (see [Actions catalogue](#actions-catalogue)). Pull requests additionally pass their changed paths and title through `scripts/validate-release-contract.sh` and their base/head renders plus the pull-request body through `scripts/validate-deploy-deletions.sh` (every managed diff --git a/deploy/README.md b/deploy/README.md index 3d9c03a0..c1efe51c 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -51,6 +51,21 @@ out-of-band changes made in the GitHub UI. self-repository `$/` references stay allowed; reusable workflows can still be referenced by tag. +## Legacy Actions retirement + +The legacy `actions` repository has no managed Repository, labels, permissions or +team grants in this render. Its former resources did not claim `Delete`, so +withdrawing their manifests leaves the hosted objects in place until the separately +authorized repository deletion. `tests/actions-retirement.sh` checks the actual +render, rejects reintroduced management and proves incomplete reads fail closed. + +This configuration change does not itself delete the repository or prove its last +publisher migrated. [#240](https://github.com/devantler-tech/.github/issues/240) +requires protected consumer migration, signed publication, observed GitOps +withdrawal and a private recovery backup before deletion. The live-coverage +exemption remains only while the repository is present; remove it after authenticated +post-delete checks. Drift, complete-census and deletion-acknowledgement gates stay enabled. + ## Roadmap GitHub Issues are the roadmap of record. Epic diff --git a/deploy/labels/actions.yaml b/deploy/labels/actions.yaml deleted file mode 100644 index 3c023b7a..00000000 --- a/deploy/labels/actions.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Issue labels for devantler-tech/actions. The canonical org taxonomy (22 labels) -# is appended to every IssueLabels by the shared patch in kustomization.yaml; this -# file adds only actions's repo-specific automation labels: dependency/ecosystem -# markers used by Dependabot/Renovate and lifecycle markers used by Release -# Please. Declaring them prevents authoritative reconciliation from deleting -# state that those automations require. See ../README.md. -apiVersion: repo.github.m.upbound.io/v1alpha1 -kind: IssueLabels -metadata: - name: actions - annotations: - crossplane.io/external-name: actions -spec: - managementPolicies: - - Observe - - Create - - Update - - LateInitialize - forProvider: - repository: actions - label: - - name: "dependencies" - color: "0366d6" - - name: "github_actions" - color: "000000" - - name: "autorelease: pending" - color: "ededed" - - name: "autorelease: tagged" - color: "ededed" - providerConfigRef: - kind: ProviderConfig - name: default diff --git a/deploy/labels/kustomization.yaml b/deploy/labels/kustomization.yaml index fe5e1fa8..683d5ed1 100644 --- a/deploy/labels/kustomization.yaml +++ b/deploy/labels/kustomization.yaml @@ -37,7 +37,6 @@ resources: - go-template.yaml - dotnet-template.yaml - homebrew-tap.yaml - - actions.yaml - cloudflare.yaml - khoist.yaml - business-site.yaml diff --git a/deploy/repository-permissions/actions.yaml b/deploy/repository-permissions/actions.yaml deleted file mode 100644 index 458563ce..00000000 --- a/deploy/repository-permissions/actions.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Actions permissions for devantler-tech/actions โ€” binds the org-wide -# shaPinningRequired policy (asserted once in kustomization.yaml) to this repo. -# Observe+LateInitialize adopts the repo's live enabled/allowedActions so turning -# on SHA-pinning never disturbs them. See ../README.md. -# -# Previously excluded while this repo's first-party self-references were tag-pinned -# (release-please rewrote them per release). Those accommodations were reverted in -# devantler-tech/actions#541 โ€” self-references now resolve by local path after a -# same-commit self-checkout and every remote `uses:` is SHA-pinned โ€” so the repo -# now joins the org-wide policy (devantler-tech/actions#426). -apiVersion: actions.github.m.upbound.io/v1alpha1 -kind: RepositoryPermissions -metadata: - name: actions - annotations: - crossplane.io/external-name: actions -spec: - managementPolicies: - - Observe - - Create - - Update - - LateInitialize - forProvider: - repository: actions - providerConfigRef: - kind: ProviderConfig - name: default diff --git a/deploy/repository-permissions/kustomization.yaml b/deploy/repository-permissions/kustomization.yaml index b7e509b9..981353f3 100644 --- a/deploy/repository-permissions/kustomization.yaml +++ b/deploy/repository-permissions/kustomization.yaml @@ -23,7 +23,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - dot-github.yaml - - actions.yaml - platform.yaml - ksail.yaml - agent-skills.yaml diff --git a/deploy/team-repositories/grant-admins-on-actions.yaml b/deploy/team-repositories/grant-admins-on-actions.yaml deleted file mode 100644 index dcc6a310..00000000 --- a/deploy/team-repositories/grant-admins-on-actions.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# Admins-team repository grant. No external-name is set because the separate -# team and its admin grant are created declaratively without claiming Delete. -apiVersion: team.github.m.upbound.io/v1alpha1 -kind: TeamRepository -metadata: - name: admins-actions -spec: - managementPolicies: ["Observe", "Create", "Update", "LateInitialize"] - forProvider: - teamIdRef: - name: admins - policy: - resolve: Always - repository: actions - permission: admin - providerConfigRef: - kind: ProviderConfig - name: default diff --git a/deploy/team-repositories/grant-maintainers-on-actions.yaml b/deploy/team-repositories/grant-maintainers-on-actions.yaml deleted file mode 100644 index 36d19c44..00000000 --- a/deploy/team-repositories/grant-maintainers-on-actions.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Team โ†’ repository access, adopted Observe-first (read-only). -# external-name = `:`. -apiVersion: team.github.m.upbound.io/v1alpha1 -kind: TeamRepository -metadata: - name: maintainers-actions - annotations: - crossplane.io/external-name: "17046606:actions" -spec: - managementPolicies: ["Observe"] - forProvider: - teamIdRef: - name: maintainers - policy: - resolve: Always - repository: actions - permission: maintain - providerConfigRef: - kind: ProviderConfig - name: default diff --git a/deploy/team-repositories/kustomization.yaml b/deploy/team-repositories/kustomization.yaml index b713ca0b..5639ad0a 100644 --- a/deploy/team-repositories/kustomization.yaml +++ b/deploy/team-repositories/kustomization.yaml @@ -7,7 +7,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: # Observe-adopted (access already existed). - - grant-maintainers-on-actions.yaml - grant-maintainers-on-dotnet-template.yaml - grant-maintainers-on-go-template.yaml - grant-maintainers-on-ksail.yaml @@ -33,7 +32,6 @@ resources: - grant-maintainers-on-client-orders.yaml # Active Admins policy (one admin grant per active repository). - grant-admins-on-dot-github.yaml - - grant-admins-on-actions.yaml - grant-admins-on-agent-plugins.yaml - grant-admins-on-agent-skills.yaml - grant-admins-on-ascoachingogvaner.yaml diff --git a/scripts/validate-actions-retirement.sh b/scripts/validate-actions-retirement.sh new file mode 100755 index 00000000..81eff1a5 --- /dev/null +++ b/scripts/validate-actions-retirement.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Read-only rendered-state guard for the maintainer-authorized Actions retirement (#240). +# 0: withdrawn; 1: a resource still targets the retired repository; 2: incomplete observation. +set -euo pipefail +abort() { printf 'actions-retirement: UNKNOWN โ€” %s\n' "$*" >&2; exit 2; } +[[ "$#" == 1 && -f "$1" && ! -L "$1" && -s "$1" ]] || + abort 'one non-empty rendered manifest is required' +for tool in yq jq; do command -v "$tool" >/dev/null || abort "missing parser"; done +work="$(mktemp -d)" || abort 'cannot allocate observation directory' +trap 'rm -rf "$work"' EXIT +if ! yq -o=json -I=0 '.' "$1" >"$work/documents.json"; then + abort 'rendered manifest could not be read completely' +fi +if ! jq -se ' + length > 0 and all(type == "object" and + (.kind | type == "string" and length > 0) and + (.metadata.name | type == "string" and length > 0)) + ' "$work/documents.json" >/dev/null; then + abort 'rendered resource inventory is empty or malformed' +fi +if ! jq -s ' + [ .[] | select( + ((.kind == "Repository") and + (.spec.forProvider.name == "actions" or .spec.initProvider.name == "actions")) or + .spec.forProvider.repository == "actions" or .spec.initProvider.repository == "actions" + ) ] | length + ' "$work/documents.json" >"$work/count"; then + abort 'retirement selector failed' +fi +count="$(<"$work/count")" +[[ "$count" =~ ^[0-9]+$ ]] || abort 'invalid retirement observation' +if ((count > 0)); then + printf 'actions-retirement: retired catalogue is still managed (%s resources)\n' "$count" >&2 + exit 1 +fi +printf 'actions-retirement: no managed resources target the retired catalogue\n' diff --git a/tests/actions-retirement.sh b/tests/actions-retirement.sh new file mode 100755 index 00000000..7c74f827 --- /dev/null +++ b/tests/actions-retirement.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Keep retirement #240 from accidentally restoring managed resources for the deleted catalogue. +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +validator="$root/scripts/validate-actions-retirement.sh" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +fail() { printf 'actions-retirement test: %s\n' "$*" >&2; exit 1; } +check() { + local expected="$1" input="$2" rc=0 + bash "$validator" "$input" >"$work/out" 2>"$work/err" || rc=$? + [[ "$rc" == "$expected" ]] || fail "expected $expected, got $rc" +} +cat >"$work/clean.yaml" <<'YAML' +apiVersion: actions.github.m.upbound.io/v1alpha1 +kind: RepositoryPermissions +metadata: + name: maintained-catalogue +spec: + forProvider: + repository: .github +YAML +check 0 "$work/clean.yaml" +grep -Fq 'no managed resources target the retired catalogue' "$work/out" || + fail 'clean control did not examine the render' +for kind in Repository IssueLabels TeamRepository RepositoryPermissions; do + cp "$work/clean.yaml" "$work/retired.yaml" + cat >>"$work/retired.yaml" <"$work/invalid.yaml" +check 2 "$work/invalid.yaml" +: >"$work/empty.yaml" +check 2 "$work/empty.yaml" +check 2 "$work/missing.yaml" +# A parser emitting plausible partial data and then failing cannot establish withdrawal. +real_yq="$(command -v yq)" +mkdir "$work/bin" +cat >"$work/bin/yq" <<'SH' +#!/usr/bin/env bash +printf '{"kind":"Repository","metadata":{"name":"safe"},"spec":{"forProvider":{"name":"safe"}}}\n' +exit 42 +SH +chmod +x "$work/bin/yq" +PATH="$work/bin:$PATH" check 2 "$work/clean.yaml" +[[ -x "$real_yq" ]] || fail 'real parser is unavailable' +# The actual rendered desired state, not filenames alone, must establish withdrawal. +kubectl kustomize "$root/deploy" >"$work/actual.yaml" +check 0 "$work/actual.yaml" +printf 'actions-retirement: OK โ€” rendered withdrawal, four re-add controls and failed reads\n' diff --git a/tests/admin-team-policy.sh b/tests/admin-team-policy.sh index 06f5dbbf..4fe70a67 100755 --- a/tests/admin-team-policy.sh +++ b/tests/admin-team-policy.sh @@ -30,9 +30,9 @@ require_fixed_count() { require_count 1 '^ name: admins$' "$production_render" require_count 1 '^ name: admins-devantler$' "$production_render" -require_count 25 '^ name: admins-' "$production_render" -require_count 24 '^ permission: admin$' "$production_render" -require_count 25 '^ name: admins$' "$production_render" +require_count 24 '^ name: admins-' "$production_render" +require_count 23 '^ permission: admin$' "$production_render" +require_count 24 '^ name: admins$' "$production_render" grant_files=("$repo_root"/deploy/team-repositories/grant-admins-on-*.yaml) policy_files=( @@ -40,15 +40,14 @@ policy_files=( "$repo_root/deploy/team-memberships/add-devantler-to-admins.yaml" "${grant_files[@]}" ) -[[ "${#grant_files[@]}" == 24 ]] || - fail "expected 24 Admins grants, got ${#grant_files[@]}" -[[ "${#policy_files[@]}" == 26 ]] || - fail "expected 26 Admins policy files, got ${#policy_files[@]}" +[[ "${#grant_files[@]}" == 23 ]] || + fail "expected 23 Admins grants, got ${#grant_files[@]}" +[[ "${#policy_files[@]}" == 25 ]] || + fail "expected 25 Admins policy files, got ${#policy_files[@]}" cat "${policy_files[@]}" >"$policy_source" repositories=( .github - actions agent-plugins agent-skills ascoachingogvaner @@ -80,6 +79,7 @@ done require_count 0 '^ repository: doggy-countdown$' "$policy_source" require_count 0 '^ repository: reusable-workflows$' "$policy_source" require_count 0 '^ repository: maintenance$' "$policy_source" +require_count 0 '^ repository: actions$' "$policy_source" if grep -Eq '^[[:space:]]*managementPolicies:.*Delete|crossplane.io/external-name' \ "${policy_files[@]}"; then diff --git a/tests/declarative-coverage-fail-closed.sh b/tests/declarative-coverage-fail-closed.sh index 0df4ff19..925b572b 100644 --- a/tests/declarative-coverage-fail-closed.sh +++ b/tests/declarative-coverage-fail-closed.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # -# Negative control for the rendered Actions-label policy. A label reader may -# emit partial output before failing; that output must never satisfy the policy. +# Negative control for rendered label-dimension coverage. A reader may emit +# partial output before failing; that output must never satisfy the policy. set -euo pipefail @@ -20,8 +20,8 @@ cat >"$work/bin/yq" <<'EOF' #!/usr/bin/env bash set -euo pipefail -if [[ "$*" == *'.spec.forProvider.label[].name'* ]]; then - printf '%s\n' 'autorelease: pending' 'autorelease: tagged' +if [[ "$*" == *'select(.kind=="IssueLabels")'* ]]; then + printf '%s\n' '.github' 'monorepo' exit 42 fi @@ -31,10 +31,10 @@ chmod +x "$work/bin/yq" if REAL_YQ="$real_yq" PATH="$work/bin:$PATH" \ bash "$repo_root/tests/declarative-coverage.sh" >"$work/stdout" 2>"$work/stderr"; then - fail "partial label output masked a yq read failure" + fail "partial dimension output masked a yq read failure" fi -grep -Fq "actions: failed to read rendered issue labels" "$work/stderr" || { +grep -Fq "dimension 'labels': yq selector failed" "$work/stderr" || { echo "declarative-coverage fail-closed test: unexpected failure:" >&2 cat "$work/stderr" >&2 exit 1 diff --git a/tests/declarative-coverage.sh b/tests/declarative-coverage.sh index 39f4787c..690736f4 100755 --- a/tests/declarative-coverage.sh +++ b/tests/declarative-coverage.sh @@ -53,9 +53,6 @@ dimensions=( # silence a fresh failure without a tracked reason. The guard fails on a stale # exemption, so this list cannot rot. exemptions=( - # actions stays outside Repository management until verified full deletion - # (#240); its other managed dimensions remain during consumer migration. - "repositories/actions" # fleet-gitops is private; the Maintainers team deliberately stays off it # (maintainer decision on #115, 2026-09-25). "team-maintainers/fleet-gitops" @@ -150,25 +147,8 @@ if [[ "$missing_count" -gt 0 ]]; then exit 1 fi -# Release Please identifies a merged release PR through these lifecycle labels. -# IssueLabels is authoritative, so omitting either marker makes the reconciler -# delete it before merge and causes Release Please to rescan historical commits. -if ! actions_labels_raw="$( - yq -N ' - select(.kind == "IssueLabels" and .spec.forProvider.repository == "actions") - | .spec.forProvider.label[].name - ' "$render" -)"; then - fail "actions: failed to read rendered issue labels" -fi -actions_labels="$( - printf '%s\n' "$actions_labels_raw" | grep -vE '^$|^null$|^---$' || true -)" -[[ -n "$actions_labels" ]] || fail "actions: no rendered issue labels" -for required_label in "autorelease: pending" "autorelease: tagged"; do - printf '%s\n' "$actions_labels" | grep -Fxq "$required_label" || - fail "actions: required Release Please label '$required_label' is undeclared" -done +# Actions' Release Please labels left the managed model with the retired repository. +# The separate retirement guard rejects restoring any of its managed dimensions (#240). echo "declarative-coverage: OK โ€” $expected_count repositories across ${#labels[@]} rendered" \ "dimensions ($exempt_count declared exemption(s))" From 575741f0d8f5726a812065ba6944de9faf5f0ad1 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Thu, 22 Oct 2026 22:04:53 +0200 Subject: [PATCH 2/4] fix(config): reject adopted and mixed-case retired identities --- scripts/validate-actions-retirement.sh | 7 +++++-- tests/actions-retirement.sh | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/scripts/validate-actions-retirement.sh b/scripts/validate-actions-retirement.sh index 81eff1a5..39991942 100755 --- a/scripts/validate-actions-retirement.sh +++ b/scripts/validate-actions-retirement.sh @@ -2,6 +2,7 @@ # Read-only rendered-state guard for the maintainer-authorized Actions retirement (#240). # 0: withdrawn; 1: a resource still targets the retired repository; 2: incomplete observation. set -euo pipefail +# An incomplete render or parser result can never prove that management was withdrawn. abort() { printf 'actions-retirement: UNKNOWN โ€” %s\n' "$*" >&2; exit 2; } [[ "$#" == 1 && -f "$1" && ! -L "$1" && -s "$1" ]] || abort 'one non-empty rendered manifest is required' @@ -19,10 +20,12 @@ if ! jq -se ' abort 'rendered resource inventory is empty or malformed' fi if ! jq -s ' + def retired: if type == "string" then ascii_downcase == "actions" else false end; [ .[] | select( ((.kind == "Repository") and - (.spec.forProvider.name == "actions" or .spec.initProvider.name == "actions")) or - .spec.forProvider.repository == "actions" or .spec.initProvider.repository == "actions" + ([.spec.forProvider.name, .spec.initProvider.name, + .metadata.annotations["crossplane.io/external-name"]] | any(retired))) or + ([.spec.forProvider.repository, .spec.initProvider.repository] | any(retired)) ) ] | length ' "$work/documents.json" >"$work/count"; then abort 'retirement selector failed' diff --git a/tests/actions-retirement.sh b/tests/actions-retirement.sh index 7c74f827..557ec2f3 100755 --- a/tests/actions-retirement.sh +++ b/tests/actions-retirement.sh @@ -5,7 +5,9 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" validator="$root/scripts/validate-actions-retirement.sh" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT +# Fail at the first incorrect observation rather than accepting a partial inventory. fail() { printf 'actions-retirement test: %s\n' "$*" >&2; exit 1; } +# Keep withdrawn, still-managed and unreadable outcomes distinct for every control. check() { local expected="$1" input="$2" rc=0 bash "$validator" "$input" >"$work/out" 2>"$work/err" || rc=$? @@ -40,6 +42,18 @@ YAML grep -Fq 'retired catalogue is still managed' "$work/err" || fail "$kind was refused for the wrong reason" done +# GitHub repository identifiers are case-insensitive in both provider phases. +for field in forProvider.name initProvider.name forProvider.repository initProvider.repository; do + FIELD="$field" yq '.kind = "Repository" | .spec |= setpath(strenv(FIELD) | split("."); "AcTiOnS")' \ + "$work/clean.yaml" >"$work/mixed.yaml" + check 1 "$work/mixed.yaml" +done +# An adopted resource can target Actions through external identity even when its +# desired name differs; Observe-only management must still prevent retirement. +yq '.kind = "Repository" | .metadata.annotations."crossplane.io/external-name" = "AcTiOnS" | + .spec.forProvider.name = "different-desired-name" | .spec.managementPolicies = ["Observe"]' \ + "$work/clean.yaml" >"$work/adopted.yaml" +check 1 "$work/adopted.yaml" printf 'not: [valid\n' >"$work/invalid.yaml" check 2 "$work/invalid.yaml" : >"$work/empty.yaml" @@ -59,4 +73,4 @@ PATH="$work/bin:$PATH" check 2 "$work/clean.yaml" # The actual rendered desired state, not filenames alone, must establish withdrawal. kubectl kustomize "$root/deploy" >"$work/actual.yaml" check 0 "$work/actual.yaml" -printf 'actions-retirement: OK โ€” rendered withdrawal, four re-add controls and failed reads\n' +printf 'actions-retirement: OK โ€” rendered withdrawal, case/adopted identities and failed reads\n' From 9f29611412d4442b79137909afd2e20a0d1bce3b Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Thu, 22 Oct 2026 23:47:35 +0200 Subject: [PATCH 3/4] fix(config): guard adopted retirement identities --- scripts/validate-actions-retirement.sh | 15 +++++++++++--- tests/actions-retirement.sh | 27 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/scripts/validate-actions-retirement.sh b/scripts/validate-actions-retirement.sh index 39991942..27e80c94 100755 --- a/scripts/validate-actions-retirement.sh +++ b/scripts/validate-actions-retirement.sh @@ -19,13 +19,22 @@ if ! jq -se ' ' "$work/documents.json" >/dev/null; then abort 'rendered resource inventory is empty or malformed' fi +# The pinned provider imports repository-owned resources by repository name and +# team grants by team ID/slug:repository; Observe-only imports may omit spec IDs. if ! jq -s ' def retired: if type == "string" then ascii_downcase == "actions" else false end; + def retired_team_repository: + if type == "string" then split(":") as $identity | + ($identity | length == 2) and ($identity[1] | retired) + else false end; [ .[] | select( ((.kind == "Repository") and - ([.spec.forProvider.name, .spec.initProvider.name, - .metadata.annotations["crossplane.io/external-name"]] | any(retired))) or - ([.spec.forProvider.repository, .spec.initProvider.repository] | any(retired)) + ([.spec.forProvider.name, .spec.initProvider.name] | any(retired))) or + ([.spec.forProvider.repository, .spec.initProvider.repository] | any(retired)) or + ((.kind == "Repository" or .kind == "IssueLabels" or .kind == "RepositoryPermissions") and + (.metadata.annotations["crossplane.io/external-name"] | retired)) or + ((.kind == "TeamRepository") and + (.metadata.annotations["crossplane.io/external-name"] | retired_team_repository)) ) ] | length ' "$work/documents.json" >"$work/count"; then abort 'retirement selector failed' diff --git a/tests/actions-retirement.sh b/tests/actions-retirement.sh index 557ec2f3..fc001ea5 100755 --- a/tests/actions-retirement.sh +++ b/tests/actions-retirement.sh @@ -54,6 +54,33 @@ yq '.kind = "Repository" | .metadata.annotations."crossplane.io/external-name" = .spec.forProvider.name = "different-desired-name" | .spec.managementPolicies = ["Observe"]' \ "$work/clean.yaml" >"$work/adopted.yaml" check 1 "$work/adopted.yaml" +# Import identities remain authoritative even when Observe-only resources omit +# both desired-state provider blocks. Each retired resource kind needs coverage. +for kind in Repository IssueLabels RepositoryPermissions; do + KIND="$kind" yq '.kind = strenv(KIND) | .spec.forProvider = {} | + .spec.managementPolicies = ["Observe"] | + .metadata.annotations."crossplane.io/external-name" = "AcTiOnS"' \ + "$work/clean.yaml" >"$work/imported.yaml" + check 1 "$work/imported.yaml" + grep -Fq 'retired catalogue is still managed' "$work/err" || + fail "$kind external identity was refused for the wrong reason" + yq '.metadata.annotations."crossplane.io/external-name" = ".github"' \ + "$work/imported.yaml" >"$work/other-import.yaml" + check 0 "$work/other-import.yaml" +done +# TeamRepository's provider identity is team ID/slug followed by the repository. +for identity in 12345:AcTiOnS maintainers:AcTiOnS; do + IDENTITY="$identity" yq '.kind = "TeamRepository" | .spec.forProvider = {} | + .spec.managementPolicies = ["Observe"] | + .metadata.annotations."crossplane.io/external-name" = strenv(IDENTITY)' \ + "$work/clean.yaml" >"$work/imported-team.yaml" + check 1 "$work/imported-team.yaml" +done +for identity in 12345:.github actions:.github; do + IDENTITY="$identity" yq '.metadata.annotations."crossplane.io/external-name" = strenv(IDENTITY)' \ + "$work/imported-team.yaml" >"$work/other-team.yaml" + check 0 "$work/other-team.yaml" +done printf 'not: [valid\n' >"$work/invalid.yaml" check 2 "$work/invalid.yaml" : >"$work/empty.yaml" From f68c9f82608574db6df0cb2b8d596cb752797fc7 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Fri, 23 Oct 2026 00:55:35 +0200 Subject: [PATCH 4/4] fix(config): refuse truncated retirement observations --- scripts/validate-actions-retirement.sh | 6 +++++- tests/actions-retirement.sh | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/validate-actions-retirement.sh b/scripts/validate-actions-retirement.sh index 27e80c94..413823f0 100755 --- a/scripts/validate-actions-retirement.sh +++ b/scripts/validate-actions-retirement.sh @@ -15,7 +15,11 @@ fi if ! jq -se ' length > 0 and all(type == "object" and (.kind | type == "string" and length > 0) and - (.metadata.name | type == "string" and length > 0)) + (.metadata.name | type == "string" and length > 0) and + (if .kind == "Repository" or .kind == "IssueLabels" or + .kind == "RepositoryPermissions" or .kind == "TeamRepository" + then (.spec | type == "object") and (.spec.forProvider | type == "object") + else true end)) ' "$work/documents.json" >/dev/null; then abort 'rendered resource inventory is empty or malformed' fi diff --git a/tests/actions-retirement.sh b/tests/actions-retirement.sh index fc001ea5..0152399c 100755 --- a/tests/actions-retirement.sh +++ b/tests/actions-retirement.sh @@ -81,6 +81,18 @@ for identity in 12345:.github actions:.github; do "$work/imported-team.yaml" >"$work/other-team.yaml" check 0 "$work/other-team.yaml" done +# Syntactically valid but truncated managed resources are incomplete observations, +# not evidence that a repository is no longer targeted. Empty provider objects +# remain valid for the Observe-only import controls above. +for kind in Repository IssueLabels RepositoryPermissions TeamRepository; do + for mutation in 'del(.spec)' '.spec = null' '.spec = []' '.spec = "truncated"' \ + 'del(.spec.forProvider)' '.spec.forProvider = null' '.spec.forProvider = []' \ + '.spec.forProvider = "truncated"'; do + KIND="$kind" yq '.kind = strenv(KIND)' "$work/clean.yaml" | + yq "$mutation" >"$work/truncated.yaml" + check 2 "$work/truncated.yaml" + done +done printf 'not: [valid\n' >"$work/invalid.yaml" check 2 "$work/invalid.yaml" : >"$work/empty.yaml"