Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6146,6 +6146,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

Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 0 additions & 32 deletions deploy/labels/actions.yaml

This file was deleted.

1 change: 0 additions & 1 deletion deploy/labels/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ resources:
- go-template.yaml
- dotnet-template.yaml
- homebrew-tap.yaml
- actions.yaml
- cloudflare.yaml
- khoist.yaml
- business-site.yaml
Expand Down
27 changes: 0 additions & 27 deletions deploy/repository-permissions/actions.yaml

This file was deleted.

1 change: 0 additions & 1 deletion deploy/repository-permissions/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 0 additions & 18 deletions deploy/team-repositories/grant-admins-on-actions.yaml

This file was deleted.

20 changes: 0 additions & 20 deletions deploy/team-repositories/grant-maintainers-on-actions.yaml

This file was deleted.

2 changes: 0 additions & 2 deletions deploy/team-repositories/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
52 changes: 52 additions & 0 deletions scripts/validate-actions-retirement.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/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
# 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'
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) 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
# 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] | 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'
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'
115 changes: 115 additions & 0 deletions tests/actions-retirement.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/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 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=$?
[[ "$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" <<YAML
---
apiVersion: repo.github.m.upbound.io/v1alpha1
kind: $kind
metadata:
name: restored-retirement
spec:
forProvider:
name: actions
repository: actions
YAML
check 1 "$work/retired.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"
# 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
# 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"
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, case/adopted identities and failed reads\n'
16 changes: 8 additions & 8 deletions tests/admin-team-policy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,24 @@ 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=(
"$repo_root/deploy/teams/admins.yaml"
"$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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading