Command reference¶
Commands are comments on a pull request.
| Comment | Effect |
|---|---|
/plan |
Plan all changed units (re-plan). |
/plan -p <label> |
Plan one unit and its dependents. |
/apply |
Apply all changed units in the PR, in dependency order. |
/apply -p <label> |
Apply exactly one unit, no dependents. |
/apply --force (or -f) |
Apply even when a unit's plan changed since it was reviewed, and run every unit for real instead of skipping no-ops. |
/unlock |
Force-release the unit locks this PR holds. |
/help |
List the commands, as a reply on the PR. |
Selecting a unit¶
<label> after -p (or --project, or as a bare argument) matches a unit by:
- its full-path label (for example
live/landingzones/corp/corp-demo-dev/westeurope/example), - its name (the leaf folder), or
- its directory.
A plan with changes tells you the exact label to use in its instructions block.
The flag is spelled --project because that is the engine's word for a unit; the
two mean the same thing here.
A selector that matches no unit gets a reply saying so, with a 😕 reaction on the command comment, instead of a silently skipped run.
A filtered run is a diagnostic: it never writes the merge gate and it skips
the unattributed-path check, so /plan -p cannot turn the gate green or red.
The full /plan or /apply is what moves the gate.
Who can run a command¶
Every command must come from a comment whose GitHub author_association is
OWNER, MEMBER, or COLLABORATOR. Any other author, for example
CONTRIBUTOR or NONE, is refused: the engine adds a 😕 reaction to the comment
and posts a reply naming the author association it saw.
Since v1.9.2, /apply and /unlock must also clear a real permission check. The
engine reads the commenter's permission on the repository and requires write or
admin; maintain reports as write and passes, triage reports as read and
does not. A refusal names the permission the command needs and the one the account
has. /plan is deliberately left on the association filter: it reads rather than
writes, and requiring write would stop a read collaborator asking for a re-plan on
their own pull request.
This is the first of the controls on the flow. It answers "who is asking". The approval gate below reads a property of the pull request, not of the person typing, so without this check any user who can comment on the PR could drive a real apply.
Association on its own is not a permission check
author_association is what GitHub reports on the comment, not the author's
permission level on the repository. MEMBER means "a member of the
organization that owns the repository", which in an organization with default
read access can include people with no write permission. That is exactly why
the state-changing commands no longer rely on it alone.
The permission check fails closed: if the API cannot be read, the command is refused rather than allowed.
Both checks only exist on the comment path. Runs from pull_request events,
workflow_dispatch, and workflow_call never see an author_association, so
they are not filtered by it.
Automatic runs¶
- Opening, updating, or reopening a PR runs
/planon the changed units. - Closing or merging a PR releases the unit locks it holds. See Cross-PR unit locks.
- The ops workflow also declares a manual
workflow_dispatchwith acommandchoice (plan,apply, orunlock), an optionalprojectinput, and apr_number. (Four further inputs are set by the engine when the PR caller hands work over; never set them by hand.)
Manual dispatch drives a pull request
A dispatch without pr_number is refused at parse with a message saying
so: every plan, apply, and unlock belongs to a pull request. For a full
plan of the default branch, dispatch drift.yml instead; the drift sweep
never needs a PR. The unlock command with a PR number is the manual
lock release for a closed PR whose close event produced no run.
What each run posts¶
- A fresh run-report comment. The superseded report of the same unit and
command is minimized with an Outdated banner (opt out with
TFPR_MINIMIZE_OUTDATED=false), so the newest report is the one that reads. - A per-unit check row,
tf-pr-ops / <command> (<label>) #<run>(a spinner while running, then a green check or red cross); the superseded row turns neutral, titledOutdated: superseded by #N. - An update to the
tf-pr-ops / merge-gatecheck run. A filtered-prun is the exception: it leaves the gate untouched. - An update to the
tf-pr-ops / approvalcheck. - A 👀 reaction on the command comment, confirming it was picked up. That is the only reaction an accepted command gets; the outcome is in the report comment, the per-unit check, and the merge gate. A refused command gets 😕 instead. See Comment reactions.
The approval check¶
Every run sets a tf-pr-ops / approval check run from GitHub's review
decision, so a required-approval rule is visible from the first plan rather than
only when an apply is refused. It reads:
| Review decision | State | Description |
|---|---|---|
APPROVED |
success | Required reviews met |
empty, with TFPR_ALLOW_UNREVIEWED_APPLY set to true |
success | No reviews required; unreviewed apply allowed |
| empty, otherwise | failure | No reviews required by this repository; apply is blocked |
| anything else | failure | Required reviews not met (<decision>) |
An empty review decision means the repository requires no reviews at all. The
engine refuses that rather than treating it as consent: an empty decision cannot
be told apart from a ruleset that was removed, so allowing it would let the apply
gate disappear with no signal. A repository that requires no reviews on purpose
opts back in with the TFPR_ALLOW_UNREVIEWED_APPLY repository variable set
to true. Leave it unset everywhere else. See
Secrets and variables.
Never make tf-pr-ops / approval a required check
It is informational. It blocks nothing on its own, and it reports failure on
every PR until someone approves. Make it required and you deadlock the
repository: the status only clears on approval, but the branch rule already
enforces approval, so the check adds nothing and can wedge merges. The gate you
do make required is tf-pr-ops / merge-gate.
/apply is gated separately, on the same review decision. If the PR does not meet
the required reviews, the engine posts a comment saying apply is blocked and does
not run.
Template repositories are skipped¶
Both workflows exit before resolve when the caller repository is a GitHub
template repository. A template holds no infrastructure and none of the
bootstrap-created variables or secrets, so plan and apply cannot work there, and
skipping keeps its own pull requests green. A repository generated from a
template is not itself a template, so a customer repo never hits this.