Skip to content

workflows: read protected owners and drift reviewers from deployment environments - #78

Merged
dev-milos merged 5 commits into
mainfrom
feature/gcss-env-scoped-deployment-vars
Aug 27, 2026
Merged

workflows: read protected owners and drift reviewers from deployment environments#78
dev-milos merged 5 commits into
mainfrom
feature/gcss-env-scoped-deployment-vars

Conversation

@dev-milos

@dev-milos dev-milos commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

What

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, WORKSPACE, app_private_key, tfc_token — already comes from the deployment environment the job runs in, or from the caller's secrets: block.

Both are now read from the environment directly, and the inputs are gone.

  • tf-plan.yaml — the validate job joins terraform-plan in the plan environment and reads vars.PROTECTED_OWNERS.
  • drift-check.yaml — reads vars.DRIFT_REVIEWERS from the schedule environment 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-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.

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.

validate was the only job in the repo outside any environment. It joins 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.

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-org now fails instead, but only when organisation/ 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 plan now cost twice. They apply per job, and validate runs in this environment too. A consumer that had required reviewers on plan for the terraform-plan job 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 variable PROTECTED_OWNERS, value copied from the existing repository variable of the same name.
  • Same repo → Settings → Environments → schedule → add variable DRIFT_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

  1. Tag a release containing this PR.
  2. In the config repo, one commit that bumps the pinned ref and drops protected_owners: / reviewers: from the with: blocks. Both must move together: a caller that still passes a removed input fails immediately with Invalid input.
  3. Once a run succeeds, delete the repository-level PROTECTED_OWNERS and DRIFT_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_OWNERS would 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_TOKEN are untouched — the decommission discover job runs outside any environment and needs them.

Verified end to end on milos-org/github-configuration

With the caller no longer passing either input, and plan carrying the same main deployment branch policy as gr-oss-developers:

Test Result
organisation/members.yaml with the protected owner present Enforcing 1 protected owner(s)validate ✅, terraform-plan
protected owner absent protected owner … is missing from members.yamlvalidate ❌, terraform-plan skipped
validate under the plan branch policy ✅ — no approval wait, policy satisfied via the workflow_run default-branch context
drift check drift PR opened with the reviewer requested

go test ./... and actionlint are clean.

…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.
pavlovic-ivan
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.
@dev-milos
dev-milos merged commit 464f59d into main Aug 27, 2026
1 check passed
@dev-milos
dev-milos deleted the feature/gcss-env-scoped-deployment-vars branch August 27, 2026 11:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants