Skip to content

feat(invitation): emit ResourceDoesNotExist marker on confirmed-absent delete (SPEC-09a)#180

Draft
c1-squire-dev[bot] wants to merge 2 commits into
mainfrom
paul.querna/nhi/resource-delete-idempotency-marker
Draft

feat(invitation): emit ResourceDoesNotExist marker on confirmed-absent delete (SPEC-09a)#180
c1-squire-dev[bot] wants to merge 2 commits into
mainfrom
paul.querna/nhi/resource-delete-idempotency-marker

Conversation

@c1-squire-dev

@c1-squire-dev c1-squire-dev Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What

Make the invitation ResourceDeleter emit the typed c1.connector.v2.ResourceDoesNotExist{} marker (with nil error) when the provider authoritatively confirms the addressed invitation is already absent, instead of returning an untyped success.

This is the second connector producer of the SPEC-09a already-absent delete outcome (after baton-aws#140), and the first non-AWS one.

Why

Per pqprime SPEC-09a (REQ-VPQ-009A) and conceptually the baton-sdk contract in baton-sdk#1033: a ResourceDeleter(V2) whose provider call authoritatively confirms the correctly-addressed resource is already gone should return annotations.New(&v2.ResourceDoesNotExist{}), nil — the resource-plane analogue of GrantAlreadyRevoked.

This lets C1 distinguish a confirmed already-gone delete (typed success ⇒ REVOKED) from an opaque / mis-addressed NotFound (untyped error ⇒ REVOKE_UNKNOWN), so a delete retried after a crash is classified correctly rather than ambiguously. There is no code dependency on #1033 — the marker type is already vendored in baton-sdk v0.19.1; this PR is independently mergeable.

Change

pkg/connector/invitation.goinvitationResourceType.Delete already treated a CancelInvite 404 as success (desired state achieved). The only new behavior: the marker is appended solely on the pure-absence success path — i.e. no active cancellation succeeded across the configured orgs, yet the invitation is considered removed because a well-formed invitation id returned 404 against a configured org.

  • Ordinary success (an active CancelInvite succeeded) remains success and carries no marker.
  • Every other provider/addressing error is unchanged: wrong resource type, unparseable invitation id, and the "failed to cancel invite" path all still return errors. No new NotFound is swallowed and no error path is broadened.
  • The multi-org loop is handled order-independently via a wasCancelled flag, so a real cancellation in one org is never masked by a 404 in another.

Intentionally excluded: the user deleter

userResourceType.Delete currently surfaces 404 as an error (wrapGitHubError, on both Users.GetByID and RemoveOrgMembership) and has no existing already-absent success branch. Adding the marker there would broaden behavior, which SPEC-09a forbids for this step; it is left for a separate, deliberate change.

No dependency churn

No go.mod / SDK bump. The resource.ResourceDoesNotExistAnnotations() helper from #1033 is unreleased (absent from vendored v0.19.1), so the marker is constructed directly (annotations.New/Append + &v2.ResourceDoesNotExist{}) — both aliases were already imported. A code comment notes swapping to the helper after a future SDK bump. No version files touched.

Tests

New pkg/connector/invitation_delete_test.go (go-github-mock inline harness, matching invitation_test.go):

  • confirmed absence (404) → annotations Contains(ResourceDoesNotExist), nil error
  • ordinary success (204) → no marker, nil error
  • other error (500) → error, nil annotations
  • exact addressing preserved → DELETE /orgs/test-org-12/invitations/5551212
  • malformed invitation id → error before any API call
  • wrong resource type → error

Verified: gofmt clean, go build ./..., go test -race ./pkg/connector/ green (focused + full package, no regressions), golangci-lint adds zero new issues.


Draft: opened as part of the SPEC-09a connector rollout for review before merge.

🤖 Generated with Claude Code

…t delete

The invitation ResourceDeleter already treats a provider 404 on CancelInvite
as success (desired state achieved). Per SPEC-09a (REQ-VPQ-009A), a deleter
that authoritatively confirms the correctly-addressed resource is already
absent should return the typed c1.connector.v2.ResourceDoesNotExist{} marker
(nil error) instead of an untyped success, so a delete retried after a crash
is classified as REVOKED rather than an ambiguous NotFound.

Only the confirmed-absence branch changes: the marker is emitted solely when
no active cancellation succeeded yet the invitation is considered removed
(a 404 for a well-formed invitation id against a configured org). An active
cancellation remains ordinary success and carries no marker; every other
provider/addressing error is unchanged.

The user deleter is intentionally excluded: it surfaces 404 as an error
(wrapGitHubError) and has no existing already-absent success branch, so adding
the marker there would broaden behavior.

No go.mod/SDK bump: the marker type is already vendored (baton-sdk v0.19.1);
the resource.ResourceDoesNotExistAnnotations() helper from baton-sdk#1033 is
unreleased, so the marker is constructed directly with a note to adopt the
helper after a future SDK bump.

Tests: new invitation_delete_test.go covers confirmed-absence -> marker,
ordinary success -> no marker, other error unchanged, exact org/invitation-id
addressing preserved, and malformed/wrong-type inputs.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/connector/invitation.go Outdated
Comment on lines +370 to +381
if !wasCancelled {
// No active cancellation succeeded, yet the invitation is considered
// removed: the provider authoritatively reported it already absent
// (404) for a well-formed invitation id against a configured org.
// Emit the typed already-absent marker so a delete retried after a
// crash is classified as success rather than an ambiguous NotFound
// (SPEC-09a; baton-sdk#1033). An active cancellation is ordinary
// success and intentionally carries no marker. A future baton-sdk
// bump can replace this direct construction with
// resource.ResourceDoesNotExistAnnotations().
annotations.Append(&v2.ResourceDoesNotExist{})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: In a multi-org config, if one org returns 404 (isRemoved=true, wasCancelled stays false) while another returns a non-404 real error (e.g. 500), the loop still exits with isRemoved=true and emits ResourceDoesNotExist. That overstates the "authoritatively confirmed absent" semantics since one org's state was never determined. Low impact (invitation IDs are org-scoped and single-org is typical, and the pre-existing loop already ignored that 500), but you may want to only emit the marker when no non-404 error occurred across orgs. (confidence: low)

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Connector PR Review: feat(invitation): emit ResourceDoesNotExist marker on confirmed-absent delete (SPEC-09a)

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: loaded .claude/skills/ci-review.md from trusted base 7b8e756
Review mode: incremental since e8e4b72
View review run

Review Summary
The full PR diff was scanned for security and correctness. The new commit addresses the prior finding: invitation.go now tracks a hadRealError flag so that in a multi-org delete loop, a 404 in one org combined with a non-404 error (e.g. 5xx) in another withholds the ResourceDoesNotExist marker while preserving the pre-existing success return. A new table-driven test (invitation_delete_test.go) covers the multi-org 404+500 case along with confirmed-absence, ordinary success, real-error, addressing, and malformed-id paths. No new issues found.

Security Issues
None found.

Correctness Issues
None found.

Suggestions
None.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

…n-404 error

Address the automated pr-review suggestion: in a multi-org config, a 404 in
one org combined with a non-404 error (e.g. 5xx) in another previously still
emitted ResourceDoesNotExist, slightly overstating "confirmed absent." Track a
hadRealError flag and emit the marker only when !wasCancelled && !hadRealError,
so the typed already-absent outcome strictly means the provider authoritatively
confirmed absence with no competing undetermined state — matching SPEC-09a's
no-false-positives rule.

The pre-existing success/error boundary is unchanged (a 404 still sets
isRemoved, so the call still returns success); only the marker emission is
tightened. Adds a multi-org test asserting the 404+500 case keeps success but
carries no marker.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
@c1-squire-dev

c1-squire-dev Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — addressed the multi-org suggestion in 72e0b835. Added a hadRealError flag so the ResourceDoesNotExist marker is emitted only when !wasCancelled && !hadRealError, i.e. a 404 with no competing non-404 error. The pre-existing success/error boundary is unchanged (a 404 still sets isRemoved → success); only the marker is now strictly gated on authoritative absence, matching SPEC-09a's no-false-positives rule. New multi-org test covers the 404+500 case: success preserved, no marker.

Note: the verify / lint failure is a pre-existing baseline on main (SA1019 trait-profile deprecations across ~7 untouched files, red since the 2026-07-21 dependency bump — last green Verify run predates it). This PR adds zero new lint issues.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@c1-squire-dev

c1-squire-dev Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Heads up: the red verify / lint on this PR is the pre-existing main SA1019 baseline (baton-sdk trait-profile deprecations from the 2026-07-21 dependency bump), not anything introduced here. #181 is a standalone prerequisite that repairs that baseline (CI-exact golangci-lint v2.11.4: 15 → 0 issues). Once #181 merges, this PR should rebase onto main to pick up the green lint — no changes to this branch are needed for the fix itself.

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.

1 participant