docs(specs): refuse a different handle on an activated organization in S10
What does this MR do and why?
Amends monolith/S10 so a repeat activation that carries a different handle, sent to an organization that is already activated, is refused as a conflict. Today it resolves silently to the existing registry.
Today's behavior disagrees with the spec's own principle. Criterion 4 says the loser of a concurrent different-handle race surfaces AR's 409 "rather than resolving to the winner's registry". The same request sent a moment after the winner, not concurrently, currently gets success and the other handle back (observed live against GDK and a local AR; details in gitlab#619522). The "repeat request against an already-mapped organization" sentence was written in the same-handle context. Nothing in the spec grants idempotency for a different handle.
Changes to docs/specs/monolith/S10-activation-and-deactivation.md
| Section | Change |
|---|---|
| Provisioning trigger, Validation row | Scoped to organizations with no mapping row; a repeat runs neither check |
| Behavior, mapping-row step 4 | Drops the repeat-request sentence, so the step covers only the concurrent uniqueness race |
| Behavior, new repeat-request passage | Checks the mapping row first and makes no provisioning call. Resolves the handle live, never from the cache. Outcomes: same handle resolves to the existing registry; different handle is refused as a conflict naming the claimed handle and carrying the resolved registry; purged is refused as re-onboarding not being available; a failed resolution maps by cause (401/403 not-available, transport or 5xx service-unavailable); an AR 404 is refused as service-unavailable. The submitted handle is never persisted, logged, forwarded, or echoed. |
| Behavior, caching paragraph | No decision reads the cache, so an emergency handle change reaches display only |
| Acceptance criterion 3 | One RSpec case per repeat arm, plus a cached-entry-disagrees case. No criterion is renumbered. |
| Setup-page criteria | The page enforces exactly Rails' syntactic rules; the conflict refusal replaces the form with a link to the registry |
| Error cases | New row: organization already activated under a different handle |
| Security considerations | The handle-validation bullet names the repeat path as its one exception |
| Out of scope | Re-onboarding: the claim precedes the repoint, and the flow owns the purged repeat arm |
| Follow-ups | New row for the monolith implementation and the plan step 2 note, both carried by gitlab#619522 |
| Resolutions | New entry recording the decision and two rejected alternatives. Appended at the end, so existing ordinals are unchanged. |
Decisions recorded here
- Never grant an uncompared handle. A failed resolution, a
404, and apurgednamespace all refuse rather than resolve to the existing registry. - Live resolution for the comparison. One AR read on a human-paced mutation path, so the cache's staleness and negative caching never decide a claim.
- Purged is refused, not provisioned. Provisioning a new handle before the out-of-scope repoint exists would orphan the new namespace against the surviving mapping row.
- Reachability. The setup page renders only without a mapping row, so the case is reachable from a page loaded before activation (for example, a second tab) or through the mutation directly.
- The plan is not touched. Step 2 of
docs/plans/monolith/2026-08-04-activation-and-deactivation.mdalready shipped (gitlab-org/gitlab!250653 (merged)) with the idempotent behavior, and a merged step's entry is a record of what shipped. The Follow-ups row carries the note it owes.
Out of scope
- Warning an organization with zero or several top-level groups before it reaches the handle form: pre-existing, filed as gitlab-org/gitlab#630929.
- Whether the permanence callout satisfies ADR-015's acknowledgment: pre-existing, already routed to the ADR-015 author in Follow-ups.
ADR check
- ADR-022 (namespace decoupling): consistent. The handle is immutable, and Rails persists only the UUID. The comparison reads the cached resolution and adds no column.
- ADR-014 (frontend to AR): the refusal reuses the mutation's payload
errors, and the service-unavailable arm follows the existing error mapping. - Freshness:
scripts/adr-freshness.shreports no upstream change to 014 or 022.
Not affected
docs/testing/: no e2e scenario covers activation. The monolith implementation MR will revisit this.docs/user/: this is a spec-only change, so no customer-visible surface changes in this MR.- Configuration, OpenAPI/Bruno: not touched.
How to verify
Read the diff against criterion 4 and the concurrent different-handle paragraph it mirrors.
Related to gitlab-org/gitlab#619522 (closed)