Guard repository destroys instead of blocking them outright - #95
Merged
Conversation
A Terraform 'delete' of github_repository is not necessarily a deletion.
archive_on_destroy defaults to TRUE, and the provider's delete path sets
archived=true and calls Edit rather than Delete - confirmed in the provider
source. So removing a definition ARCHIVES the repository, and the archive-then-
delete-in-UI retirement path already existed.
prevent_destroy is therefore NOT added: it would block the archive too,
replacing a working retirement path with a hard plan error. It also cannot help
decide the interesting cases, being a lifecycle meta-argument that must be a
literal and cannot read the before-state.
The plan shows 'delete' for both an archive and a real deletion, so the plan
alone cannot tell them apart. The guard reads the before-state and splits them:
REFUSED archive_on_destroy = false the only irreversible path. Nothing
sets it today.
REFUSED repository not already archived archiving as a SIDE EFFECT of a
definition going missing - a bug
signature, not a retirement.
ALLOWED already-archived repo leaving provider no-ops, state forgets it.
The last step of a deliberate
retirement; blocking it would make
retirement impossible.
REPORTED non-repository destroys rulesets and environments churn
legitimately, but a destroy should
never scroll past unread.
The premature-archive rule is the one that matters. On the-hero-wars-guys a
dotfile-blind glob dropped a definitions file and the plan proposed to archive
.github - archived=false, public, the org profile repo. Archiving it would have
made it read-only and stopped its Actions org-wide.
tools/test_destroy_guard.py extracts the guard's python from the workflow and
runs it, so the tested code is the shipped code rather than a copy that can
drift. 12 tests including the real incident plan shape, plus two edge cases that
decide ambiguity in opposite directions: an absent archive_on_destroy falls to
the provider default (allow), a null before-state is unknown (refuse).
Gate: 12/12 destroy-guard, assemble-guard passing, 93/93 terraform, fmt clean.
NWarila
force-pushed
the
feat/never-destroy-repositories
branch
from
July 20, 2026 18:41
ac96824 to
d1e816a
Compare
2 tasks
NWarila
added a commit
to nwarila-platform/github-terraform-runner
that referenced
this pull request
Jul 20, 2026
Picks up the two fixes made while working on the-hero-wars-guys. This runner is currently **two commits behind** and has **no destroy guard** — while holding org-admin credentials for 21 repositories. | PR | Fix | |---|---| | nwarila-platform/github-terraform-framework#94 | assembly guard used `compgen -G`, which skips dotfiles — a definitions directory containing only dotfiles read as empty and its repositories silently stopped being declared | | nwarila-platform/github-terraform-framework#95 | destroy guard: refuses genuine deletion (`archive_on_destroy=false`) and refuses archiving a repository that was not deliberately archived first, while still allowing a deliberate retirement to complete | ## Was this runner exposed to #94? Not in practice — `terraform/public/` holds 19 non-dotfile YAMLs, so the guard passed. It was closer than that sounds. `terraform/private/` contains **only `.gitkeep`**, so it reads as empty under the old guard, and it is safe only because the S3 fetch populates that directory *before* assembly runs. A change to that step ordering, or moving the last public definition, would have exposed it. ## Acceptance - [ ] Real-state plan on merge is unchanged — expected `No changes`, since this is CI-only - [ ] Destroy guard reports "No repository destroys in this plan"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Terraform
deleteofgithub_repositoryis not necessarily a deletion.archive_on_destroydefaults totrue, and the provider's delete path setsarchived = trueand callsEditrather thanDelete— confirmed in the provider source:So removing a definition archives the repository. The archive-then-delete-in-UI retirement path already existed.
Why
prevent_destroyis not usedIt would block the archive too — replacing a working retirement path with a hard plan error. It also can't help decide the interesting cases: it's a
lifecyclemeta-argument, must be a literal, and cannot read the before-state.What the guard does
The plan shows
deletefor both an archive and a real deletion, so the plan alone can't distinguish them. The guard reads the before-state and splits the cases:archive_on_destroy = falseThe case that matters in practice
On
the-hero-wars-guys, a dotfile-blind glob (fixed in #94) dropped a definitions file and the plan proposed to archive.github—archived=false, public, the org profile repo. Archiving it makes it read-only and stops its Actions org-wide. Reversible, but an outage during a ramp.That's the second rule, and it costs nothing: it only insists an archive be deliberate rather than a consequence of a file going missing.
Correction to this PR's earlier revision
The first version added
prevent_destroyand described the incident as a repository deletion with no safety net. Both were wrong —archive_on_destroywas already the default, so it was an archive, and it was already a safety net.Testing
tools/test_destroy_guard.pyextracts the guard's Python from the workflow and executes it, so the tested code is the shipped code rather than a copy that can drift. 12 tests, including the real incident plan shape.Two edge cases decide which way ambiguity falls, deliberately in opposite directions:
archive_on_destroy→ provider default applies → allow (assuming deletion would block ordinary retirements)nullbefore-state → archived state unknown → refuseGate: 12/12 guard tests, 93/93 terraform tests,
fmtandvalidateclean.