workflows: read protected owners and drift reviewers from deployment environments - #78
Merged
Merged
Conversation
…environments PROTECTED_OWNERS and DRIFT_REVIEWERS were the only pieces of deployment config still living as repository-level variables in the config repo, handed to the reusable workflows as workflow_call inputs. Everything else -- APP_ID, APP_NAME, WORKSPACE, app_private_key, tfc_token -- already comes from the deployment environment the job runs in. Both inputs defaulted to "", so a config repo that forgot to wire one got a green check and no enforcement at all: validate-org only warns, and the drift PR silently skips --add-reviewer. Reading the values from the environment removes that hand-off, and with it the fail-open. It also puts the protected-owner list behind the same gate as the rule it backs. validate-org already refuses to read its schema or its owner list from the config repo so that a pull request cannot weaken the rule it is validated against; the value itself now sits behind the plan environment's branch policy rather than in an open repository variable. The validate job was the only job in the repo outside any environment, so it joins terraform-plan in 'plan' rather than getting one of its own -- a dedicated environment would be auto-created empty on first use in config repos that had not set it up, reintroducing the silent fail-open. Breaking change for consumers: see the migration section in docs/workflows.md.
…ers` and `reviewers` inputs
pavlovic-ivan
previously approved these changes
Aug 27, 2026
…nfig exists An empty --protected-owners warned and returned 0, so a config repo that never wired the value got a green check and no enforcement. Moving the value behind a deployment environment does not fix that on its own: an unset variable still resolves to "" and lands in the same branch. The list is now required, but only when organisation/ config is actually present. An unconditional error would break config repos that manage repositories and nothing else, which TestValidateOrg_NoFilesAndNoProtected OwnersPasses exists to protect. A repo that has organisation config is exactly the one that had the variable set, so the narrower rule closes the hole without a regression. It is reported through the existing failures slice rather than returned early, so schema and cross-file errors are still collected and shown in the same run. Protected owner logins are no longer echoed. Actions logs on a public config repo are world-readable while repository variables are not, so printing the list widened its exposure rather than narrowing it -- the opposite of the reason for moving it behind the environment. The count is enough to confirm the value was read.
The guard around --add-reviewer skipped silently on an empty list, so a drift PR could open unassigned on an otherwise green run with nothing in the logs to say so. Now that the value comes from DRIFT_REVIEWERS on the schedule environment rather than from the caller, a missing variable lands in exactly that branch. Both this and the existing --add-reviewer failure path are raised as ::warning:: so they surface in the run summary instead of only in the log body.
Three problems with the table as written: APP_NAME was listed as required but nothing reads it -- it matched only that row in the whole repo. gh_token was missing, so a consumer following "must provide all of" exactly would fail on first run. Splitting the table also fixes a conflation: the variables are read from the environment by the jobs themselves, while the secrets are workflow_call secrets the caller passes in, and the two are not configured in the same place. The note on PROTECTED_OWNERS contradicted itself in consecutive sentences, claiming the check could not be silently disabled and then describing how it silently disabled itself. That is now accurate against the validation change. The warning about protection rules on plan is raised from IMPORTANT to WARNING and states the cost plainly: protection rules apply per job, so a consumer that had required reviewers on plan for terraform-plan now pays two approvals per pull request and sees no validation feedback until the first one lands.
pavlovic-ivan
approved these changes
Aug 27, 2026
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.
What
PROTECTED_OWNERSandDRIFT_REVIEWERSwere the only pieces of deployment config still living as repository-level variables in the config repo, handed to the reusable workflows asworkflow_callinputs. Everything else —APP_ID,WORKSPACE,app_private_key,tfc_token— already comes from the deployment environment the job runs in, or from the caller'ssecrets:block.Both are now read from the environment directly, and the inputs are gone.
tf-plan.yaml— thevalidatejob joinsterraform-planin theplanenvironment and readsvars.PROTECTED_OWNERS.drift-check.yaml— readsvars.DRIFT_REVIEWERSfrom thescheduleenvironment it already ran in.The composite actions (
validate-org-configs,drift-pr) keep their inputs unchanged; only what feeds them changed.Why
It puts the protected-owner list behind the same gate as the rule it backs.
validate-orgalready refuses to read its schema or its owner list from the config repo, so that a pull request cannot weaken the rule it is validated against. The value itself now sits behind theplanenvironment's branch policy rather than in an open repository variable.It removes a hand-off that could be dropped. The inputs meant every config repo had to remember to wire the value through its own caller. Reading it from the environment removes that step.
validatewas the only job in the repo outside any environment. It joinsplanrather than getting one of its own: a dedicated environment would be auto-created empty on first use in config repos that had not set it up.Two behaviour changes worth reviewing
An empty protected-owner list is now a failure, not a warning. Moving the value behind an environment does not close the fail-open on its own — an unset variable still resolves to
""and hit the same warn-and-exit-0 branch, so a repo that bumped the ref without wiring the variable would have got a green check and no enforcement.validate-orgnow fails instead, but only whenorganisation/config is actually present: an unconditional error would break config repos that manage repositories and nothing else. It is reported through the existing failures slice rather than returned early, so schema errors still surface in the same run.Protection rules on
plannow cost twice. They apply per job, andvalidateruns in this environment too. A consumer that had required reviewers onplanfor theterraform-planjob now needs two approvals per pull request, and the author sees no validation feedback until the first one lands. Documented as a[!WARNING]rather than silently inherited.Also: protected owner logins are no longer echoed to the logs. Actions logs on a public config repo are world-readable while repository variables are not, so printing the list widened its exposure rather than narrowing it. The count alone confirms the value was read.
Before merging
Both steps are additive and safe to do now — nothing reads the new variables until a config repo's pinned ref moves to a tag containing this PR. Doing them before the merge means there is no window in which the protected-owner check is off.
Needs admin on the config repo. Copy the current values across; do not retype them.
gr-oss-developers/github-configuration→ Settings → Environments →plan→ add variablePROTECTED_OWNERS, value copied from the existing repository variable of the same name.schedule→ add variableDRIFT_REVIEWERS, value copied from the existing repository variable of the same name.Leave the repository-level variables in place for now — the currently pinned ref still reads them through the inputs.
After merging
protected_owners:/reviewers:from thewith:blocks. Both must move together: a caller that still passes a removed input fails immediately withInvalid input.PROTECTED_OWNERSandDRIFT_REVIEWERS.Important
Step 3 is not just tidying — it is the verification. Environment variables take precedence over repository variables of the same name, so if the environment variable were missing,
vars.PROTECTED_OWNERSwould quietly fall back to the repository one and everything would still look fine. Deleting the repository variables is what proves the value is actually coming from the environment.Repository-level
WORKSPACE/TFC_TOKENare untouched — the decommissiondiscoverjob runs outside any environment and needs them.Verified end to end on
milos-org/github-configurationWith the caller no longer passing either input, and
plancarrying the samemaindeployment branch policy asgr-oss-developers:organisation/members.yamlwith the protected owner presentEnforcing 1 protected owner(s)→validate✅,terraform-plan✅protected owner … is missing from members.yaml→validate❌,terraform-planskippedvalidateunder theplanbranch policyworkflow_rundefault-branch contextgo test ./...andactionlintare clean.