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
18 changes: 18 additions & 0 deletions .changeset/loud-pandas-wander.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"apply-terraform-plan": patch
---

Don't fail the job when the plan-file cleanup PR can't be merged automatically.

The action opens a cleanup PR after a successful apply and merges it with the `GITHUB_TOKEN`, retrying five times before `exit 1`. On a repo whose base branch requires an approving review, that merge can never succeed — the token acts as the actions bot, and a bot cannot approve its own PR:

```
GraphQL: At least 1 approving review is required by reviewers with write access. (mergePullRequest)
X Pull request is not mergeable: the base branch policy prohibits the merge.
```

The apply itself has already succeeded at that point, so the job went red on a deploy that completed fine, and the real signal (`Apply complete! Resources: 22 added, 0 changed, 0 destroyed.`) was buried under a cleanup failure. The 2.2.4 release that introduced the cleanup PR assumed a base branch that "requires PRs but no approvals"; repos that also require a review have never had a green apply.

The retry loop now falls back to enabling auto-merge (a no-op where auto-merge is disabled) and emits a `::warning::` with the PR URL instead of exiting non-zero. An unmerged cleanup PR only leaves a stale plan pointer on the base branch, which the next run's staleness check already catches. A failing `terraform apply` still fails the job, unchanged.

Because an unmergeable cleanup PR no longer fails the job, reaching the "Delete uploaded plan from GCS" step no longer implies the pointer is gone from the base branch. That step is now gated on the merge step's new `merged` output, preserving the 3.0.0 invariant that the uploaded plan is deleted only once the cleanup PR has actually landed — otherwise the base branch would keep a pointer to a missing object and every re-run would hard-fail at download.
32 changes: 25 additions & 7 deletions actions/apply-terraform-plan/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,34 +228,52 @@ runs:
${{ inputs.terraform_path }}/${{ inputs.plan_text_path }}

- name: Merge plan-file cleanup PR
id: merge_cleanup
if: steps.cleanup_pr.outputs.pull-request-number != ''
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github_token }}
PR_NUMBER: ${{ steps.cleanup_pr.outputs.pull-request-number }}
PR_URL: ${{ steps.cleanup_pr.outputs.pull-request-url }}
run: |
# GitHub needs a moment to compute mergeability after a PR is created,
# so retry a few times before giving up. Auto-merge is disabled on the
# repo, so we merge directly rather than with --auto.
# so retry a few times before giving up. We merge directly rather than
# with --auto because auto-merge is disabled on most consumer repos.
for attempt in 1 2 3 4 5; do
if gh pr merge "$PR_NUMBER" --squash --delete-branch; then
echo "Merged cleanup PR #$PR_NUMBER"
echo "merged=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Merge attempt $attempt failed; retrying in 5s..."
sleep 5
done
echo "Failed to merge cleanup PR #$PR_NUMBER after several attempts" >&2
exit 1
echo "merged=false" >> "$GITHUB_OUTPUT"
# If the base branch requires an approving review, this merge can never
# succeed: the GITHUB_TOKEN acts as the actions bot, and a bot cannot
# approve its own PR. The apply has already succeeded by this point, so
# failing here reports a completed deploy as a failed one and buries the
# real signal. Leave the PR for a human instead of exiting non-zero —
# the only cost of an unmerged cleanup PR is a stale plan pointer on the
# base branch, which the next run's staleness check catches anyway.
#
# Try auto-merge first so the PR lands by itself once its requirements
# are met; it's a no-op on repos where auto-merge is disabled.
if gh pr merge "$PR_NUMBER" --squash --delete-branch --auto; then
echo "Enabled auto-merge on cleanup PR #$PR_NUMBER; it will land once its requirements are met."
fi
echo "::warning::Terraform apply succeeded, but the plan-file cleanup PR could not be merged automatically: $PR_URL. Merge it to complete the apply cycle. If the base branch requires an approving review, the GITHUB_TOKEN cannot merge it unattended — enable auto-merge on the repo, or allow the actions bot to bypass the review requirement."

# Delete the uploaded binary plan only after the cleanup PR has merged.
# If the object were deleted first and the cleanup PR then failed to
# create or merge, the base branch would keep a pointer to a missing
# object and every re-run of this workflow would hard-fail at download
# until a new plan regenerates. (This step is skipped automatically if
# any earlier step failed.)
# until a new plan regenerates. That's why this is gated on the merge
# step's `merged` output and not merely on reaching this step: an
# unmergeable cleanup PR no longer fails the job, so "we got here" no
# longer implies "the pointer is gone from the base branch".
- name: Delete uploaded plan from GCS
if: steps.apply.outcome == 'success' && inputs.cleanup_plan_files == 'true' && inputs.auto_approve != 'true' && steps.download.outputs.object != ''
if: steps.apply.outcome == 'success' && inputs.cleanup_plan_files == 'true' && inputs.auto_approve != 'true' && steps.download.outputs.object != '' && steps.merge_cleanup.outputs.merged == 'true'
shell: bash
run: |
# Best-effort: a failure (e.g. a service account without delete
Expand Down
Loading