Skip to content

Commit 89cb9de

Browse files
claude[bot]claude
andauthored
Repair corrupted GroupMember state so deploys stop failing (#139)
* Repair corrupted GroupMember state so deploys stop failing (#133) Every Deploy to Production run since 2026-06-30 aborts about a minute in with "googleapi: Error 409: Member already exists" and merged changes (e.g. #134, #137) never apply. Two external-email GroupMember records in the maintainers group are recorded in state with an empty member ID ("groups/04f1mdlm38smb30/members/"), so each pulumi up plans a create-before-delete replace whose create conflicts with the live membership and fails the whole update. Five other records corrupted at the same time were already healed by the 2026-07-02 deploy run. Add a one-time TEMP block to the deploy workflow (same precedent as the auth-maintainers repair and the experimental-ext-tasks state delete): drop the two broken records, then re-import each membership by member email so the provider records the numeric member ID. All commands are || true-guarded, and the block must be removed after one successful deploy. Fixes #133 Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NazA3QKftRn72tdPG5vrpb * Widen state repair to all seven corrupted GroupMember records (#133) * Replace per-URN state deletes with export surgery; verify healing post-deploy (#133) The committed repair used 'pulumi state delete <urn>' per record, but the failed create-before-delete replaces left duplicate same-URN entries (a live record plus a pending-delete leftover) for some of the seven corrupted GroupMember records. 'pulumi state delete' fails on an ambiguous URN, and the '|| true' guard hid that failure, making the repair a silent no-op. - Replace the per-URN deletes with state surgery: export the stack, drop every entry (including pending-delete duplicates) whose URN is one of the seven with jq, and re-import. Unguarded, so a surgery failure is loud; skips the import when nothing matches (idempotent). - Keep the seven imports-by-email ('|| true' so a not-currently-live membership falls back to a create instead of blocking the deploy). - Add a post-deploy verification step that fails if any GroupMember record still has an empty member ID or a duplicate URN. The pinned provider (SamuZad/googleworkspace 0.11.1) writes an empty member ID even on successful creates (fixed upstream in 0.11.2), so a green 'make up' alone does not prove healing; the TEMP block must only be removed after this verification step passes. - Correct the root-cause comment: Google-side membership removals -> refresh dropped the records -> re-create -> provider create bug wrote empty IDs. * Bump googleworkspace provider 0.11.1 -> 0.11.2 (#133) 0.11.2 carries upstream commit "handle edge case with group memberships" (SamuZad/terraform-provider-googleworkspace, 2026-03-12), which fixes the create-path bug that stores an empty member ID for external-email group members — the bug that corrupted the seven records repaired by the TEMP block in deploy.yml and that would re-corrupt any record restored via the fallback create path. CI regenerates the SDK from this pin via 'pulumi install' and typechecks against it. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bcfc49f commit 89cb9de

2 files changed

Lines changed: 108 additions & 1 deletion

File tree

.github/workflows/deploy.yml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,111 @@ jobs:
7676
pulumi config set discord:guildId "$DISCORD_GUILD_ID" --stack prod
7777
pulumi config set discord:botToken "$DISCORD_BOT_TOKEN" --secret --stack prod
7878
pulumi config set githubBillingEmail "$ORG_BILLING_EMAIL" --secret --stack prod
79+
# TEMP (#133): one-time state surgery for seven corrupted GroupMember
80+
# records. Root cause chain: between 2026-06-30 and 2026-07-02 the seven
81+
# external-email memberships below were removed on the Google side
82+
# (actor unknown); the deploy's refresh then dropped their records from
83+
# state; the subsequent up re-created them; and the pinned provider
84+
# (SamuZad/googleworkspace 0.11.1) has a create-path bug that stores an
85+
# empty member ID ("groups/<groupId>/members/") even on successful
86+
# creates (fixed upstream in 0.11.2). The broken ID is immutable, so
87+
# every deploy since plans a create-before-delete replace whose create
88+
# step 409s ("Member already exists") against the live membership and
89+
# aborts the whole update. The failed replaces also left DUPLICATE
90+
# same-URN entries in state (a live record plus a pending-delete
91+
# '"delete": true' leftover) for some of the seven, and
92+
# `pulumi state delete <urn>` FAILS on an ambiguous URN — which is why
93+
# this block edits an exported copy of the state instead of deleting
94+
# per URN.
95+
#
96+
# Step 1: export the state, drop EVERY entry (including pending-delete
97+
# duplicates) whose URN is one of the seven, and re-import. These
98+
# commands are deliberately NOT `|| true`-guarded: if the surgery
99+
# fails, we want a loud failure, not a silent no-op. If nothing
100+
# matches (state already repaired), the import is skipped and the
101+
# deploy proceeds — the block is idempotent.
102+
GROUPMEMBER_URNS='[
103+
"urn:pulumi:prod::mcp-access::googleworkspace:index/groupMember:GroupMember::adamj@anthropic.com-catch-all",
104+
"urn:pulumi:prod::mcp-access::googleworkspace:index/groupMember:GroupMember::bob.dickinson@gmail.com-maintainers",
105+
"urn:pulumi:prod::mcp-access::googleworkspace:index/groupMember:GroupMember::bob.dickinson@gmail.com-registry-wg",
106+
"urn:pulumi:prod::mcp-access::googleworkspace:index/groupMember:GroupMember::chugh.tapan@gmail.com-maintainers",
107+
"urn:pulumi:prod::mcp-access::googleworkspace:index/groupMember:GroupMember::davideramian@anthropic.com-antitrust",
108+
"urn:pulumi:prod::mcp-access::googleworkspace:index/groupMember:GroupMember::davidsp@anthropic.com-antitrust",
109+
"urn:pulumi:prod::mcp-access::googleworkspace:index/groupMember:GroupMember::mattsamuels@anthropic.com-antitrust"
110+
]'
111+
pulumi stack export --stack prod --file /tmp/state.json
112+
jq --argjson urns "$GROUPMEMBER_URNS" \
113+
'.deployment.resources |= map(select(.urn as $u | ($urns | index($u)) | not))' \
114+
/tmp/state.json > /tmp/state-repaired.json
115+
before=$(jq '.deployment.resources | length' /tmp/state.json)
116+
after=$(jq '.deployment.resources | length' /tmp/state-repaired.json)
117+
echo "State surgery: removing $((before - after)) corrupted GroupMember entries"
118+
if [ "$before" -eq "$after" ]; then
119+
echo "No matching entries in state (already repaired); skipping import"
120+
else
121+
pulumi stack import --stack prod --file /tmp/state-repaired.json
122+
fi
123+
# Step 2: re-import the live memberships by member email (the
124+
# Directory API accepts an email as the member key, and the provider
125+
# then stores the real member ID). `|| true` is deliberate here: if a
126+
# membership is not currently live, its import fails harmlessly and
127+
# the subsequent up creates it, restoring access. Note that on the
128+
# 0.11.1 provider such a fallback create re-corrupts the record's
129+
# member ID — the "Verify GroupMember state healed" step after the
130+
# deploy catches that instead of letting a green run masquerade as
131+
# healed. --protect=false keeps the imported records deletable, like
132+
# every other GroupMember.
133+
#
134+
# REMOVAL CRITERION: remove this block (and the verification step
135+
# below) only after the "Verify GroupMember state healed" step has
136+
# passed on a deploy — a green `make up` alone does NOT prove the
137+
# records are healed.
138+
pulumi import googleworkspace:index/groupMember:GroupMember 'adamj@anthropic.com-catch-all' 'groups/00upglbi31qapnv/members/adamj@anthropic.com' --stack prod --yes --protect=false || true
139+
pulumi import googleworkspace:index/groupMember:GroupMember 'bob.dickinson@gmail.com-maintainers' 'groups/04f1mdlm38smb30/members/bob.dickinson@gmail.com' --stack prod --yes --protect=false || true
140+
pulumi import googleworkspace:index/groupMember:GroupMember 'bob.dickinson@gmail.com-registry-wg' 'groups/040ew0vw3g472qb/members/bob.dickinson@gmail.com' --stack prod --yes --protect=false || true
141+
pulumi import googleworkspace:index/groupMember:GroupMember 'chugh.tapan@gmail.com-maintainers' 'groups/04f1mdlm38smb30/members/chugh.tapan@gmail.com' --stack prod --yes --protect=false || true
142+
pulumi import googleworkspace:index/groupMember:GroupMember 'davideramian@anthropic.com-antitrust' 'groups/03jtnz0s4hi6gld/members/davideramian@anthropic.com' --stack prod --yes --protect=false || true
143+
pulumi import googleworkspace:index/groupMember:GroupMember 'davidsp@anthropic.com-antitrust' 'groups/03jtnz0s4hi6gld/members/davidsp@anthropic.com' --stack prod --yes --protect=false || true
144+
pulumi import googleworkspace:index/groupMember:GroupMember 'mattsamuels@anthropic.com-antitrust' 'groups/03jtnz0s4hi6gld/members/mattsamuels@anthropic.com' --stack prod --yes --protect=false || true
79145
make up
146+
147+
# TEMP (#133): a green deploy alone does NOT prove the GroupMember records
148+
# are healed — the pinned provider (0.11.1) writes an empty member ID even
149+
# on successful creates, so the fallback create path in the repair block
150+
# above can silently re-corrupt state while the run stays green. This step
151+
# makes that visible: it fails loudly if any GroupMember record still has
152+
# an empty member ID or a duplicate URN. Remove the TEMP repair block in
153+
# the deploy step, and this step, only after this step has PASSED on a
154+
# deploy — not merely after a green run.
155+
- name: "Verify GroupMember state healed (TEMP, #133)"
156+
env:
157+
PULUMI_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
158+
run: |
159+
echo "$PULUMI_PASSPHRASE" > passphrase.prod.txt
160+
export PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt
161+
pulumi login gs://mcp-access-prod-pulumi-state
162+
pulumi stack export --stack prod --file /tmp/state-verify.json
163+
empty_ids=$(jq -r '[.deployment.resources[]
164+
| select(.type == "googleworkspace:index/groupMember:GroupMember")
165+
| select((.id // "") | endswith("/members/"))
166+
| .urn] | .[]' /tmp/state-verify.json)
167+
dup_urns=$(jq -r '[.deployment.resources[]
168+
| select(.type == "googleworkspace:index/groupMember:GroupMember")
169+
| .urn] | group_by(.) | map(select(length > 1) | .[0]) | .[]' /tmp/state-verify.json)
170+
status=0
171+
if [ -n "$empty_ids" ]; then
172+
echo "::error::GroupMember records still have an empty member ID (state NOT healed):"
173+
echo "$empty_ids"
174+
status=1
175+
fi
176+
if [ -n "$dup_urns" ]; then
177+
echo "::error::Duplicate GroupMember URNs remain in state (pending-delete leftovers):"
178+
echo "$dup_urns"
179+
status=1
180+
fi
181+
if [ "$status" -ne 0 ]; then
182+
echo "::error::Deploy succeeded but the state repair did NOT stick. Do not remove the TEMP repair block; see #133."
183+
exit 1
184+
fi
185+
echo "All GroupMember records have real member IDs and unique URNs; state is healed."
186+
echo "The TEMP repair block (and this step) can now be removed."

Pulumi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ packages:
77
version: 0.14.0
88
parameters:
99
- SamuZad/googleworkspace
10-
- 0.11.1
10+
- 0.11.2

0 commit comments

Comments
 (0)