From f6cb726fc38fc9fdffd4675a01b0c7f34153e17c Mon Sep 17 00:00:00 2001 From: Elior Erez Date: Mon, 3 Aug 2026 21:56:57 -0400 Subject: [PATCH 1/4] NO-ISSUE: Add one-time TF state rm support for unrefreshable resources Terraform's apply fails atomically on any resource whose live state can no longer be refreshed (e.g. github_membership/github_team_membership for a user who has left the org), blocking every other pending change in the same apply -- including the archived=true settings from #161. Mirrors the existing one-time TF import mechanism. --- .github/workflows/apply.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/apply.yaml b/.github/workflows/apply.yaml index 3f883a6..7400951 100644 --- a/.github/workflows/apply.yaml +++ b/.github/workflows/apply.yaml @@ -11,6 +11,10 @@ on: description: "One-time: import ID for the resource above (e.g. the repo name for github_repository)." required: false default: "" + state_rm_addresses: + description: "One-time: comma-separated Terraform resource addresses to remove from state (e.g. for a member who left the org and whose resource can no longer be refreshed). Leave empty for a normal apply." + required: false + default: "" schedule: - cron: "17 */4 * * *" push: @@ -64,6 +68,23 @@ jobs: tofu import "${{ inputs.import_address }}" "${{ inputs.import_id }}" env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} + # Handles a resource whose live state can no longer be refreshed (e.g. + # a github_membership/github_team_membership for a user who left the + # org, which errors on read instead of just reporting "gone") -- + # refresh failures like this abort the whole apply before anything + # else in the plan can land, atomically, even resources unrelated to + # the broken one. Manual, one-time use via workflow_dispatch input; a + # no-op (skipped entirely) for the normal scheduled/push triggers, + # which never set this input. + - name: TF State Remove (one-time, manual only) + if: inputs.state_rm_addresses != '' + run: | + IFS=',' read -ra ADDRS <<< "${{ inputs.state_rm_addresses }}" + for addr in "${ADDRS[@]}"; do + tofu state rm "$addr" + done + env: + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} - name: TF Apply run: | tofu apply -concise -auto-approve From 40fdc27aac86f4a48a3ca5af42c53afac9d2b31a Mon Sep 17 00:00:00 2001 From: Elior Erez Date: Wed, 5 Aug 2026 18:07:57 -0400 Subject: [PATCH 2/4] Address CodeRabbit review: concurrency, atomicity, injection pattern - Simplify the concurrency group to just github.workflow. This workflow has no pull_request trigger, so github.event.pull_request.number was always null and the group key collapsed to github.ref -- meaning a workflow_dispatch run against a non-default ref could run concurrently with a scheduled/push apply against main, racing on the same remote state. A single, unqualified group serializes every run of this workflow against every other, regardless of ref or trigger. - Pass all parsed addresses to a single `tofu state rm` invocation instead of looping one call per address, so the state write happens once instead of N times. - Pass state_rm_addresses through env + a quoted shell variable instead of interpolating the workflow expression directly into the script. Signed-off-by: Elior Erez --- .github/workflows/apply.yaml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/apply.yaml b/.github/workflows/apply.yaml index 7400951..4f7fcad 100644 --- a/.github/workflows/apply.yaml +++ b/.github/workflows/apply.yaml @@ -25,7 +25,15 @@ on: - main concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # This workflow has no pull_request trigger, so github.event.pull_request.number + # is always null here -- the group key collapsed to just github.ref in + # practice, which only serializes runs against the same ref. A manual + # workflow_dispatch run (e.g. one-time state rm) dispatched against a + # non-default ref could then run concurrently with a scheduled/push apply + # against main, racing on the same remote state. Use a single, unqualified + # group so every run of this workflow -- manual or automatic, any ref -- + # is always serialized against every other. + group: ${{ github.workflow }} cancel-in-progress: false jobs: @@ -79,12 +87,11 @@ jobs: - name: TF State Remove (one-time, manual only) if: inputs.state_rm_addresses != '' run: | - IFS=',' read -ra ADDRS <<< "${{ inputs.state_rm_addresses }}" - for addr in "${ADDRS[@]}"; do - tofu state rm "$addr" - done + IFS=',' read -ra ADDRS <<< "${STATE_RM_ADDRESSES}" + tofu state rm "${ADDRS[@]}" env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} + STATE_RM_ADDRESSES: ${{ inputs.state_rm_addresses }} - name: TF Apply run: | tofu apply -concise -auto-approve From 165e4144ddc0a7ef34d6989172fd0690419413a0 Mon Sep 17 00:00:00 2001 From: Elior Erez Date: Wed, 5 Aug 2026 18:19:00 -0400 Subject: [PATCH 3/4] Address second CodeRabbit review round - queue: max on the concurrency group -- default queue behavior only keeps the single most-recently-queued run pending and cancels older ones, so a manual one-time recovery run (state rm, or the exclude_addresses escape hatch added in a follow-up PR) dispatched while a scheduled/push run is in progress could get silently dropped and replaced before it ever executes. - Reject state_rm_addresses containing a newline before parsing -- `read` without -a per-field handling would otherwise silently truncate at the first newline and drop later addresses. (The already-present `read -ra` already implies -r; CodeRabbit's specific claim that -r was missing didn't hold up against the actual committed content, verified via `git show`.) - Drop the unused GITHUB_TOKEN from the TF State Remove step -- `tofu state rm` is a pure state-file operation against the S3/AWS backend and never calls the GitHub provider API. Signed-off-by: Elior Erez --- .github/workflows/apply.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/apply.yaml b/.github/workflows/apply.yaml index 4f7fcad..a2ec6cf 100644 --- a/.github/workflows/apply.yaml +++ b/.github/workflows/apply.yaml @@ -35,6 +35,14 @@ concurrency: # is always serialized against every other. group: ${{ github.workflow }} cancel-in-progress: false + # Default queue behavior only keeps the single most-recently-queued run + # pending in a group -- an older pending run gets canceled and replaced. + # A manual one-time recovery run (state rm, or the exclude_addresses + # escape hatch) dispatched while a scheduled/push run is in progress + # could get silently dropped and replaced by the next automatic trigger + # before it ever executes. queue: max keeps every pending run queued + # (up to GitHub's cap of 100) instead of dropping older ones. + queue: max jobs: apply: @@ -87,10 +95,13 @@ jobs: - name: TF State Remove (one-time, manual only) if: inputs.state_rm_addresses != '' run: | + if [[ "${STATE_RM_ADDRESSES}" == *$'\n'* ]]; then + echo "::error::state_rm_addresses must be comma-separated on a single line, not newline-separated." >&2 + exit 1 + fi IFS=',' read -ra ADDRS <<< "${STATE_RM_ADDRESSES}" tofu state rm "${ADDRS[@]}" env: - GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} STATE_RM_ADDRESSES: ${{ inputs.state_rm_addresses }} - name: TF Apply run: | From ca3c7027602888857b5b3ae15fc84f9999be3a7f Mon Sep 17 00:00:00 2001 From: Elior Erez Date: Wed, 5 Aug 2026 18:29:22 -0400 Subject: [PATCH 4/4] Reject option-like/empty resource addresses in state_rm_addresses A stray comma or a typo'd leading "-" would otherwise be passed straight to `tofu state rm`, which parses it as a CLI option rather than a resource address (e.g. "-dry-run,github_membership.example"). Validate each token before running the command. Signed-off-by: Elior Erez --- .github/workflows/apply.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/apply.yaml b/.github/workflows/apply.yaml index a2ec6cf..58a83d9 100644 --- a/.github/workflows/apply.yaml +++ b/.github/workflows/apply.yaml @@ -100,6 +100,12 @@ jobs: exit 1 fi IFS=',' read -ra ADDRS <<< "${STATE_RM_ADDRESSES}" + for addr in "${ADDRS[@]}"; do + if [[ -z "${addr}" || "${addr}" == -* ]]; then + echo "::error::invalid resource address '${addr}' -- addresses must be non-empty and cannot start with '-' (tofu would parse it as an option)." >&2 + exit 1 + fi + done tofu state rm "${ADDRS[@]}" env: STATE_RM_ADDRESSES: ${{ inputs.state_rm_addresses }}