Skip to content

feat: emit PENDING status on invitation resources - #184

Merged
mstanbCO merged 3 commits into
mainfrom
mstanbCO/IGA-1212/emit-pending-invitation-status
Sep 1, 2026
Merged

feat: emit PENDING status on invitation resources#184
mstanbCO merged 3 commits into
mainfrom
mstanbCO/IGA-1212/emit-pending-invitation-status

Conversation

@mstanbCO

@mstanbCO mstanbCO commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What changes

Pending GitHub org invitations are modeled as a dedicated invitation resource type. Until now they had no way to say "pending": the connector emitted UserTrait_Status_STATUS_UNSPECIFIED through the deprecated WithStatus trait option, with a //nolint:staticcheck suppression, because the SDK had no PENDING value and NewUserTrait force-defaults an unset trait status to ENABLED.

The SDK now has that value, so invitations emit it explicitly:

  • Resource level onlyWithResourceStatus(v2.Status_RESOURCE_STATUS_PENDING, <detail>), the same non-deprecated option the accepted-member path in user.go uses. Per review, the trait-level status is deliberately not set: every consumer resolves status resource-first via the SDK's GetStatus (audited across baton-sdk tooling and c1's uplift/provisioning paths — no direct trait reader exists). The trait falls to NewUserTrait's ENABLED force-default, which the tests pin as a known artifact that must not be read directly. This also removes the file's last deprecated-API //nolint.

The <detail> string is the existing invitation_status value, so consumers can tell the two flavors apart:

Invitation Resource status Status details
Pending acceptance RESOURCE_STATUS_PENDING invitation_pending_acceptance
Expired RESOURCE_STATUS_PENDING invitation_expired

Expired invitations stay PENDING rather than getting a distinct enum value. An expired invite is still not a usable account, and the details field preserves the distinction without inventing new enum semantics.

What changes for consumers

Invitation resources move from STATUS_UNSPECIFIED to STATUS_PENDING at the resource level (resolve via GetStatus; the trait-level status is a force-default artifact). Nothing else about the resource changes — profile, IDs, entitlements, grants, and provisioning are untouched.

Dependency

Depends on ConductorOne/baton-sdk#1104 Resolved: #1104 merged and is included in the released baton-sdk v0.26.0, which this repo's main already uses. This branch was rebased onto that main and the pseudo-version pin commit was dropped entirely — the PR is now the four connector files only, with no go.mod/vendor changes. The stated undraft gate (tagged SDK release) is satisfied.

Rollout ordering

Updated: c1's PENDING handling (ductone/c1#24091) was narrowed during review to treat only explicit STATUS_PENDING as pending — explicit UNSPECIFIED is deliberately not a pending signal, because other connectors (Cloudflare, MongoDB Atlas, and others) emit it for real, active accounts. Consequences for ordering:

  • No ordering is unsafe. c1's c1z ingestion does not run enum validation on synced resources; a c1 that predates the vendored-SDK bump decodes value 4 and maps it to UNSPECIFIED via the existing default: arm — i.e., invitations behave exactly as they do today (defined_only rules matter only to consumers that call Validate(), which c1's read paths do not).
  • The feature activates only when the chain is complete: baton-sdk released → this connector emitting PENDING → c1 running #24091 plus a vendored-SDK bump that maps RESOURCE_STATUS_PENDING in NewAppUserStatusV2. Until all three are live, pending invitations keep today's behavior — no better, no worse.

SDK bump: behavior beyond the enum

The SDK bump now arrives via this repo's main (v0.26.0) rather than this PR, but for release notes: v0.24.6 → v0.26.0 changes runtime behavior the connector inherits — the default dotc1z storage engine for newly created c1z files flips from SQLite to Pebble (pkg/field/defaults.go: "Defaults to pebble when unset"; overridable via the storage-engine flag), and diff-sync support (--diff-syncs / --base-sync-id / --applied-sync-id) is removed. Nothing in this repo references the removed flags.

Testing

pkg/connector/invitation_test.go asserts both status levels for both fixtures via a new requireInvitationPending helper. Verified the trait-level assertion is load-bearing: removing the WithDetailedStatus line makes the trait status come back as 1 (ENABLED) instead of 4 (PENDING) while the resource level stays PENDING — exactly the trap the removed comment described — and three subtests fail.

go build ./cmd/...   # ok
go test ./...        # ok
golangci-lint run ./pkg/connector/...  # only a pre-existing gosec finding in repository.go, untouched here

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Aug 25, 2026

Copy link
Copy Markdown

IGA-1212

// attributes. Expired invitations stay PENDING - they are still not a
// usable account - and carry the distinction in the status details.
resourceSdk.WithResourceProfile(profile),
resourceSdk.WithResourceStatus(v2.Status_RESOURCE_STATUS_PENDING, status),

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: docs/connector.mdx:26 still says "if an invitation is pending, the account status will be shown as Unspecified", which this change makes wrong. Update it to Pending in this PR so the docs don't lag the emitted status.

Separately, both status fields carry defined_only enum validation, so a reader still on the pre-PENDING proto descriptor would reject enum value 4 rather than fall back to unspecified. The PR description covers the c1-first rollout ordering, but nothing in the connector enforces it — worth confirming that ordering is tracked before this leaves draft, since there is no config gate to fall back to.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Docs fixed in 382a57f — connector.mdx now says Pending, with the expired-invitations nuance (also Pending, distinguished by invitation_expired in status details).

On the defined_only ordering concern: the situation changed since this review ran. c1 PR ductone/c1#24091 was narrowed to treat only explicit STATUS_PENDING as pending (UNSPECIFIED turned out to be emitted for real accounts by other connectors). We also verified c1's c1z ingestion runs no enum validation on synced resources — a pre-bump c1 decodes value 4 and maps it to UNSPECIFIED via the existing default arm, i.e. today's behavior exactly. So no ordering is unsafe and no config gate is needed; the feature simply stays inert until c1 ships its vendored-SDK bump mapping PENDING. The PR description's Rollout section is updated with this.

Comment thread go.mod Outdated

require (
github.com/conductorone/baton-sdk v0.24.6
github.com/conductorone/baton-sdk v0.25.1-0.20260825204020-991ca45253a7

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: two things on this bump.

  1. It pins an unreleased pseudo-version (v0.25.1-0.20260825204020-991ca45253a7). The PR already commits to swapping to the tagged release before undrafting — flagging so it isn't lost.
  2. The bump is scoped in the description to "adds the two PENDING enum values", but v0.24.6 → v0.25.x also changes runtime behavior the connector inherits: the default dotc1z storage engine flips from SQLite to Pebble for new files (pkg/dotc1z/engine_registry.go, pkg/field/defaults.go), and the --diff-syncs / --base-sync-id / --applied-sync-id flags are removed along with diff-sync support. Nothing in this repo references those flags, so there is no build or CI break, but the storage-engine default change affects the artifact this connector produces and is worth calling out in the PR description and release notes rather than shipping silently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Both addressed in the PR description:

  1. Pseudo-version: acknowledged and tracked — the pin stays while feat: add PENDING status for invitation-style principals baton-sdk#1104 is unreleased (its branch has since advanced by two test/comment-only commits, 8a66dfb and 9de8e65, with no API change, so the pin needn't chase them); the swap to the tagged release remains the gate for undrafting, isolated as its own commit.

  2. The SDK bump's behavior changes are now documented in a dedicated "SDK bump: behavior beyond the enum" section — the SQLite→Pebble default engine flip for new c1z files (verified against vendored pkg/field/defaults.go) and the removal of diff-sync flags — so they land in release notes rather than silently.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Connector PR Review: feat: emit PENDING status on invitation resources

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base dd986def5db7.
Review mode: incremental since 995a4a1
View review run

Review Summary

The new commit (487f98d) acts on @laurenleach's review by dropping the deprecated WithDetailedStatus trait option from invitationToUserResource, leaving WithResourceStatus(RESOURCE_STATUS_PENDING, <detail>) as the sole status source, and rewrites requireInvitationPending to assert the resolved status through resourceSdk.GetStatus while pinning the trait-level ENABLED force-default as a known artifact. I verified this against the vendored SDK: GetStatus returns the resource status first (resource_attrs.go:78-81), NewUserResource appends WithUserTrait after the caller's resource options (resource.go:534) so syncUserTraitToResource's !r.HasStatus() guard never overwrites the explicit PENDING — the emitted resource status is correct for both the pending and expired fixtures. I also re-scanned the full PR diff (docs/connector.mdx, pkg/connector/connector.go, pkg/connector/invitation.go, pkg/connector/invitation_test.go; no go.mod/go.sum changes after the rebase) for security and correctness, and found no new issues; the prior docs and pseudo-version findings remain addressed.

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.

@mstanbCO
mstanbCO marked this pull request as ready for review August 31, 2026 23:41
mstanbCO and others added 2 commits August 31, 2026 23:42
Invitation resources previously emitted STATUS_UNSPECIFIED via the
deprecated trait-level WithStatus option, because the SDK had no way to
express "pending" and NewUserTrait force-defaults an unset trait status
to ENABLED.

Now that the SDK has a PENDING value, emit it explicitly:

  - resource level via the non-deprecated WithResourceStatus, matching
    the accepted-member path in user.go
  - trait level via WithDetailedStatus, which is still required to
    override NewUserTrait's ENABLED default

Both levels carry the invitation_status value (pending acceptance vs
expired) as status details. Expired invitations stay PENDING rather than
getting a distinct enum value: an expired invite is still not a usable
account, and the details field preserves the distinction.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mstanbCO
mstanbCO force-pushed the mstanbCO/IGA-1212/emit-pending-invitation-status branch from 382a57f to 995a4a1 Compare August 31, 2026 23:43

@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.

Comment thread pkg/connector/invitation.go Outdated
// Set explicitly: NewUserTrait defaults an unset trait status to
// ENABLED, which an unaccepted invitation is not.
//nolint:staticcheck // trait status is deprecated but must be set to override the ENABLED default.
resourceSdk.WithDetailedStatus(v2.UserTrait_Status_STATUS_PENDING, status),

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.

do we need this now that we have Status_RESOURCE_STATUS_PENDING for WithResourceStatus?

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.

we've been using these type of helpers to prioritize resource traits over user,group,etc traits https://github.com/ConductorOne/baton-sdk/blob/94143a9f85b525bbaf28922560aae978d460c5eb/pkg/types/resource/resource_attrs.go#L78

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right — dropped in 487f98d. We audited every trait-status reader across baton-sdk and c1 before conceding: all of them resolve resource-first (baton-sdk's GetStatus never consults the trait when the resource status is set; c1's uplift and ProvisionAppUser paths go through that same helper; the CLI csv/xlsx exporters too), so the trait set was belt-and-suspenders with no reader. One artifact to be aware of: the trait status now falls to NewUserTrait's ENABLED force-default, so the raw trait annotation in a c1z reads ENABLED next to the PENDING resource status — no code path reads it, and the tests pin it so a change gets caught. Dropping it also removed the file's last deprecated-API nolint.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — GetStatus's resource-first precedence is exactly what makes this safe, and it's now the documented contract: the tests assert the resolved status via that helper rather than reading the trait. Possible SDK follow-up if the force-defaulted ENABLED trait bothers anyone as stored data: skip the ENABLED default in NewUserTrait when the resource already carries an explicit status, so the trait would be genuinely absent instead of falsely ENABLED.

Consumers resolve status resource-first via GetStatus, which never
consults the trait when the resource status is set. Audited every
reader in baton-sdk and c1: none reads the trait directly. The trait
falls to NewUserTrait's ENABLED default, pinned in tests as a known
artifact readers must not consult.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

@mstanbCO
mstanbCO merged commit 60acf8d into main Sep 1, 2026
9 checks passed
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.

2 participants