Skip to content

MM-69269 - Spaces permissions and RBAC: read gates, capabilities, auto-join - #10

Draft
catalintomai wants to merge 31 commits into
masterfrom
MM-69269-permissions-rbac
Draft

MM-69269 - Spaces permissions and RBAC: read gates, capabilities, auto-join#10
catalintomai wants to merge 31 commits into
masterfrom
MM-69269-permissions-rbac

Conversation

@catalintomai

Copy link
Copy Markdown
Collaborator

Summary

Puts every space and page route behind the Spaces permission set added in the paired core PR. Until now any logged-in user could reach any space or page; this change resolves a read gate first, then a capability gate, on every request.

Access model — A space is open or private (ViewAccess, new column, defaults to open). A read resolves to one of four outcomes: denied, sysadmin, backing-channel member, or the open-space fall-through for a team member who is not yet a member. Every denial — space missing, caller not a team member, caller no longer a member — returns the same 403, so existence cannot be probed by status code or message. A backend lookup failure is never collapsed into a denial; it surfaces as a 500 rather than misreporting an outage as "not permitted".

Capabilities — A member's effective capability set is derived from the atomic capability roles on their backing-channel membership plus the space's default set. SchemeAdmin members additionally hold the full admin set regardless of what is recorded in ExplicitRoles; SchemeGuest members are pinned to read_page plus explicit grants only and cannot be given capabilities. Two new routes manage this: PATCH /spaces/{space_id}/members/{user_id}/capabilities for one member, and PATCH /spaces/{space_id}/default-capabilities for the space default. Both enforce a self-escalation guard and a last-admin invariant, so a space cannot be left without an authorized admin.

Auto-join — A non-member admitted only through the open-space fall-through is joined to the backing channel before the write gate re-runs, but only when the space's current default set would grant that permission to a plain member. The join re-validates the open-read admission under the space's membership lock, so an openprivate flip racing the request aborts the join rather than admitting on a stale read. A caller denied on the source side of a cross-space move is never left holding a membership the rejected request created.

Cross-space movemove-to-space is gated on source read_page, a remove-class delete permission over the whole moved subtree, and target create_page. When the caller resolves to the own-scoped delete permission, subtree ownership is verified inside the move transaction against its own locked subtree read, closing the window a pre-scan would leave open to a concurrent reparent grafting another user's page into the subtree.

Scheme lifecycle — A space with a non-default capability set gets its own custom scheme, created from the seeded presets and retired when nothing references it. Retirement is reference-counted over live and soft-deleted spaces alike (a soft-deleted space is restorable and keeps its SchemeId), and refuses any scheme this plugin did not create. A backing channel archived by a failed space creation is excluded from that count, so an abandoned channel cannot pin a scheme forever.

Testing — Adds a containerized end-to-end suite that stands up a real server with the paired core image and exercises the gates over HTTP, plus a build script for that image. Unit coverage extends across the model, store, app and handler layers.

Dependency

Requires mattermost/mattermost#37685. server/public is pinned to that branch's head and must be repinned to a release tag once the core change merges and ships.

Ticket Link

Fixes: https://mattermost.atlassian.net/browse/MM-69269

Release Note

NONE

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change introduces capability-based Space RBAC, open/private view access, scheme-backed capabilities, ownership checks for page moves, member capability APIs, and Docker/Testcontainers end-to-end coverage with local and CI execution.

Changes

Space RBAC and capability model

Layer / File(s) Summary
Capability contracts and storage
server/model/*, server/store/*, go.mod, assets/i18n/en.json
Adds view-access and capability models, persistence, validation, preset and pooled schemes, ownership checks, bounded transactions, and related error translations.
Service authorization and schemes
server/app/permissions.go, server/app/scheme.go, server/app/space*.go, server/app/page_hierarchy.go
Adds centralized read resolution, capability gates, auto-join rollback, scheme configuration, member management, view-access updates, and ownership-aware page moves.
API authorization and request flows
server/api*.go, server/app/page_draft.go, server/app/*_test.go
Replaces membership checks with capability gates, adds capability endpoints, enriches responses, and updates draft, page, and space authorization flows.
End-to-end execution and CI wiring
.github/workflows/ci.yml, Makefile, build/*, server/e2e/*, server/internal/testutil/*
Adds image construction, local and CI E2E execution, Testcontainers setup, RBAC scenarios, test stubs, and WebSocket event coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: Spaces permission enforcement and RBAC with read gates, capabilities, and auto-join.
Description check ✅ Passed The description directly explains the space and page permission, RBAC, auto-join, scheme lifecycle, and end-to-end testing changes.
Docstring Coverage ✅ Passed Docstring coverage is 84.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MM-69269-permissions-rbac

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (8)
go.mod (1)

17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Temporary pin to the paired core branch must not merge.

server/public is pinned to a branch pseudo-version. Track replacing it with a released version once core PR #37685 lands, otherwise the module is unbuildable for anyone once that branch is deleted or force-pushed. Want me to open a follow-up issue for the un-pin?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go.mod` at line 17, Replace the branch-based pseudo-version for
github.com/mattermost/mattermost/server/public in go.mod with the released
version once core PR `#37685` is available, and remove the temporary pin so the
module depends on a stable published release.
server/e2e/container_test.go (1)

132-146: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Polling ignores context cancellation.

The loop only watches its own 2-minute wall-clock deadline; when the caller's context is cancelled it keeps sleeping and retrying failing calls instead of returning promptly.

♻️ Proposed refactor
 	for time.Now().Before(deadline) {
 		_, _, err := adminClient.GetSchemes(ctx, "", 0, 1)
 		if err == nil {
 			return nil
 		}
 		lastErr = err
-		time.Sleep(2 * time.Second)
+		select {
+		case <-ctx.Done():
+			return fmt.Errorf("waiting for advanced-permissions phase-2 migration: %w (last error: %v)", ctx.Err(), lastErr)
+		case <-time.After(2 * time.Second):
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/e2e/container_test.go` around lines 132 - 146, Update
waitForPhase2Migration to honor ctx cancellation during both polling and the
2-second delay, returning ctx.Err() promptly when cancelled. Replace the
unconditional time.Sleep and ensure the loop checks the context before retries
while preserving the existing deadline and final timeout error behavior.
server/e2e/helpers_test.go (1)

79-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

context.Context should come before *testing.T, or the lint config needs an exemption. revive’s context-as-argument rule is enabled, and server/e2e/helpers_test.go doesn’t whitelist *testing.T, so createActor, addSpaceMember, spaceHasMember, and deleteSpace will be flagged when the e2e package is linted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/e2e/helpers_test.go` at line 79, Update createActor, addSpaceMember,
spaceHasMember, and deleteSpace so context.Context is the first parameter,
before *testing.T, and adjust every call site accordingly; alternatively, add
the e2e test helpers to the revive context-as-argument exemption if that is the
established linting approach.
server/store/migrations/000007_add_viewaccess_to_spaces.up.sql (1)

5-5: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider NOT VALID for the CHECK, and note this statement is not retry-safe.

Two things about line 5:

  • ADD CONSTRAINT ... CHECK takes ACCESS EXCLUSIVE and scans the table to validate. Every row was just written by the DEFAULT 'private' on line 1, so the scan can only pass — adding it NOT VALID (enforced for new writes, no scan) avoids blocking writes on a large DOCS_Space.
  • Unlike line 1, Postgres has no ADD CONSTRAINT IF NOT EXISTS, so a re-run after a partially applied migration fails with duplicate_object and leaves the version dirty for an operator to clear.
♻️ Suggested change
-ALTER TABLE DOCS_Space ADD CONSTRAINT chk_docs_space_view_access CHECK (ViewAccess IN ('open', 'private'));
+ALTER TABLE DOCS_Space ADD CONSTRAINT chk_docs_space_view_access CHECK (ViewAccess IN ('open', 'private')) NOT VALID;

As per static analysis hint constraint-missing-not-valid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/store/migrations/000007_add_viewaccess_to_spaces.up.sql` at line 5,
Update the CHECK constraint creation in this migration to use NOT VALID,
avoiding the table-wide validation scan while continuing to enforce the
constraint on new writes. Preserve the existing constraint name and condition;
do not add retry handling beyond what PostgreSQL supports for ADD CONSTRAINT.

Source: Linters/SAST tools

server/app/space.go (1)

372-375: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add the space == nil guard the other exported space methods all have.

BuildSpaceWithAccess dereferences space.Id immediately, while CreateSpace, SetSpaceDefaultCapabilities, UpdateSpace, ListSpaceMembers, and AddSpaceMember all open with an explicit nil check returning 400. Current callers pass a non-nil record, so this is convention/future-proofing rather than a live panic.

🛡️ Suggested guard
 func (s *Service) BuildSpaceWithAccess(space *model.Space, userID string) (*model.SpaceWithAccess, *mmmodel.AppError) {
+	if space == nil {
+		return nil, mmmodel.NewAppError("BuildSpaceWithAccess", "app.space.get.invalid_id.app_error", nil, "", http.StatusBadRequest)
+	}
 	if appErr := s.requireClient("BuildSpaceWithAccess", "space_id", space.Id, "user_id", userID); appErr != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/space.go` around lines 372 - 375, Add an explicit nil check at the
start of BuildSpaceWithAccess before dereferencing space.Id, returning the same
HTTP 400 AppError pattern used by CreateSpace, SetSpaceDefaultCapabilities,
UpdateSpace, ListSpaceMembers, and AddSpaceMember.
server/store/space_store.go (1)

104-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer building the OR branch in Go over binding a bare bool parameter as a predicate.

sq.Expr("?", callerHasOpenFallthrough) emits a placeholder in boolean position and leans on Postgres resolving the untyped parameter to boolean. It works with the current driver (the store tests cover both values), but dropping the branch when the caller lacks the fall-through is clearer and lets the planner skip the ViewAccess predicate entirely.

♻️ Suggested change
-	memberExists := sq.Expr("EXISTS (SELECT 1 FROM ChannelMembers cm WHERE cm.ChannelId = sp.ChannelId AND cm.UserId = ?)", userID)
-	openFallthrough := sq.And{sq.Eq{"sp.ViewAccess": model.ViewAccessOpen}, sq.Expr("?", callerHasOpenFallthrough)}
-
+	memberExists := sq.Expr("EXISTS (SELECT 1 FROM ChannelMembers cm WHERE cm.ChannelId = sp.ChannelId AND cm.UserId = ?)", userID)
+	visible := sq.Or{memberExists}
+	if callerHasOpenFallthrough {
+		visible = append(visible, sq.Eq{"sp.ViewAccess": model.ViewAccessOpen})
+	}
+
 	builder := s.getQueryBuilder().
 		Select(columnsWithAlias("sp", spaceSelectColumns)...).
 		From("DOCS_Space sp").
 		Where(sq.Eq{"sp.TeamId": teamID, "sp.DeleteAt": 0}).
-		Where(sq.Or{memberExists, openFallthrough}).
+		Where(visible).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/store/space_store.go` around lines 104 - 111, Update the query
construction around memberExists and openFallthrough to build the OR branches in
Go: always include memberExists, but add the ViewAccess-open branch only when
callerHasOpenFallthrough is true. Remove the boolean placeholder predicate while
preserving the existing team, deletion, and membership filters.
server/app/permissions.go (1)

254-313: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Confirm the WS publish inside the membership lock is intentional.

publishToChannels runs while WithSpaceMembershipLock still holds its dedicated connection; a slow plugin-API RPC extends lock hold time and can push concurrent membership mutations into ReasonLockTimeout 409s. Moving the publish after the lock closure (using the captured joined/member.UserId) would keep the critical section to DB + membership work only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/permissions.go` around lines 254 - 313, The WS publication in
AutoJoinIfDefaultGranted currently runs inside WithSpaceMembershipLock,
unnecessarily extending the membership lock during plugin/API work. Capture the
joined member’s user ID while performing AddMember, then move publishToChannels
outside the lock closure and invoke it only after a successful lock operation
and join.
server/model/space_capabilities_test.go (1)

112-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Modernize the backward loop per static analysis.

golangci-lint flags this as convertible to slices.Backward (already imported).

♻️ Proposed modernization
-		shuffled := make([]string, 0, len(contribute)*2)
-		for i := len(contribute) - 1; i >= 0; i-- {
-			shuffled = append(shuffled, contribute[i], contribute[i])
-		}
+		shuffled := make([]string, 0, len(contribute)*2)
+		for _, v := range slices.Backward(contribute) {
+			shuffled = append(shuffled, v, v)
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/model/space_capabilities_test.go` around lines 112 - 121, Update the
reverse iteration in the test’s shuffled construction to use the already
imported slices.Backward helper instead of the manual index loop, while
preserving the duplicate append order and existing assertions.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build/build-core-image.sh`:
- Around line 14-17: Update the usage examples in build-core-image.sh to
reference build/build-core-image.sh instead of scripts/build-core-image.sh,
including the default, CORE_IMAGE, and --skip-build invocations.

In `@go.mod`:
- Line 37: Update the dependency resolution represented by go.mod so containerd
is at least 1.7.33 and Docker is at least 29.3.1, either by upgrading
testcontainers-go or adding explicit indirect overrides. Verify the resulting
module graph no longer retains the vulnerable containerd v1.7.18 or Docker
v27.1.1 pins.

In `@server/app/page_hierarchy.go`:
- Around line 165-169: Update MovePageToSpace’s same-space branch to enforce
non-empty requiredOwnerID before or during the
reparentWithinSpace/store.MovePage path, preserving the existing in-transaction
ownership semantics used for cross-space moves. Ensure same-space
delete_own_page callers cannot reparent pages outside the required owner, and
add coverage for a same-space move with requiredOwnerID set.

In `@server/e2e/container_test.go`:
- Around line 148-159: Update resolveBundlePath to select the most recently
modified matching bundle rather than matches[len(matches)-1]. Stat each
filepath.Glob result, compare modification times, and return the newest path
while preserving existing glob and error handling.
- Around line 106-127: Use a fresh, independent teardown context whenever
startup fails in the container setup flow around container.URL,
container.GetAdminClient, and waitForPhase2Migration. Replace each
container.Terminate(ctx) call with termination using a short-lived context
created specifically for cleanup, ensuring cleanup still runs when the 3-minute
startEnv context has expired.

In `@server/e2e/helpers_test.go`:
- Around line 61-69: Update the response helper around json.Unmarshal to
propagate decode failures instead of discarding them: capture the unmarshal
error and return it from the helper, wrapping it with the response context as
needed using fmt. Preserve the existing behavior for nil output targets and
empty response bodies so callers such as scenario3_private_team_space can assert
on actual decoded payloads.

In `@server/store/page_move.go`:
- Around line 250-255: Update MovePageToSpace so the IDs returned by
collectLiveSubtreeIDs are re-locked within the transaction before enforcing
requiredOwnerID or trusting the owners map. Re-read or obtain ownership data
from that locked subtree, then preserve the existing validation and
rewriteSubtreeSpace flow using the locked result.

In `@server/store/scheme_store.go`:
- Around line 235-256: Lock the target scheme row before counting channel
references in the surrounding deletion flow, using the existing scheme
identifier and transaction so retirement serializes with channel repointing.
Ensure the repoint path uses the same Schemes-row lock, then retain the
reference check and role/scheme deletion only after the lock is acquired.

---

Nitpick comments:
In `@go.mod`:
- Line 17: Replace the branch-based pseudo-version for
github.com/mattermost/mattermost/server/public in go.mod with the released
version once core PR `#37685` is available, and remove the temporary pin so the
module depends on a stable published release.

In `@server/app/permissions.go`:
- Around line 254-313: The WS publication in AutoJoinIfDefaultGranted currently
runs inside WithSpaceMembershipLock, unnecessarily extending the membership lock
during plugin/API work. Capture the joined member’s user ID while performing
AddMember, then move publishToChannels outside the lock closure and invoke it
only after a successful lock operation and join.

In `@server/app/space.go`:
- Around line 372-375: Add an explicit nil check at the start of
BuildSpaceWithAccess before dereferencing space.Id, returning the same HTTP 400
AppError pattern used by CreateSpace, SetSpaceDefaultCapabilities, UpdateSpace,
ListSpaceMembers, and AddSpaceMember.

In `@server/e2e/container_test.go`:
- Around line 132-146: Update waitForPhase2Migration to honor ctx cancellation
during both polling and the 2-second delay, returning ctx.Err() promptly when
cancelled. Replace the unconditional time.Sleep and ensure the loop checks the
context before retries while preserving the existing deadline and final timeout
error behavior.

In `@server/e2e/helpers_test.go`:
- Line 79: Update createActor, addSpaceMember, spaceHasMember, and deleteSpace
so context.Context is the first parameter, before *testing.T, and adjust every
call site accordingly; alternatively, add the e2e test helpers to the revive
context-as-argument exemption if that is the established linting approach.

In `@server/model/space_capabilities_test.go`:
- Around line 112-121: Update the reverse iteration in the test’s shuffled
construction to use the already imported slices.Backward helper instead of the
manual index loop, while preserving the duplicate append order and existing
assertions.

In `@server/store/migrations/000007_add_viewaccess_to_spaces.up.sql`:
- Line 5: Update the CHECK constraint creation in this migration to use NOT
VALID, avoiding the table-wide validation scan while continuing to enforce the
constraint on new writes. Preserve the existing constraint name and condition;
do not add retry handling beyond what PostgreSQL supports for ADD CONSTRAINT.

In `@server/store/space_store.go`:
- Around line 104-111: Update the query construction around memberExists and
openFallthrough to build the OR branches in Go: always include memberExists, but
add the ViewAccess-open branch only when callerHasOpenFallthrough is true.
Remove the boolean placeholder predicate while preserving the existing team,
deletion, and membership filters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 188835b3-ba09-46b3-b0fe-6018ce2f4806

📥 Commits

Reviewing files that changed from the base of the PR and between b6a7ef3 and cc00562.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (41)
  • .github/workflows/ci.yml
  • Makefile
  • assets/i18n/en.json
  • build/build-core-image.sh
  • go.mod
  • server/api.go
  • server/api_handler_test.go
  • server/api_page.go
  • server/api_space.go
  • server/app/page_duplicate_test.go
  • server/app/page_hierarchy.go
  • server/app/page_move_test.go
  • server/app/page_move_to_space_test.go
  • server/app/page_reorder_test.go
  • server/app/permissions.go
  • server/app/service.go
  • server/app/service_test.go
  • server/app/space.go
  • server/app/space_members.go
  • server/app/space_test.go
  • server/app/ws_events.go
  • server/app/ws_events_test.go
  • server/e2e/README.md
  • server/e2e/container_test.go
  • server/e2e/helpers_test.go
  • server/e2e/scenarios_test.go
  • server/internal/testutil/fixtures.go
  • server/internal/testutil/permstub.go
  • server/model/space.go
  • server/model/space_capabilities.go
  • server/model/space_capabilities_test.go
  • server/model/space_test.go
  • server/store/migrations/000007_add_viewaccess_to_spaces.down.sql
  • server/store/migrations/000007_add_viewaccess_to_spaces.up.sql
  • server/store/page_move.go
  • server/store/page_move_test.go
  • server/store/scheme_store.go
  • server/store/scheme_store_test.go
  • server/store/space_store.go
  • server/store/store.go
  • server/store/store_test.go

Comment thread build/build-core-image.sh Outdated
Comment thread go.mod Outdated
Comment thread server/app/page_hierarchy.go Outdated
Comment thread server/e2e/container_test.go
Comment thread server/e2e/container_test.go
Comment thread server/e2e/helpers_test.go
Comment thread server/store/page_move.go Outdated
Comment thread server/store/scheme_store.go Outdated
@catalintomai

catalintomai commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai - On two of the nitpicks from the review body (no inline threads on those, so answering here):

server/e2e/helpers_test.go:79 (revive context-as-argument): revive is configured with an explicit rules list (only exported + unused-parameter, see .golangci.yml), so context-as-argument is not enabled. Note also that server/e2e sits behind the e2e build tag and we do not set run.build-tags, so ./... never loads the package. Verified both ways: golangci-lint run --build-tags e2e ./server/e2e/... -> 0 issues.

server/model/space_capabilities_test.go:112-121 (slices.Backward): modernize is enabled and does not flag this loop, since it appends each element twice per iteration (not the shape it rewrites). golangci-lint run ./server/model/... -> 0 issues, on both v2.9.0 (bin/) and v2.11.4.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@catalintomai Thanks for the detailed verification. You’re right on both points:

  • context-as-argument is not enabled by the explicit revive rule configuration, and the tagged E2E lint invocation is clean.
  • The reverse loop’s double append is intentional structure that modernize does not rewrite to slices.Backward; the model lint checks are clean as well.

I’m withdrawing both review-body nitpicks.


✏️ Learnings added
Learnt from: catalintomai
URL: https://github.com/mattermost/mattermost-plugin-docs/pull/10

Timestamp: 2026-07-27T17:45:32.029Z
Learning: In this repository, `.golangci.yml` explicitly enables only the `revive` rules `exported` and `unused-parameter`; do not raise `revive` `context-as-argument` findings. The `server/e2e` package is behind the `e2e` build tag and should be linted explicitly with `golangci-lint run --build-tags e2e ./server/e2e/...` when reviewing E2E linting.

Learnt from: catalintomai
URL: https://github.com/mattermost/mattermost-plugin-docs/pull/10

Timestamp: 2026-07-27T17:45:32.029Z
Learning: In `server/model/space_capabilities_test.go`, the reverse traversal around Lines 112–121 deliberately appends each element twice, so it is not an applicable `modernize`/`slices.Backward` rewrite candidate.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@catalintomai
catalintomai marked this pull request as draft July 27, 2026 19:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (2)
server/app/ws_events_test.go (2)

674-680: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The escalation guard is not reached, so the HasPermissionToChannel stub is dead setup.

Trace SetSpaceMemberCapabilities with this input. capabilities is {edit_page}, so requestedAdmin is false. targetUserID != actingUserID, so selfTargeted is false. The mocked target has SchemeAdmin false. The condition adminAffected || selfTargeted is therefore false and RequireSpaceAdminOrSysadmin never runs.

Two consequences. The comment at Line 674 misstates what this test covers. And the stray stub would keep the test green if the guard later became unconditional, which would silently drop coverage of that guard.

Remove the stub and correct the comment. If the guard itself needs coverage, add a separate case that requests CapabilityAdminSpace or targets the acting user.

🛠️ Proposed fix
-	// The acting user must clear the admin-affecting escalation guard.
-	mockAPI.On("HasPermissionToChannel", actingUserID, space.ChannelId, mmmodel.PermissionAdminSpace).Return(true)
+	// A non-admin capability on another non-admin member does not reach the escalation guard, so
+	// no space-admin permission stub is needed here.
 	mockAPI.On("GetChannelMember", space.ChannelId, targetUserID).
 		Return(&mmmodel.ChannelMember{ChannelId: space.ChannelId, UserId: targetUserID}, nil)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/ws_events_test.go` around lines 674 - 680, Remove the unused
HasPermissionToChannel stub from the SetSpaceMemberCapabilities test and revise
the nearby comment so it accurately describes the non-admin capability scenario.
Do not add guard coverage to this case; use a separate test case requesting
CapabilityAdminSpace or targeting actingUserID if coverage is needed.

641-662: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

TestServiceAddSpaceMember_PublishesToChannelAndUser is a superset of the existing test.

TestServiceAddSpaceMember_PublishesMemberAddedEvent (Lines 598-613) uses the same helper, the same AddChannelMember stub, and asserts the channel-scoped half. This new test repeats all of that and adds only the UserId assertion. Two tests now cover one behaviour, so each future signature change to AddSpaceMember has to be applied twice — a cost this PR already paid across many call sites in this file.

Add the user-scoped assertion to the existing test and drop this one.

♻️ Proposed refactor

Extend the existing test at Lines 610-612:

-	mockAPI.AssertCalled(t, "PublishWebSocketEvent", "space_member_added",
-		map[string]any{"space_id": space.Id, "user_id": member.UserId},
-		&mmmodel.WebsocketBroadcast{ChannelId: space.ChannelId})
+	// Both delivery targets: the channel-scoped broadcast for observers, and the direct publish
+	// that reaches the added user, whom the channel-scoped resolution may not yet include.
+	payload := map[string]any{"space_id": space.Id, "user_id": member.UserId}
+	mockAPI.AssertCalled(t, "PublishWebSocketEvent", "space_member_added", payload,
+		&mmmodel.WebsocketBroadcast{ChannelId: space.ChannelId})
+	mockAPI.AssertCalled(t, "PublishWebSocketEvent", "space_member_added", payload,
+		&mmmodel.WebsocketBroadcast{UserId: member.UserId})

Then remove Lines 642-662.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/ws_events_test.go` around lines 641 - 662, Merge the user-scoped
PublishWebSocketEvent assertion from
TestServiceAddSpaceMember_PublishesToChannelAndUser into
TestServiceAddSpaceMember_PublishesMemberAddedEvent, preserving its existing
channel assertion and setup. Remove the redundant
TestServiceAddSpaceMember_PublishesToChannelAndUser test entirely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Makefile`:
- Around line 388-389: Update the test-e2e target so ./build/build-core-image.sh
runs only when CORE_IMAGE is unset or refers to a local image; skip it for
namespaced, pullable CORE_IMAGE values and allow the existing e2e test command
to let Testcontainers pull the image.

In `@server/api_handler_test.go`:
- Around line 2259-2264: Remove the stale opening line from the doc comment
above TestHandler_SetSpaceDefaultCapabilities_ReusesPooledScheme, leaving the
description focused on pooled-scheme reuse and its non-retirement behavior.

In `@server/api_space.go`:
- Around line 122-127: Update UpdateSpace so failures from the post-commit
BuildSpaceWithAccess call are logged as projection errors rather than returned
via writeAppError. Return HTTP 200 with a degraded SpaceWithAccess response
based on the committed update, preserving non-nil empty default_capabilities and
capabilities arrays; do not return a bare Space.

In `@server/app/ws_events.go`:
- Line 55: Add the web app’s WebSocket handler registration for the
space_member_capabilities_updated event, and implement the corresponding
capability-state update flow using the existing event-consumer patterns and
state symbols. Ensure the handler is wired into the web app’s registration path
so incoming capability updates can update the UI.

In `@server/e2e/README.md`:
- Around line 7-9: Remove references to the nonexistent
scripts/smoke-scenarios.sh from server/e2e/README.md and the related comments in
scenarios_test.go and helpers_test.go, unless the script is being added to the
repository. Keep the documentation describing the Go test suite’s seven
canonical Confluence permission scenarios without identifying the missing script
as an authoritative source.

In `@server/e2e/scenarios_test.go`:
- Around line 90-96: Update the comment above the drive-by page request and the
assertion message in the test around outsider.id to describe outsider as a team
member who is not a space member, removing the misleading “team non-member”
wording while preserving the existing test behavior.

In `@server/internal/testutil/schemestub.go`:
- Around line 330-343: Update resolveChannelRoles so a non-empty, unregistered
channel.SchemeId returns a not-found *mmodel.AppError instead of empty role
names with nil error. Preserve the existing nil/empty scheme branch and
registered-scheme role lookup unchanged.

In `@server/model/space_capabilities_test.go`:
- Around line 17-25: The grantableCapabilities slice in the test is missing
model.CapabilityDeletePage, which prevents the round-trip test from covering all
64 valid member-grant combinations and leaves delete-any role behavior untested.
Add model.CapabilityDeletePage to the grantableCapabilities slice and update the
comment to reference five atomic per-page capabilities instead of four.

---

Nitpick comments:
In `@server/app/ws_events_test.go`:
- Around line 674-680: Remove the unused HasPermissionToChannel stub from the
SetSpaceMemberCapabilities test and revise the nearby comment so it accurately
describes the non-admin capability scenario. Do not add guard coverage to this
case; use a separate test case requesting CapabilityAdminSpace or targeting
actingUserID if coverage is needed.
- Around line 641-662: Merge the user-scoped PublishWebSocketEvent assertion
from TestServiceAddSpaceMember_PublishesToChannelAndUser into
TestServiceAddSpaceMember_PublishesMemberAddedEvent, preserving its existing
channel assertion and setup. Remove the redundant
TestServiceAddSpaceMember_PublishesToChannelAndUser test entirely.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31e3cddd-5e41-4dfe-9d10-d804fb1b4235

📥 Commits

Reviewing files that changed from the base of the PR and between d58e3e1 and b1f43de.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (49)
  • .github/workflows/ci.yml
  • Makefile
  • assets/i18n/en.json
  • build/build-core-image.sh
  • go.mod
  • server/api.go
  • server/api_handler_test.go
  • server/api_page.go
  • server/api_page_drafts.go
  • server/api_page_drafts_test.go
  • server/api_page_presence.go
  • server/api_space.go
  • server/app/page_draft.go
  • server/app/page_draft_test.go
  • server/app/page_duplicate_test.go
  • server/app/page_hierarchy.go
  • server/app/page_move_test.go
  • server/app/page_move_to_space_test.go
  • server/app/page_reorder_test.go
  • server/app/permissions.go
  • server/app/permissions_test.go
  • server/app/scheme.go
  • server/app/service.go
  • server/app/service_test.go
  • server/app/space.go
  • server/app/space_members.go
  • server/app/space_test.go
  • server/app/ws_events.go
  • server/app/ws_events_test.go
  • server/e2e/README.md
  • server/e2e/container_test.go
  • server/e2e/helpers_test.go
  • server/e2e/scenarios_test.go
  • server/internal/testutil/fixtures.go
  • server/internal/testutil/permstub.go
  • server/internal/testutil/schemestub.go
  • server/model/space.go
  • server/model/space_capabilities.go
  • server/model/space_capabilities_test.go
  • server/model/space_test.go
  • server/store/migrations/000007_add_viewaccess_to_spaces.down.sql
  • server/store/migrations/000007_add_viewaccess_to_spaces.up.sql
  • server/store/page_move.go
  • server/store/page_move_test.go
  • server/store/page_store.go
  • server/store/space_store.go
  • server/store/store.go
  • server/store/store_test.go
  • webapp/i18n/en.json
💤 Files with no reviewable changes (1)
  • webapp/i18n/en.json

Comment thread Makefile Outdated
Comment thread server/api_handler_test.go Outdated
Comment thread server/api_space.go
Comment thread server/app/ws_events.go
Comment thread server/e2e/README.md Outdated
Comment thread server/e2e/scenarios_test.go Outdated
Comment thread server/internal/testutil/schemestub.go
Comment thread server/model/space_capabilities_test.go Outdated
@mattermost mattermost deleted a comment from coderabbitai Bot Aug 5, 2026
@mattermost mattermost deleted a comment from coderabbitai Bot Aug 5, 2026
@mattermost mattermost deleted a comment from coderabbitai Bot Aug 5, 2026
@catalintomai

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
build/build-core-image.sh (1)

138-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the duplicated default image tag.

Line 35 already applies CORE_IMAGE="${CORE_IMAGE:-mm-docs-rbac-core:dev}", and set -u is active, so CORE_IMAGE is always non-empty here. The :-mm-docs-rbac-core:dev fallback on line 138 is unreachable and states the default literal a second time. If one copy changes, the script builds an image under a name its documentation does not describe.

♻️ Proposed simplification
-IMAGE_TAG="${CORE_IMAGE:-mm-docs-rbac-core:dev}"
+IMAGE_TAG="$CORE_IMAGE"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build/build-core-image.sh` at line 138, Remove the duplicated default
fallback from the IMAGE_TAG assignment, reusing the already-initialized
CORE_IMAGE value established earlier in the script. Keep the existing image tag
behavior unchanged while ensuring the default literal appears only in the
initial CORE_IMAGE initialization.
server/api_handler_test.go (1)

149-160: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused db parameter from seedSpaceInTeam and its forwarding calls. The revive unused-parameter rule is disabled, so this is not a lint failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api_handler_test.go` around lines 149 - 160, Remove the unused db
parameter from seedSpaceInTeam and update seedSpace and every other caller to
stop forwarding it, while preserving the existing space-creation behavior
through testutil.MustCreateSpace.
server/app/space_test.go (2)

646-648: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the pooled scheme was actually attached.

require.NotEmpty(t, pooledSchemeID) checks a locally generated id, so it can never fail. The test's stated intent is that the abandoned channel pointed at the pooled scheme and the scheme survived. Assert the attachment instead, as TestServiceCreateSpace_CustomSchemeConfiguredAfterChannelAttach does at Line 712.

♻️ Proposed change
 	mockAPI.AssertCalled(t, "DeleteChannel", backingChannelID)
 	mockAPI.AssertNotCalled(t, "DeleteScheme", mock.Anything)
-	require.NotEmpty(t, pooledSchemeID)
+	require.NotNil(t, channel.SchemeId, "the doomed channel must have carried the pooled scheme")
+	require.Equal(t, pooledSchemeID, *channel.SchemeId)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/space_test.go` around lines 646 - 648, Replace the ineffective
require.NotEmpty assertion on pooledSchemeID in the test with an assertion that
the abandoned channel’s scheme attachment references the pooled scheme, matching
the established assertion pattern in
TestServiceCreateSpace_CustomSchemeConfiguredAfterChannelAttach. Keep the
existing DeleteChannel and DeleteScheme expectations unchanged.

588-611: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider one shared custom-scheme stub helper.

stubCustomSchemeCreate and stubCustomSchemeCreateFailingPatch are the same 15 lines of setup. Only the role-name prefix and the PatchRole outcome differ. A single helper that takes the role-name prefix and the patch result removes the duplication, and a later change to the scheme-creation contract then needs one edit instead of two.

♻️ Sketch of the consolidated helper
// stubSchemeCreate wires the mock calls a non-preset default-capability set needs. patchErr, when
// non-nil, makes every role patch fail, so the configure step cannot complete. Returns the new
// scheme's id.
func stubSchemeCreate(t *testing.T, mockAPI *plugintest.API, prefix string, patchErr *mmmodel.AppError) string {
	t.Helper()
	testutil.StubPooledSchemeMiss(mockAPI)
	schemeID := mmmodel.NewId()
	userRole, adminRole, guestRole := prefix+"_user_role", prefix+"_admin_role", prefix+"_guest_role"
	mockAPI.On("CreateScheme", mock.AnythingOfType("*model.Scheme")).Return(&mmmodel.Scheme{
		Id:                      schemeID,
		Name:                    model.SharedSchemeNamePrefix + mmmodel.NewId(),
		Scope:                   mmmodel.SchemeScopeChannel,
		DefaultChannelUserRole:  userRole,
		DefaultChannelAdminRole: adminRole,
		DefaultChannelGuestRole: guestRole,
	}, nil)
	testutil.RegisterSchemeRoles(schemeID, guestRole, userRole, adminRole)
	for _, r := range []string{userRole, adminRole, guestRole} {
		testutil.StubRole(mockAPI, r, nil)
	}
	if patchErr != nil {
		mockAPI.On("PatchRole", mock.AnythingOfType("string"), mock.AnythingOfType("*model.RolePatch")).
			Return(nil, patchErr)
	} else {
		testutil.StubPatchRole(mockAPI)
	}
	return schemeID
}

Also applies to: 731-754

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/space_test.go` around lines 588 - 611, The custom scheme setup is
duplicated between stubCustomSchemeCreate and
stubCustomSchemeCreateFailingPatch. Consolidate both into one shared
stubSchemeCreate helper that accepts a role-name prefix and optional patch
error, reuses the common scheme, role registration, and role stubbing setup, and
configures PatchRole to either succeed or return the supplied error; update both
callers to use it.
server/e2e/helpers_test.go (1)

126-140: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

spaceHasMember only inspects the first response page.

The request sends no page or per_page parameter, so the handler returns the first page at the default size. If a scenario space ever holds more members than that page, this helper reports false for a member that exists, and the calling assertion fails for the wrong reason or passes silently. Today's scenarios use a few actors, so the defect is latent. Either follow has_more or assert that the page was not full.

♻️ Proposed guard
 type spaceMembersResponse struct {
 	Items   []*spaceMemberJSON `json:"items"`
+	HasMore bool               `json:"has_more"`
 }
 	require.Equal(t, http.StatusOK, status, "list members of space %s: %s", spaceID, body)
+	require.False(t, resp.HasMore, "space %s has more members than one page; this helper would miss them", spaceID)
 	for _, m := range resp.Items {

Confirm the paginated response field name against paginatedResponse in server/api.go before applying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/e2e/helpers_test.go` around lines 126 - 140, Update spaceHasMember to
handle pagination instead of inspecting only the initial response page: follow
the response’s has_more indicator using the pagination field names defined by
paginatedResponse in server/api.go, requesting subsequent pages until the user
is found or no pages remain. Alternatively, assert that the initial page is not
full before returning false.
server/app/space.go (1)

324-331: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider schemeAppError for the default-capability lookup failure.

spaceDefaultCapabilities resolves through the pluginapi client (channel scheme, then role permissions), not through the plugin store. storeAppError recognizes only store sentinels, so a core-supplied *AppError — for example a license denial or a refusal during the permissions migration — collapses to a generic 500 with the store error key. schemeAppError (server/app/service.go Lines 148-157) preserves that status and was added for this exact shape. SetSpaceDefaultCapabilities already routes scheme errors through it.

♻️ Proposed change
 	defaultCapabilities := knownDefaults
 	if defaultCapabilities == nil {
 		var err error
 		defaultCapabilities, err = s.spaceDefaultCapabilities(space)
 		if err != nil {
-			return nil, storeAppError("BuildSpaceWithAccess", err)
+			return nil, schemeAppError("BuildSpaceWithAccess", err)
 		}
 	}

Note: GetSpaceMembers and SetSpaceMemberCapabilities in server/app/space_members.go wrap the same lookups with storeAppError, so the same change may apply there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/space.go` around lines 324 - 331, Update the error handling in the
default-capability lookup within the space-building flow to use schemeAppError
instead of storeAppError, preserving core-supplied AppError status and keys from
spaceDefaultCapabilities. Also inspect GetSpaceMembers and
SetSpaceMemberCapabilities for the same pluginapi lookup pattern and route those
lookup failures through schemeAppError consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 141-149: Update the gomod_commit extraction in the CI cross-check
to support both standard Go pseudo-version forms, including tagless versions
like v0.0.0-<timestamp>-<sha>, by matching the trailing timestamp and commit
revision rather than requiring a base-version counter. When server/public is
present but no revision can be parsed, fail the step instead of printing the
released-version message and exiting successfully; retain the skip behavior only
when the dependency is genuinely pinned to a released version.

In `@server/e2e/container_test.go`:
- Around line 143-159: Update waitForPhase2Migration to inspect each non-nil
error from adminClient.GetSchemes and continue polling only when its message
contains the known pending condition
app.schemes.is_phase_2_migration_completed.not_completed.app_error; return
unrelated errors immediately, preserving the existing timeout and context
handling for the pending-migration case. Add the errors import as needed and
reuse the existing strings import.

In `@server/e2e/README.md`:
- Around line 36-37: Update the “Running it” description in the e2e README to
qualify that make test-e2e runs build/build-core-image.sh only when CORE_IMAGE
is not namespaced (does not contain “/”); otherwise it uses the provided image.
Keep the existing plugin-bundle behavior and command flow unchanged.

---

Nitpick comments:
In `@build/build-core-image.sh`:
- Line 138: Remove the duplicated default fallback from the IMAGE_TAG
assignment, reusing the already-initialized CORE_IMAGE value established earlier
in the script. Keep the existing image tag behavior unchanged while ensuring the
default literal appears only in the initial CORE_IMAGE initialization.

In `@server/api_handler_test.go`:
- Around line 149-160: Remove the unused db parameter from seedSpaceInTeam and
update seedSpace and every other caller to stop forwarding it, while preserving
the existing space-creation behavior through testutil.MustCreateSpace.

In `@server/app/space_test.go`:
- Around line 646-648: Replace the ineffective require.NotEmpty assertion on
pooledSchemeID in the test with an assertion that the abandoned channel’s scheme
attachment references the pooled scheme, matching the established assertion
pattern in TestServiceCreateSpace_CustomSchemeConfiguredAfterChannelAttach. Keep
the existing DeleteChannel and DeleteScheme expectations unchanged.
- Around line 588-611: The custom scheme setup is duplicated between
stubCustomSchemeCreate and stubCustomSchemeCreateFailingPatch. Consolidate both
into one shared stubSchemeCreate helper that accepts a role-name prefix and
optional patch error, reuses the common scheme, role registration, and role
stubbing setup, and configures PatchRole to either succeed or return the
supplied error; update both callers to use it.

In `@server/app/space.go`:
- Around line 324-331: Update the error handling in the default-capability
lookup within the space-building flow to use schemeAppError instead of
storeAppError, preserving core-supplied AppError status and keys from
spaceDefaultCapabilities. Also inspect GetSpaceMembers and
SetSpaceMemberCapabilities for the same pluginapi lookup pattern and route those
lookup failures through schemeAppError consistently.

In `@server/e2e/helpers_test.go`:
- Around line 126-140: Update spaceHasMember to handle pagination instead of
inspecting only the initial response page: follow the response’s has_more
indicator using the pagination field names defined by paginatedResponse in
server/api.go, requesting subsequent pages until the user is found or no pages
remain. Alternatively, assert that the initial page is not full before returning
false.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89ddf508-2943-4048-ba3f-b62b0911e9b0

📥 Commits

Reviewing files that changed from the base of the PR and between 8bf4f9a and 14034a3.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (52)
  • .github/workflows/ci.yml
  • Makefile
  • assets/i18n/en.json
  • build/build-core-image.sh
  • go.mod
  • server/api.go
  • server/api_handler_test.go
  • server/api_page.go
  • server/api_page_drafts.go
  • server/api_page_drafts_test.go
  • server/api_page_presence.go
  • server/api_space.go
  • server/app/page_draft.go
  • server/app/page_draft_test.go
  • server/app/page_duplicate_test.go
  • server/app/page_hierarchy.go
  • server/app/page_move_test.go
  • server/app/page_move_to_space_test.go
  • server/app/page_reorder_test.go
  • server/app/permissions.go
  • server/app/permissions_test.go
  • server/app/scheme.go
  • server/app/scheme_test.go
  • server/app/service.go
  • server/app/service_test.go
  • server/app/space.go
  • server/app/space_access.go
  • server/app/space_members.go
  • server/app/space_test.go
  • server/app/ws_events.go
  • server/app/ws_events_test.go
  • server/e2e/README.md
  • server/e2e/container_test.go
  • server/e2e/helpers_test.go
  • server/e2e/scenarios_test.go
  • server/internal/testutil/fixtures.go
  • server/internal/testutil/permstub.go
  • server/internal/testutil/schemestub.go
  • server/model/space.go
  • server/model/space_capabilities.go
  • server/model/space_capabilities_test.go
  • server/model/space_test.go
  • server/store/migrations/000006_add_viewaccess_to_spaces.down.sql
  • server/store/migrations/000006_add_viewaccess_to_spaces.up.sql
  • server/store/page_move.go
  • server/store/page_move_test.go
  • server/store/page_store.go
  • server/store/space_store.go
  • server/store/store.go
  • server/store/store_test.go
  • webapp/i18n/en.json
  • webapp/src/components/spaces_sidebar/space_item_menu.tsx

Comment thread .github/workflows/ci.yml
Comment on lines +141 to +149
# The shape check above cannot catch the drift that matters. go.mod pins server/public to a
# core commit; CORE_IMAGE is a separate repository variable a human sets. Nothing ties them
# together, so bumping the pin without updating the variable leaves this suite green against
# a core image that predates the very changes under test.
gomod_commit="$(grep -oE 'server/public v[0-9]+\.[0-9]+\.[0-9]+-[0-9]+\.[0-9]+-[0-9a-f]+' go.mod | grep -oE '[0-9a-f]{12}$' || true)"
if [[ -z "$gomod_commit" ]]; then
echo "server/public is pinned to a released version, not a core commit; skipping the sha cross-check."
exit 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The go.mod pseudo-version pattern misses the tagless shape, so the cross-check can silently self-disable.

The pattern requires a base release plus a pre-release counter: v[0-9]+\.[0-9]+\.[0-9]+-[0-9]+\.[0-9]+-[0-9a-f]+. It matches the current pin (v0.4.4-0.20260804125319-416829fe8ff3). It does not match the other standard Go pseudo-version shape, v0.0.0-20260804125319-416829fe8ff3, which has no -<n>. counter group. go mod tidy produces that shape whenever the pinned commit is not descended from a tag.

In that case gomod_commit is empty, the step prints "pinned to a released version" and exits 0. The drift the comment above describes then goes unreported, which is the outcome this step exists to prevent.

Match the revision by position instead of by base-version shape, and fail rather than exit 0 when server/public is present but the revision cannot be parsed.

🛠️ Proposed fix
-          gomod_commit="$(grep -oE 'server/public v[0-9]+\.[0-9]+\.[0-9]+-[0-9]+\.[0-9]+-[0-9a-f]+' go.mod | grep -oE '[0-9a-f]{12}$' || true)"
-          if [[ -z "$gomod_commit" ]]; then
+          gomod_version="$(grep -oE 'server/public v[^[:space:]]+' go.mod | head -1 | awk '{print $2}')"
+          if [[ -z "$gomod_version" ]]; then
+            echo "could not find a server/public requirement in go.mod." >&2
+            exit 1
+          fi
+          # A pseudo-version always ends in -<14-digit timestamp>-<12-hex revision>, whatever its
+          # base version looks like. Anything else is a real released version.
+          gomod_commit="$(printf '%s' "$gomod_version" | grep -oE '\-[0-9]{14}-[0-9a-f]{12}$' | grep -oE '[0-9a-f]{12}$' || true)"
+          if [[ -z "$gomod_commit" ]]; then
             echo "server/public is pinned to a released version, not a core commit; skipping the sha cross-check."
             exit 0
           fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 141 - 149, Update the gomod_commit
extraction in the CI cross-check to support both standard Go pseudo-version
forms, including tagless versions like v0.0.0-<timestamp>-<sha>, by matching the
trailing timestamp and commit revision rather than requiring a base-version
counter. When server/public is present but no revision can be parsed, fail the
step instead of printing the released-version message and exiting successfully;
retain the skip behavior only when the dependency is genuinely pinned to a
released version.

Comment on lines +143 to +159
func waitForPhase2Migration(ctx context.Context, adminClient *mmmodel.Client4) error {
deadline := time.Now().Add(2 * time.Minute)
var lastErr error
for time.Now().Before(deadline) {
_, _, err := adminClient.GetSchemes(ctx, "", 0, 1)
if err == nil {
return nil
}
lastErr = err
select {
case <-ctx.Done():
return fmt.Errorf("waiting for advanced-permissions phase-2 migration: %w (last error: %v)", ctx.Err(), lastErr)
case <-time.After(2 * time.Second):
}
}
return fmt.Errorf("advanced-permissions phase-2 migration did not complete within 2 minutes: %w", lastErr)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

The poll loop cannot distinguish "migration pending" from an unrelated failure.

Line 147 treats every non-nil error as "migration not complete". The comment at lines 127-132 names the specific condition being waited on, app.schemes.is_phase_2_migration_completed.not_completed.app_error, and the doc comment says the loop polls "until it stops reporting 'not completed'". The code never inspects the error id.

A bad admin token, a 500, or a connection reset therefore loops for the full two minutes and then reports that the migration did not complete. That consumes two of the caller's three boot minutes and points the reader at the wrong cause.

Return immediately on an error that is not the pending-migration condition.

🛠️ Proposed fix
 	for time.Now().Before(deadline) {
 		_, _, err := adminClient.GetSchemes(ctx, "", 0, 1)
 		if err == nil {
 			return nil
 		}
+		// Only the pending-migration condition is worth waiting on. Any other failure (auth, a
+		// 500, a dropped connection) will not resolve itself, so surface it instead of spending
+		// the remaining boot budget on it and then blaming the migration.
+		var appErr *mmmodel.AppError
+		if !errors.As(err, &appErr) || !strings.Contains(appErr.Id, "is_phase_2_migration_completed") {
+			return fmt.Errorf("polling for the advanced-permissions phase-2 migration failed: %w", err)
+		}
 		lastErr = err

The fix needs errors and strings in the import block; strings is already imported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/e2e/container_test.go` around lines 143 - 159, Update
waitForPhase2Migration to inspect each non-nil error from adminClient.GetSchemes
and continue polling only when its message contains the known pending condition
app.schemes.is_phase_2_migration_completed.not_completed.app_error; return
unrelated errors immediately, preserving the existing timeout and context
handling for the pending-migration case. Add the errors import as needed and
reuse the existing strings import.

Comment thread server/e2e/README.md
Comment on lines +36 to +37
This builds the core image (`build/build-core-image.sh`), ensures the plugin bundle exists
(`make dist` if missing), then runs:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

"Running it" contradicts the namespaced-CORE_IMAGE rule stated above.

This section says make test-e2e "builds the core image (build/build-core-image.sh)" without qualification. The test-e2e target now skips that build when CORE_IMAGE contains a /, which lines 24-26 describe. A reader who follows this section expects a local build in the CI-style configuration and does not get one.

📝 Proposed fix
-This builds the core image (`build/build-core-image.sh`), ensures the plugin bundle exists
-(`make dist` if missing), then runs:
+This ensures the plugin bundle exists (`make dist` if it is missing or carries no linux binary for
+the Docker daemon's architecture), builds the core image with `build/build-core-image.sh` unless
+`CORE_IMAGE` is namespaced, then runs:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
This builds the core image (`build/build-core-image.sh`), ensures the plugin bundle exists
(`make dist` if missing), then runs:
This ensures the plugin bundle exists (`make dist` if it is missing or carries no linux binary for
the Docker daemon's architecture), builds the core image with `build/build-core-image.sh` unless
`CORE_IMAGE` is namespaced, then runs:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/e2e/README.md` around lines 36 - 37, Update the “Running it”
description in the e2e README to qualify that make test-e2e runs
build/build-core-image.sh only when CORE_IMAGE is not namespaced (does not
contain “/”); otherwise it uses the provided image. Keep the existing
plugin-bundle behavior and command flow unchanged.

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