Skip to content

Create FIPS complaint boards plugin#120

Open
stafot wants to merge 26 commits into
mainfrom
CLD-9417-build-boards-fips-compliant
Open

Create FIPS complaint boards plugin#120
stafot wants to merge 26 commits into
mainfrom
CLD-9417-build-boards-fips-compliant

Conversation

@stafot

@stafot stafot commented Jul 21, 2025

Copy link
Copy Markdown
Contributor

🔒 Add FIPS 140-2 Compliance Support for Mattermost Boards Plugin

Summary

This PR implements FIPS (Federal Information Processing Standards) 140-2 compliance for the Mattermost Boards plugin, enabling secure deployment in government and enterprise environments that require cryptographic validation.
Changes Made

🛠️ FIPS Build Infrastructure

Added FIPS build targets: server-fips, bundle-fips, dist-fips, dist-all
FIPS Docker integration: Uses cgr.dev/mattermost.com/go-msft-fips:1.24.6 for FIPS-validated Go compilation
Dual distribution support: Builds both regular and FIPS-compliant plugin bundles

🔧 Plugin Compatibility Fixes

Binary naming standardization: FIPS bundles now contain correctly named binaries (plugin-linux-amd64) for server compatibility
Fixed broken variable references: Resolved FOCALBOARD_PLUGIN_PATH → BOARD_PLUGIN_PATH inconsistencies
Updated Go version: Upgraded to Go 1.24.6 for latest security patches

Testing

✅ FIPS server compatibility: Tested with FIPS-enabled Mattermost server
✅ Plugin loading: Verified successful installation and startup
✅ Functionality: Confirmed full plugin operation in FIPS mode
✅ Security compliance: Maintains FIPS 140-2 cryptographic standards

Breaking Changes
None - fully backward compatible with existing installations.

Deployment

FIPS-compliant organizations can now deploy the Boards plugin using the -fips bundle variant while maintaining full functionality and security compliance.
Related: Part of broader FIPS compliance initiative across Mattermost plugin ecosystem.

Ticket Link

https://mattermost.atlassian.net/browse/CLD-9417

Change Impact: 🟡 Medium

Regression Risk: The changes introduce moderate regression risk, though several mitigating factors reduce it:

  • Logic Refactoring with Equivalence Claims: Most control-flow changes convert if/else chains to switch statements and rewrite boolean expressions (De Morgan transformations). These are stated to be behaviorally equivalent but introduce code path verification requirements.
  • Category Board Lookup Fix: The change from categoryBoard.Category.ID to categoryBoard.ID appears to fix a structural access issue, reducing rather than increasing regression risk. The grep results confirm categoryBoard.ID is the correct field pattern used elsewhere in the codebase.
  • Distributed Changes Across Core Modules: Changes span server/api, server/app, server/model, and test files, affecting board management, permissions, and category operations—all critical paths.
  • FIPS Build Infrastructure: New build targets (dist-fips, bundle-fips) and Docker-based compilation paths are additive and isolated to the build system, reducing impact on runtime behavior.
  • Variable Naming Propagation: Renaming FOCALBOARD_PLUGIN_PATHBOARD_PLUGIN_PATH and PLUGIN_NAMEPLUGIN_ID in artifact naming requires coordinated deployment updates.
  • Good Test Coverage: Repository contains 87 test files, including dedicated tests for affected modules (category_boards_test.go, boards_test.go, api_test.go, integrationtests).

QA Recommendation: Standard automated test execution should be sufficient as a primary gate, but recommended focused manual validation on:

  • Board category operations (assignment, duplication, lookup)
  • Permission enforcement workflows (member addition, role changes)
  • Plugin installation/startup with updated artifact naming conventions
  • FIPS-mode deployment and cryptographic operations validation
  • Both standard and FIPS artifact production in CI/CD pipeline

Generated by CodeRabbitAI

Comment thread .github/workflows/ci.yml Outdated
Comment thread Makefile

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

Thanks Stavro, a few comments:

Comment thread Makefile Outdated
Comment thread Makefile Outdated
Comment thread go.mod Outdated
Comment thread server/app/files.go
Comment thread server/integrationtests/pluginteststore.go

@harshilsharma63 harshilsharma63 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Only change required is not returning that error there as in comment

Comment thread .github/workflows/ci.yml Outdated
Comment thread Makefile Outdated
@harshilsharma63

Copy link
Copy Markdown
Member

Deferring the Makefile changes related to FIPS to @esarafianou , but boards changes look good.

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

@stafot great work, I didn't know we were not building the Boards plugin during CI at all.

Just one comment for naming. It should stay Boards to distinguish itself from the community supported focalboard.

Comment thread Makefile Outdated
Comment thread Makefile
@stafot
stafot requested a review from esarafianou August 20, 2025 12:01
@stafot
stafot force-pushed the CLD-9417-build-boards-fips-compliant branch from 13ffe4c to cd67501 Compare September 3, 2025 13:01
Signed-off-by: Stavros Foteinopoulos <stafot@gmail.com>
Signed-off-by: Stavros Foteinopoulos <stafot@gmail.com>
Signed-off-by: Stavros Foteinopoulos <stafot@gmail.com>
Signed-off-by: Stavros Foteinopoulos <stafot@gmail.com>
Signed-off-by: Stavros Foteinopoulos <stafot@gmail.com>
Signed-off-by: Stavros Foteinopoulos <stafot@gmail.com>
Signed-off-by: Stavros Foteinopoulos <stafot@gmail.com>
Signed-off-by: Stavros Foteinopoulos <stafot@gmail.com>
Signed-off-by: Stavros Foteinopoulos <stafot@gmail.com>
@mattermost-build

Copy link
Copy Markdown
Contributor

This PR has been automatically labelled "stale" because it hasn't had recent activity.
A core team member will check in on the status of the PR to help with questions.
Thank you for your contribution!

@agarciamontoro

Copy link
Copy Markdown
Member

Merged up to the v9.1.7 tag for prepackaging for MM v11.1

@mattermost-build

Copy link
Copy Markdown
Contributor

This PR has been automatically labelled "stale" because it hasn't had recent activity.
A core team member will check in on the status of the PR to help with questions.
Thank you for your contribution!

@agarciamontoro

Copy link
Copy Markdown
Member

Merged up to the v9.2.1 tag for prepackaging for MM v11.2

@mattermost-build

Copy link
Copy Markdown
Contributor

This PR has been automatically labelled "stale" because it hasn't had recent activity.
A core team member will check in on the status of the PR to help with questions.
Thank you for your contribution!

@agarciamontoro

Copy link
Copy Markdown
Member

Merged up to the v9.2.2 tag for prepackaging for MM v11.4

@agarciamontoro

Copy link
Copy Markdown
Member

Merged up to the v9.2.4 tag for prepackaging for MM v11.7

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request updates the build and CI infrastructure to use Go 1.24.6 and golangci-lint v2.1.6, introduces FIPS build support in the Makefile, refactors CI workflows with inline job execution and artifact handling, standardizes error-value handling across the codebase, updates linting rules, and applies control-flow improvements (if/else to switch statements) while fixing category board lookup logic.

Changes

Cohort / File(s) Summary
CI/CD Workflows
.github/workflows/ci.yml, .github/workflows/lint-server.yml
Updated Go and golangci-lint versions; replaced reusable workflow for distribution with inline job including Node setup, caching, chainctl authentication, tooling builds, and artifact uploads without S3 distribution.
Build Configuration
.gitignore, Makefile
Added dist-fips and .build-cache/ to ignore patterns; refactored Makefile to support FIPS builds via Docker container, added shared bundle-file copying, switched from GO111MODULE to explicit GOBIN export, updated packaging to use PLUGIN_ID instead of PLUGIN_NAME, and added dist-all target composing normal and FIPS distributions.
Manifest Tooling
build/manifest/main.go
Added dist and dist-fips subcommands to generate plugin manifest JSON files; updated output format for dumpPluginID and dumpPluginVersion to include trailing newlines.
Linting Configuration
server/.golangci.yml, server/plugin.go, server/services/metrics/service.go, server/web/webserver.go, server/web/webserver_test.go
Simplified golangci-lint configuration from broad explicit-enable list to focused set of linters; added //nolint:staticcheck directives throughout to suppress QF1008 warnings regarding explicit type qualifiers.
Error-Value Handling (fmt/Fprintf/Printf)
server/api/api.go, server/api/archive.go, server/assets/build-template-archive/main.go, server/client/client.go, server/services/store/generators/main.go, server/services/webhook/webhook.go
Standardized handling of ignored return values from fmt.Fprint[f], writer.Close(), and resp.Body.Close() by explicitly assigning to _ instead of implicit ignoring.
Error-Value Handling (Deferred Close)
server/api/api_test.go, server/api/files.go, server/app/export.go, server/boards/boardsapp_test.go, server/integrationtests/permissions_test.go, server/services/store/sqlstore/data_migrations.go, server/services/store/sqlstore/data_migrations_test.go, server/services/store/sqlstore/migrationstests/boards_migrator_test.go, server/services/store/sqlstore/util.go, server/ws/server.go
Refactored deferred close operations to explicitly discard returned errors via defer func() { _ = resource.Close() }() pattern instead of direct defer resource.Close().
Error-Value Handling (os.Setenv/RemoveAll)
server/boards/data_retention_test.go, server/integrationtests/clienttestlib.go, server/integrationtests/compliance_test.go, server/services/store/sqlstore/helpers_test.go, server/services/telemetry/telemetry_test.go
Standardized os.Setenv and os.RemoveAll error handling by explicitly assigning to _ instead of implicit ignoring.
Control Flow Refactoring
server/app/boards.go, server/services/store/sqlstore/data_migrations.go, server/services/store/sqlstore/pluginteststore.go, server/model/properties_test.go
Converted chained if/else conditional checks to switch statements with explicit case clauses for improved code clarity.
Logic Improvements
server/app/category_boards.go, server/api/members.go, server/model/team.go, server/boards/boardsapp_util.go, server/boards/configuration.go
Fixed category board lookup to use existingCategoryBoards[i].ID instead of .Category.ID; simplified boolean derivations by removing intermediate variables and conditional assignments; refactored permission logic using De Morgan equivalence.
Test Error Checking
server/integrationtests/board_test.go, server/services/store/sqlstore/migrationstests/deleted_membership_boards_migration_test.go, server/services/store/sqlstore/migrationstests/migrate_34_test.go, server/services/store/sqlstore/migrationstests/migration36_test.go, server/services/store/sqlstore/migrationstests/migration_18_test.go
Enhanced test robustness by capturing and asserting database operation errors in migration and integration tests; fixed category board ID assignment in duplicate board test.
Store Migration Improvements
server/services/store/sqlstore/migrate.go
Enhanced deferred cleanup in database migrations to capture and log Close() errors from SQL connections and Morph engine instead of silently discarding them.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

2: Dev Review, 3: QA Review, 3: Security Review, Setup Cloud Test Server

Suggested reviewers

  • harshilsharma63
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Create FIPS complaint boards plugin' directly describes the main objective: adding FIPS 140-2 compliance support to the Mattermost Boards plugin through new FIPS build infrastructure and dual distribution support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch CLD-9417-build-boards-fips-compliant

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@dryrunsecurity

dryrunsecurity Bot commented Apr 17, 2026

Copy link
Copy Markdown

DryRun Security

This pull request introduces a high-severity authorization bypass in server/api/members.go where users with only PermissionManageBoardProperties on Open boards can add members and set their role directly from the request payload without any PermissionManageBoardRoles check. This can allow privilege escalation by granting Admin access to themselves or others.

🟠 Potential Authorization Bypass in Board Member management in server/api/members.go (drs_72f33e8e)
Vulnerability Potential Authorization Bypass in Board Member management
Description The modified permission check allows users with PermissionManageBoardProperties to add members to Open boards. However, within the handleAddMember function, the new member's role (Admin/Editor/Viewer) is directly taken from the request payload. The function lacks a check to ensure that the user adding the new member has PermissionManageBoardRoles before granting them administrative privileges, or that the assigned roles do not exceed the privileges the adder is permitted to grant. As such, a user who is only supposed to have ManageBoardProperties for an Open board can, via this API, grant themselves or others Admin roles.

}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) &&
(board.Type != model.BoardTypeOpen || !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties)) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return
}


Comment to provide feedback on these findings.

Report false positive: @dryrunsecurity fp [FINDING ID] [FEEDBACK]
Report low-impact: @dryrunsecurity nit [FINDING ID] [FEEDBACK]

Example: @dryrunsecurity fp drs_90eda195 This code is not user-facing

All finding details can be found in the DryRun Security Dashboard.

@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: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/services/store/generators/main.go (1)

126-134: ⚠️ Potential issue | 🟡 Minor

Close the opened file via defer to avoid leaks on read errors.

If io.ReadAll fails, the current flow returns before Line 134 and leaves the descriptor open.

💡 Proposed fix
 	file, err := os.Open("store.go")
 	if err != nil {
 		return nil, fmt.Errorf("unable to open store/store.go file: %w", err)
 	}
+	defer func() { _ = file.Close() }()
 	src, err := io.ReadAll(file)
 	if err != nil {
 		return nil, err
 	}
-	_ = file.Close()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/store/generators/main.go` around lines 126 - 134, The file
descriptor opened with os.Open("store.go") is closed only after ReadAll, so if
io.ReadAll returns an error the descriptor leaks; fix by calling defer
file.Close() immediately after the successful os.Open (i.e., right after the err
check for os.Open) to ensure closure on all return paths, and remove the later
manual _ = file.Close() to avoid double-close confusion.
server/.golangci.yml (1)

1-47: ⚠️ Potential issue | 🟠 Major

Config uses deprecated golangci-lint v1 syntax in v2; exclusions will not work.

With version: 2, this configuration has three critical issues:

  1. Top-level linters-settings: (lines 6–15) is ignored. In v2, linter settings must be nested under linters.settings:. The staticcheck configuration here will not be applied, making the //nolint:staticcheck // QF1008 suppressions scattered in the codebase the only active enforcement.

  2. issues.exclude-files and issues.exclude-rules (lines 32–47) have been removed in v2. These v1 keys are not recognized. Exclusions must now be configured under linters.exclusions: with the new structure:

    • issues.exclude-fileslinters.exclusions.paths:
    • issues.exclude-ruleslinters.exclusions.rules:
  3. Duplicate govet settings. Lines 6–9 (top-level) and lines 19–22 (nested) both configure govet, but only the nested version is read.

The correct v2 structure consolidates all linter settings under linters.settings: and exclusions under linters.exclusions::

♻️ Corrected v2 configuration
 version: 2

 run:
   timeout: 5m

 linters:
   disable-all: true
   settings:
     govet:
       enable:
         - shadow
       disable:
         - fieldalignment
     staticcheck:
       checks:
         - all
         - "-QF1008"
   enable:
     - errcheck
     - govet
     - ineffassign
     - staticcheck
     - unused
     - misspell

-issues:
-  exclude-files:
-    - product/boards_product.go
-    - services/store/sqlstore/migrations
-  exclude-rules:
-    - path: server/manifest.go
-      linters:
-        - unused
-    - path: server/configuration.go  
-      linters:
-        - unused
-    - path: _test\.go
-      linters:
-        - bodyclose
-    - linters:
-        - staticcheck
-      source: QF1008
+linters:
+  exclusions:
+    paths:
+      - product/boards_product.go
+      - services/store/sqlstore/migrations
+    rules:
+      - path: server/manifest.go
+        linters:
+          - unused
+      - path: server/configuration.go
+        linters:
+          - unused
+      - path: _test\.go
+        linters:
+          - bodyclose
+      - linters:
+          - staticcheck
+        source: QF1008

Use golangci-lint migrate to convert this configuration to v2 format automatically.

Minor: trailing whitespace on lines 30 and 39.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/.golangci.yml` around lines 1 - 47, The config uses deprecated v1 keys
so linters settings and exclusions are ignored; move the top-level
"linters-settings:" block into "linters.settings:" and consolidate the duplicate
"govet" entries under that nested "linters.settings.govet", and replace
"issues.exclude-files" and "issues.exclude-rules" with the v2 structure under
"linters.exclusions" (use "linters.exclusions.paths:" for the former and
"linters.exclusions.rules:" for the latter, preserving the same paths and rule
entries and sources like QF1008); run "golangci-lint migrate" or manually
restructure the YAML accordingly and remove trailing whitespace on the blank
lines.
🧹 Nitpick comments (11)
server/services/store/sqlstore/migrate.go (1)

146-151: Minor: defer assigns to outer err, which is harmless here but fragile.

err = db.Close() on line 148 reassigns the enclosing function-scoped err (declared at line 129). Since Migrate() has unnamed returns, this does not change the returned value — but if someone later converts the signature to a named return (err error), this deferred Close would silently overwrite the real migration error. Using := / a local variable makes the intent explicit and forward-safe.

Proposed tweak
 		defer func() {
 			s.logger.Debug("Closing migrations connection")
-			if err = db.Close(); err != nil {
-				s.logger.Error("Error closing migrations connection", mlog.Err(err))
+			if cErr := db.Close(); cErr != nil {
+				s.logger.Error("Error closing migrations connection", mlog.Err(cErr))
 			}
 		}()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/store/sqlstore/migrate.go` around lines 146 - 151, The defer
in Migrate() currently does "if err = db.Close(); err != nil { ... }" which
reassigns the outer variable err; change it to capture the Close error in a new
local variable (e.g. localErr or cerr) using := so you don't overwrite the
function-scoped err—update the defer block around db.Close() to use a distinct
local variable and log that local error via s.logger.Error(...) without
assigning to the outer err.
server/integrationtests/board_test.go (1)

2064-2071: Optional: break once found.

The .Category.ID.ID change is a no-op refactor (embedded field promotion). Separately, the loop continues scanning all categories/metadata after a match; adding a labeled break would make intent clearer and avoid unnecessary iterations, though correctness isn't affected since each board should belong to only one category.

Proposed tweak
 		var duplicateBoardCategoryID string
+	outer:
 		for _, categoryBoard := range userCategoryBoards {
 			for _, boardMetadata := range categoryBoard.BoardMetadata {
 				if boardMetadata.BoardID == duplicateBoard.ID {
 					duplicateBoardCategoryID = categoryBoard.ID
+					break outer
 				}
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/integrationtests/board_test.go` around lines 2064 - 2071, The code
loops through userCategoryBoards and their BoardMetadata to set
duplicateBoardCategoryID when boardMetadata.BoardID == duplicateBoard.ID but
continues scanning unnecessarily; modify the nested loops (iterating
userCategoryBoards and categoryBoard.BoardMetadata) to exit early once a match
is found by using a labeled break (or setting a flag and breaking both loops) so
that after assigning duplicateBoardCategoryID the outer loop stops immediately,
improving clarity and avoiding extra iterations.
server/services/store/sqlstore/migrationstests/migration_18_test.go (1)

100-100: Prefer asserting the unmarshal error over silencing it.

Using _ = json.Unmarshal(...) trades a lint warning for a silent failure mode: if the JSON is malformed, the subsequent require.NotNil(fields["columnCalculations"]) will report a misleading failure instead of pointing at the real cause. Assert it instead.

Proposed fix
-		_ = json.Unmarshal([]byte(view.Fields), &fields)
+		require.NoError(t, json.Unmarshal([]byte(view.Fields), &fields))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/store/sqlstore/migrationstests/migration_18_test.go` at line
100, The current test silences JSON unmarshal errors by using `_ =
json.Unmarshal([]byte(view.Fields), &fields)` which hides parse failures; change
this to capture the error from json.Unmarshal on view.Fields into a variable
(err) and assert it is nil using the test helper (e.g., require.NoError(t, err)
or require.Nil(t, err)) before accessing fields["columnCalculations"] so any
malformed JSON fails with the actual unmarshal error.
server/client/client.go (1)

768-768: Consider logging/propagating the multipart.Writer.Close() error instead of silently discarding it.

writer.Close() finalizes the multipart body by writing the trailing boundary. If it fails, the server will receive a truncated/malformed multipart body and produce a confusing error far from the real root cause. Since both methods already return a *Response with an Error field, returning the close error would give callers useful diagnostics.

Minor — not a correctness bug in practice (writing to bytes.Buffer rarely fails), but discarding the error loses debuggability.

♻️ Suggested change
-	_ = writer.Close()
+	if err := writer.Close(); err != nil {
+		return nil, &Response{Error: err}  // (or `return &Response{Error: err}` in ImportArchive)
+	}

Also applies to: 883-883

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/client/client.go` at line 768, The multipart.Writer.Close() error is
being discarded; capture its returned error from writer.Close() in the relevant
methods (those that return *Response with an Error field) and propagate it into
the Response.Error instead of ignoring it. Locate the call to writer.Close() in
server/client/client.go (and the other occurrence) and if err := writer.Close();
err != nil { return &Response{Error: err} } (or set resp.Error = err before
returning) so callers receive the actual close error for better diagnostics.
build/manifest/main.go (1)

217-228: Recommended: use filepath.Join and assert the output directory exists.

Two small refinements:

  1. Building paths with fmt.Sprintf("%s/%s/plugin.json", ...) works on POSIX but is non-idiomatic; prefer filepath.Join for portability and clarity.
  2. os.WriteFile will fail if <outputDir>/<manifest.Id>/ doesn't exist. Today the Makefile bundle/bundle-fips targets pre-create it, so there's an implicit ordering contract — an os.MkdirAll here would make the helper robust when invoked directly.
♻️ Suggested refactor
 func distManifest(manifest *model.Manifest, outputDir string) error {
 	manifestBytes, err := json.MarshalIndent(manifest, "", "    ")
 	if err != nil {
 		return err
 	}
 
-	if err := os.WriteFile(fmt.Sprintf("%s/%s/plugin.json", outputDir, manifest.Id), manifestBytes, 0600); err != nil {
+	dir := filepath.Join(outputDir, manifest.Id)
+	if err := os.MkdirAll(dir, 0755); err != nil {
+		return errors.Wrap(err, "failed to create output directory")
+	}
+	if err := os.WriteFile(filepath.Join(dir, "plugin.json"), manifestBytes, 0600); err != nil {
 		return errors.Wrap(err, "failed to write plugin.json")
 	}
 
 	return nil
 }

Requires adding "path/filepath" to the imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build/manifest/main.go` around lines 217 - 228, In distManifest, replace the
manual path fmt.Sprintf("%s/%s/plugin.json", outputDir, manifest.Id) with
filepath.Join(outputDir, manifest.Id, "plugin.json") and ensure the directory
exists before writing by calling os.MkdirAll(filepath.Join(outputDir,
manifest.Id), 0755) (or similar) then call os.WriteFile; also add the
"path/filepath" import. This makes distManifest robust when the target directory
is missing and portable across platforms.
Makefile (3)

259-265: Factor the FIPS archive name into a variable (parity with BUNDLE_NAME).

$(PLUGIN_ID)-$(PLUGIN_VERSION)-fips.tar.gz is repeated three times (Lines 260, 262, 265) and cannot be overridden by callers the way BUNDLE_NAME ?= ... can. Define it once alongside BUNDLE_NAME so release tooling / downstream callers can override it consistently.

♻️ Suggested

Near Line 61:

 BUNDLE_NAME ?= $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz
+FIPS_BUNDLE_NAME ?= $(PLUGIN_ID)-$(PLUGIN_VERSION)-fips.tar.gz

Then in bundle-fips:

 ifeq ($(shell uname),Darwin)
-	cd dist-fips && tar --disable-copyfile -cvzf $(PLUGIN_ID)-$(PLUGIN_VERSION)-fips.tar.gz $(PLUGIN_ID)
+	cd dist-fips && tar --disable-copyfile -cvzf $(FIPS_BUNDLE_NAME) $(PLUGIN_ID)
 else
-	cd dist-fips && tar -cvzf $(PLUGIN_ID)-$(PLUGIN_VERSION)-fips.tar.gz $(PLUGIN_ID)
+	cd dist-fips && tar -cvzf $(FIPS_BUNDLE_NAME) $(PLUGIN_ID)
 endif

-	`@echo` "==> FIPS plugin built at: dist-fips/$(PLUGIN_ID)-$(PLUGIN_VERSION)-fips.tar.gz"
+	`@echo` "==> FIPS plugin built at: dist-fips/$(FIPS_BUNDLE_NAME)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 259 - 265, The Makefile repeats the literal archive
name "$(PLUGIN_ID)-$(PLUGIN_VERSION)-fips.tar.gz" in the bundle-fips target;
define a single overridable variable (e.g., FIPS_BUNDLE_NAME ?=
$(PLUGIN_ID)-$(PLUGIN_VERSION)-fips.tar.gz) alongside BUNDLE_NAME near where
BUNDLE_NAME is declared and then replace the three occurrences in the
bundle-fips target (the tar and the echo) to use $(FIPS_BUNDLE_NAME) instead of
the repeated literal so callers can override it consistently.

237-246: Redundant copy of plugin-linux-amd64-fips.

Lines 237–239 explicitly copy plugin-linux-amd64-fipsplugin-linux-amd64. The glob loop on Lines 241–246 matches plugin-*-fips* which also matches plugin-linux-amd64-fips and will copy it again to the same destination. Harmless but confusing. Either drop the explicit block and rely on the loop, or drop the loop if only amd64 is ever produced (see my earlier comment about FIPS arch coverage).

♻️ Simplified
-	# Copy FIPS binaries but rename them to standard names for server compatibility
-	if [ -f server/dist-fips/plugin-linux-amd64-fips ]; then \
-		cp server/dist-fips/plugin-linux-amd64-fips dist-fips/$(PLUGIN_ID)/server/dist/plugin-linux-amd64; \
-	fi
-	# Copy any other FIPS binaries and rename them
-	for file in server/dist-fips/plugin-*-fips*; do \
+	# Copy FIPS binaries and strip the -fips suffix so server loader finds them by OS/arch.
+	for file in server/dist-fips/plugin-*-fips*; do \
 		if [ -f "$$file" ]; then \
 			target=$$(basename "$$file" | sed 's/-fips//g'); \
 			cp "$$file" "dist-fips/$(PLUGIN_ID)/server/dist/$$target"; \
 		fi; \
 	done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 237 - 246, The Makefile currently copies
plugin-linux-amd64-fips explicitly and the for-loop over
server/dist-fips/plugin-*-fips* will copy it again, causing a redundant
duplicate; fix by removing the explicit block that handles
plugin-linux-amd64-fips (the lines that cp
server/dist-fips/plugin-linux-amd64-fips ...) and rely on the for-loop that sets
target=$$(basename "$$file" | sed 's/-fips//g') to handle all plugin-*-fips*
files, or alternatively adjust the for-loop to skip plugin-linux-amd64-fips if
you prefer keeping the explicit copy (refer to the for file in the loop and the
target variable to locate the code).

465-475: deploy-to-mattermost-directory unconditionally copies public/ and assets.

Unlike bundle (which gates public/ behind $(HAS_PUBLIC) via copy_bundle_files), this target always runs cp -r public $(BOARD_PLUGIN_PATH)/ and cp -r $(ASSETS_DIR) …. This plugin has both directories today so it works, but for parity with the bundler and future-proofing consider the same conditional guards. Low priority since this target is local-dev only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 465 - 475, The deploy-to-mattermost-directory target
unconditionally copies public and assets; change it to mirror the bundler guards
by wrapping the public and ASSETS_DIR copies in the same conditional checks used
by copy_bundle_files/$(HAS_PUBLIC) (or use directory-existence checks) so cp -r
public $(BOARD_PLUGIN_PATH)/ and cp -r $(ASSETS_DIR) $(BOARD_PLUGIN_PATH)/ only
run when public and ASSETS_DIR are present/allowed; update the Makefile block
around the deploy-to-mattermost-directory target and reference
BOARD_PLUGIN_PATH, ASSETS_DIR, and $(HAS_PUBLIC)/copy_bundle_files so it behaves
the same as bundle.
.github/workflows/ci.yml (2)

31-34: Pick a single source of truth for the Go version across jobs.

webapp-test resolves Go from focalboard/go.mod (Line 34) while dist uses the workflow-level GO_VERSION: 1.24.6 (Line 66). If go.mod ever drifts from 1.24.6, the two jobs compile with different toolchains and this PR's FIPS binary can get built on a different minor than what lint/test validated.

♻️ Suggested alignment

Either read from go.mod in both jobs, or use go-version: "${{ env.GO_VERSION }}" in both. Using go-version-file in both is simplest and removes the GO_VERSION env entirely:

-    - name: Set up Go
-      uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
-      with:
-        go-version: "${{ env.GO_VERSION }}"
-        cache: true
+    - name: Set up Go
+      uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
+      with:
+        go-version-file: focalboard/go.mod
+        cache: true

Also applies to: 63-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 31 - 34, The workflow uses two
different sources for the Go version—webapp-test uses actions/setup-go with
go-version-file: focalboard/go.mod while dist relies on the workflow-level
GO_VERSION env (1.24.6); unify them by choosing one source of truth and update
the other job: either set both jobs' actions/setup-go to use go-version-file:
focalboard/go.mod (remove GO_VERSION env) or set both to use go-version: "${{
env.GO_VERSION }}" (replace go-version-file in webapp-test), ensuring you update
the actions/setup-go invocations in the webapp-test and dist jobs and remove or
align the GO_VERSION variable accordingly.

92-95: Pin setup-chainctl by commit SHA, like the other actions in this file.

Every other uses: in this workflow is pinned to a 40-char SHA with the tag as a comment (v4.2.2, v5.2.0, v4.1.0, v4.2.3, v4.5.0). chainguard-dev/setup-chainctl@v0.3.2 relies on tag mutability, which weakens the supply-chain posture of the FIPS build — the very thing this PR is trying to strengthen. Use chainguard-dev/setup-chainctl@f4ed65b781b048c44d4f033ae854c025c5531c19 # v0.3.2 instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 92 - 95, Replace the mutable tag
reference in the workflow step that currently reads "uses:
chainguard-dev/setup-chainctl@v0.3.2" with the pinned commit SHA and add the
original tag as a trailing comment (e.g., use
"chainguard-dev/setup-chainctl@f4ed65b781b048c44d4f033ae854c025c5531c19 #
v0.3.2"); update the step named "ci/setup-chainctl" to use the SHA-pinned ref
while preserving the existing "with: identity: ${{ secrets.CHAINGUARD_IDENTITY
}}" input.
.github/workflows/lint-server.yml (1)

41-41: Consider pinning the install.sh ref to the same version tag.

The install script is currently fetched from master, then piped into sh. While the tool version (v2.1.6) is pinned, the installer itself floats with whatever is on master at run time. For improved supply-chain security, reference the same version tag for the script (and apply the same change in .github/workflows/ci.yml at line 45):

🔧 Suggested change
-        run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.6
+        run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v2.1.6/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.6
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/lint-server.yml at line 41, Replace the floating installer
URL that pipes the install.sh from master with a tag-pinned URL that matches the
tool version (change the curl target from
raw.githubusercontent.com/.../master/install.sh to the tag-specific path e.g.
raw.githubusercontent.com/.../v2.1.6/install.sh) so the installer script is
pinned to v2.1.6; apply the same replacement to the identical curl line in the
other CI workflow where the installer is fetched.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 76-108: The CI currently caches and installs focalboard/webapp
node_modules in the steps named "Cache webapp node modules" and "Setup webapp
npm deps", but those are always deleted because the Makefile target dist-all
declares clean as a prerequisite (target dist-all -> clean) and clean removes
webapp/node_modules; fix by removing clean from the dist-all prerequisites in
the Makefile (edit the dist-all target to no longer depend on clean) so the
cached node_modules survive the make dist-all run, or alternatively remove the
two CI steps ("Cache webapp node modules" and "Setup webapp npm deps") from
.github/workflows/ci.yml if you prefer to keep clean in dist-all.

In `@Makefile`:
- Around line 140-152: The fallback Docker-login branch in the Makefile is using
Makefile expansion ($(CHAINGUARD_DEV_USERNAME)/$(CHAINGUARD_DEV_TOKEN)) instead
of reading the CI shell environment, so the tests [ -n
"$(CHAINGUARD_DEV_USERNAME)" ] always fail; change those Make-variable
references to shell env references by escaping the dollar sign (use
$$CHAINGUARD_DEV_USERNAME and $$CHAINGUARD_DEV_TOKEN wherever they appear in the
conditional and the docker login command) so the shell invoked by the recipe
reads the CI-provided environment variables at runtime.
- Around line 283-296: The dist-all target currently claims a parallel build but
invokes $(MAKE) dist and $(MAKE) dist-fips sequentially and causes the webapp to
be built twice because both dist and dist-fips depend on the webapp target; fix
by either (A) adjusting the echo to remove the "parallel" claim and keep
sequential invocation, or (B) avoid duplicate webapp builds by removing webapp
from the prerequisites of the dist-fips target (so dist-all runs dist then
dist-fips reusing the already-built webapp) or by making the webapp target
idempotent; update the dist-all message string accordingly and ensure references
to webapp/pack and server/dist remain correct when you remove the duplicate
prerequisite.
- Around line 132-172: The server-fips target produces only an amd64 binary
(server/dist-fips/plugin-linux-amd64-fips) because the FIPS Docker image
(FIPS_IMAGE) is amd64-only; add a short comment above the server-fips target
documenting this amd64-only constraint and why cross-compilation isn't
supported. Also add an explicit host-architecture check in the server-fips
recipe (use uname -m or similar) and abort early with a clear error message if
the host is not x86_64, writing the FIPS_BUILD_FAILED.txt placeholder and
exiting non-zero so admins installing the bundle see a deterministic failure;
reference the server-fips target, FIPS_IMAGE, output filename, and
FIPS_BUILD_FAILED.txt when making the changes.

In `@server/api/api.go`:
- Line 214: The line containing "fmt.Fprint(w, message)" has extra leading tabs
and should be indented to match the surrounding function block (single tab per
indent level); locate the statement (fmt.Fprint(w, message)) in
server/api/api.go and replace the extra tabs so it uses the same one-tab
indentation as the adjacent lines, then run gofmt/goimports (or ensure
formatting matches gofmt style) so the file's indentation is consistent.

In `@server/api/archive.go`:
- Line 155: Fix the mis-indented error write that uses fmt.Fprintf(w, "%v", err)
so it aligns with the surrounding if/return block and is gofmt-compliant; also
set a proper response status before writing (e.g., call
w.WriteHeader(http.StatusBadRequest) prior to the fmt.Fprintf) or replace the
raw write with the existing helper (e.g., a.errorResponse(w, req,
http.StatusBadRequest, err.Error())) so the client gets a non-200 status for the
error.

In `@server/boards/boardsapp_util.go`:
- Line 63: The assignment for enableBoardsDeletion currently checks only that
mmconfig.DataRetentionSettings.EnableBoardsDeletion is non-nil, which treats a
false value as enabled; change the logic to check the pointer's boolean value
(e.g., require mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil AND
*mmconfig.DataRetentionSettings.EnableBoardsDeletion == true) so
enableBoardsDeletion is true only when the setting is explicitly true.

In `@server/boards/configuration.go`:
- Line 90: The code treats DataRetentionSettings.EnableBoardsDeletion as enabled
if the pointer is non-nil, causing false to be treated as true; change the logic
that sets enableBoardsDeletion (and any use assigning EnableDataRetention) to
check both that mmconfig.DataRetentionSettings.EnableBoardsDeletion is non-nil
and its dereferenced value is true (i.e., use a nil-safe boolean check like
mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil &&
*mmconfig.DataRetentionSettings.EnableBoardsDeletion). Update any related
assignments or flags that rely on enableBoardsDeletion to use this corrected
check.

In `@server/integrationtests/permissions_test.go`:
- Around line 283-300: The per-branch defers call response.Body.Close()
unguarded and can panic if the request failed and response is nil; update each
branch (where DoAPIGet/DoAPIPost/DoAPIPatch/DoAPIPut/DoAPIDelete are called) to
only defer response.Body.Close() after verifying response != nil (or after
checking err == nil), e.g. call the API, check err/response and then if response
!= nil defer response.Body.Close(), before asserting tc.expectedStatusCode using
response.StatusCode.

In
`@server/services/store/sqlstore/migrationstests/deleted_membership_boards_migration_test.go`:
- Around line 34-49: The test has a copy/paste bug: both DB queries for
boardDirectMessage use id = 'board-group-channel' so boardDirectMessage and
boardGroupChannel are identical; update the second th.f.DB().Get call that fills
boardDirectMessage (the one after th.f.RunInterceptor(18)) to query the correct
direct-message board id (e.g., "board-direct-message") so that
boardDirectMessage reflects the direct message board and the subsequent
assertions on boardDirectMessage.Created_By and Team_ID validate the intended
board type.

In `@server/services/telemetry/telemetry_test.go`:
- Line 55: The test sets only RUDDER_DATAPLANE_URL which leaves telemetry
missing RUDDER_KEY and can make checkMockRudderServer block; set a dummy
RUDDER_KEY environment variable in the test (alongside RUDDER_DATAPLANE_URL) so
telemetry is initialized and the checkMockRudderServer() path can proceed;
update telemetry_test.go to set os.Setenv("RUDDER_KEY", "<dummy-key>") before
invoking checkMockRudderServer (and optionally restore or unset the env var
after the test).

In `@server/services/webhook/webhook.go`:
- Around line 29-31: The http.Post call in the webhook sender ignores its
returned error and immediately dereferences resp.Body (the http.Post call and
the subsequent io.ReadAll/resp.Body.Close), which can nil-deref if the POST
fails; update the code around the http.Post call so you capture and handle the
returned error, check that resp != nil before reading/closing the body, log or
return/continue on error, and only call io.ReadAll and defer resp.Body.Close
after confirming resp is non-nil (place the defer immediately after the
nil-check). Ensure you update the symbols in this file: the http.Post call, the
resp variable, io.ReadAll usage, and resp.Body.Close handling inside the webhook
sender loop/function.

---

Outside diff comments:
In `@server/.golangci.yml`:
- Around line 1-47: The config uses deprecated v1 keys so linters settings and
exclusions are ignored; move the top-level "linters-settings:" block into
"linters.settings:" and consolidate the duplicate "govet" entries under that
nested "linters.settings.govet", and replace "issues.exclude-files" and
"issues.exclude-rules" with the v2 structure under "linters.exclusions" (use
"linters.exclusions.paths:" for the former and "linters.exclusions.rules:" for
the latter, preserving the same paths and rule entries and sources like QF1008);
run "golangci-lint migrate" or manually restructure the YAML accordingly and
remove trailing whitespace on the blank lines.

In `@server/services/store/generators/main.go`:
- Around line 126-134: The file descriptor opened with os.Open("store.go") is
closed only after ReadAll, so if io.ReadAll returns an error the descriptor
leaks; fix by calling defer file.Close() immediately after the successful
os.Open (i.e., right after the err check for os.Open) to ensure closure on all
return paths, and remove the later manual _ = file.Close() to avoid double-close
confusion.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 31-34: The workflow uses two different sources for the Go
version—webapp-test uses actions/setup-go with go-version-file:
focalboard/go.mod while dist relies on the workflow-level GO_VERSION env
(1.24.6); unify them by choosing one source of truth and update the other job:
either set both jobs' actions/setup-go to use go-version-file: focalboard/go.mod
(remove GO_VERSION env) or set both to use go-version: "${{ env.GO_VERSION }}"
(replace go-version-file in webapp-test), ensuring you update the
actions/setup-go invocations in the webapp-test and dist jobs and remove or
align the GO_VERSION variable accordingly.
- Around line 92-95: Replace the mutable tag reference in the workflow step that
currently reads "uses: chainguard-dev/setup-chainctl@v0.3.2" with the pinned
commit SHA and add the original tag as a trailing comment (e.g., use
"chainguard-dev/setup-chainctl@f4ed65b781b048c44d4f033ae854c025c5531c19 #
v0.3.2"); update the step named "ci/setup-chainctl" to use the SHA-pinned ref
while preserving the existing "with: identity: ${{ secrets.CHAINGUARD_IDENTITY
}}" input.

In @.github/workflows/lint-server.yml:
- Line 41: Replace the floating installer URL that pipes the install.sh from
master with a tag-pinned URL that matches the tool version (change the curl
target from raw.githubusercontent.com/.../master/install.sh to the tag-specific
path e.g. raw.githubusercontent.com/.../v2.1.6/install.sh) so the installer
script is pinned to v2.1.6; apply the same replacement to the identical curl
line in the other CI workflow where the installer is fetched.

In `@build/manifest/main.go`:
- Around line 217-228: In distManifest, replace the manual path
fmt.Sprintf("%s/%s/plugin.json", outputDir, manifest.Id) with
filepath.Join(outputDir, manifest.Id, "plugin.json") and ensure the directory
exists before writing by calling os.MkdirAll(filepath.Join(outputDir,
manifest.Id), 0755) (or similar) then call os.WriteFile; also add the
"path/filepath" import. This makes distManifest robust when the target directory
is missing and portable across platforms.

In `@Makefile`:
- Around line 259-265: The Makefile repeats the literal archive name
"$(PLUGIN_ID)-$(PLUGIN_VERSION)-fips.tar.gz" in the bundle-fips target; define a
single overridable variable (e.g., FIPS_BUNDLE_NAME ?=
$(PLUGIN_ID)-$(PLUGIN_VERSION)-fips.tar.gz) alongside BUNDLE_NAME near where
BUNDLE_NAME is declared and then replace the three occurrences in the
bundle-fips target (the tar and the echo) to use $(FIPS_BUNDLE_NAME) instead of
the repeated literal so callers can override it consistently.
- Around line 237-246: The Makefile currently copies plugin-linux-amd64-fips
explicitly and the for-loop over server/dist-fips/plugin-*-fips* will copy it
again, causing a redundant duplicate; fix by removing the explicit block that
handles plugin-linux-amd64-fips (the lines that cp
server/dist-fips/plugin-linux-amd64-fips ...) and rely on the for-loop that sets
target=$$(basename "$$file" | sed 's/-fips//g') to handle all plugin-*-fips*
files, or alternatively adjust the for-loop to skip plugin-linux-amd64-fips if
you prefer keeping the explicit copy (refer to the for file in the loop and the
target variable to locate the code).
- Around line 465-475: The deploy-to-mattermost-directory target unconditionally
copies public and assets; change it to mirror the bundler guards by wrapping the
public and ASSETS_DIR copies in the same conditional checks used by
copy_bundle_files/$(HAS_PUBLIC) (or use directory-existence checks) so cp -r
public $(BOARD_PLUGIN_PATH)/ and cp -r $(ASSETS_DIR) $(BOARD_PLUGIN_PATH)/ only
run when public and ASSETS_DIR are present/allowed; update the Makefile block
around the deploy-to-mattermost-directory target and reference
BOARD_PLUGIN_PATH, ASSETS_DIR, and $(HAS_PUBLIC)/copy_bundle_files so it behaves
the same as bundle.

In `@server/client/client.go`:
- Line 768: The multipart.Writer.Close() error is being discarded; capture its
returned error from writer.Close() in the relevant methods (those that return
*Response with an Error field) and propagate it into the Response.Error instead
of ignoring it. Locate the call to writer.Close() in server/client/client.go
(and the other occurrence) and if err := writer.Close(); err != nil { return
&Response{Error: err} } (or set resp.Error = err before returning) so callers
receive the actual close error for better diagnostics.

In `@server/integrationtests/board_test.go`:
- Around line 2064-2071: The code loops through userCategoryBoards and their
BoardMetadata to set duplicateBoardCategoryID when boardMetadata.BoardID ==
duplicateBoard.ID but continues scanning unnecessarily; modify the nested loops
(iterating userCategoryBoards and categoryBoard.BoardMetadata) to exit early
once a match is found by using a labeled break (or setting a flag and breaking
both loops) so that after assigning duplicateBoardCategoryID the outer loop
stops immediately, improving clarity and avoiding extra iterations.

In `@server/services/store/sqlstore/migrate.go`:
- Around line 146-151: The defer in Migrate() currently does "if err =
db.Close(); err != nil { ... }" which reassigns the outer variable err; change
it to capture the Close error in a new local variable (e.g. localErr or cerr)
using := so you don't overwrite the function-scoped err—update the defer block
around db.Close() to use a distinct local variable and log that local error via
s.logger.Error(...) without assigning to the outer err.

In `@server/services/store/sqlstore/migrationstests/migration_18_test.go`:
- Line 100: The current test silences JSON unmarshal errors by using `_ =
json.Unmarshal([]byte(view.Fields), &fields)` which hides parse failures; change
this to capture the error from json.Unmarshal on view.Fields into a variable
(err) and assert it is nil using the test helper (e.g., require.NoError(t, err)
or require.Nil(t, err)) before accessing fields["columnCalculations"] so any
malformed JSON fails with the actual unmarshal error.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c861d99f-f167-4ddb-9e70-e9b5d11814e2

📥 Commits

Reviewing files that changed from the base of the PR and between b87f32e and 457dfa5.

📒 Files selected for processing (45)
  • .github/workflows/ci.yml
  • .github/workflows/lint-server.yml
  • .gitignore
  • Makefile
  • build/manifest/main.go
  • server/.golangci.yml
  • server/api/api.go
  • server/api/api_test.go
  • server/api/archive.go
  • server/api/files.go
  • server/api/members.go
  • server/app/boards.go
  • server/app/category_boards.go
  • server/app/export.go
  • server/assets/build-template-archive/main.go
  • server/boards/boardsapp_test.go
  • server/boards/boardsapp_util.go
  • server/boards/configuration.go
  • server/boards/data_retention_test.go
  • server/client/client.go
  • server/integrationtests/board_test.go
  • server/integrationtests/clienttestlib.go
  • server/integrationtests/compliance_test.go
  • server/integrationtests/permissions_test.go
  • server/integrationtests/pluginteststore.go
  • server/model/properties_test.go
  • server/model/team.go
  • server/plugin.go
  • server/services/metrics/service.go
  • server/services/store/generators/main.go
  • server/services/store/sqlstore/data_migrations.go
  • server/services/store/sqlstore/data_migrations_test.go
  • server/services/store/sqlstore/helpers_test.go
  • server/services/store/sqlstore/migrate.go
  • server/services/store/sqlstore/migrationstests/boards_migrator_test.go
  • server/services/store/sqlstore/migrationstests/deleted_membership_boards_migration_test.go
  • server/services/store/sqlstore/migrationstests/migrate_34_test.go
  • server/services/store/sqlstore/migrationstests/migration36_test.go
  • server/services/store/sqlstore/migrationstests/migration_18_test.go
  • server/services/store/sqlstore/util.go
  • server/services/telemetry/telemetry_test.go
  • server/services/webhook/webhook.go
  • server/web/webserver.go
  • server/web/webserver_test.go
  • server/ws/server.go

Comment thread .github/workflows/ci.yml
Comment on lines +76 to +108
- name: Cache webapp node modules
id: cache-webapp-node-modules
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: focalboard/webapp/node_modules
key: ${{ runner.os }}-webapp-node-modules-${{ hashFiles('focalboard/webapp/package-lock.json') }}
restore-keys: ${{ runner.os }}-webapp-node-modules-

- name: Setup webapp npm deps
if: steps.cache-webapp-node-modules.outputs.cache-hit != 'true'
env:
NODE_ENV: development
run: |
cd focalboard/webapp
npm install --ignore-scripts --no-save

- name: ci/setup-chainctl
uses: chainguard-dev/setup-chainctl@v0.3.2
with:
identity: ${{ secrets.CHAINGUARD_IDENTITY }}

- name: ci/setup-build-tools
run: |
echo "Setting up build tools..."
cd focalboard
mkdir -p build/bin
cd build/manifest && go build -o ../bin/manifest
cd ../pluginctl && go build -o ../bin/pluginctl

- name: Build both distributions
env:
GO_VERSION: ${{ env.GO_VERSION }}
run: cd focalboard; make dist-all

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Webapp cache + install steps are nullified by make dist-all’s clean target.

dist-all declares clean as a prerequisite (see Makefile Line 285), and clean does rm -fr webapp/node_modules (Makefile Line 434). So the Cache webapp node modules and Setup webapp npm deps steps here (Lines 76–90) are always thrown away before the actual build — distwebappwebapp/node_modules triggers a fresh npm install anyway.

Either drop these two steps, or remove clean from dist-all’s prerequisites (preferable — see my comment on the Makefile) so the cache actually buys you something.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 76 - 108, The CI currently caches and
installs focalboard/webapp node_modules in the steps named "Cache webapp node
modules" and "Setup webapp npm deps", but those are always deleted because the
Makefile target dist-all declares clean as a prerequisite (target dist-all ->
clean) and clean removes webapp/node_modules; fix by removing clean from the
dist-all prerequisites in the Makefile (edit the dist-all target to no longer
depend on clean) so the cached node_modules survive the make dist-all run, or
alternatively remove the two CI steps ("Cache webapp node modules" and "Setup
webapp npm deps") from .github/workflows/ci.yml if you prefer to keep clean in
dist-all.

Comment thread Makefile
Comment on lines +132 to +172
## Builds the server with FIPS compliance using Docker (requires Docker)
.PHONY: server-fips
server-fips: templates-archive
ifneq ($(HAS_SERVER),)
@echo Building FIPS-compliant plugin server binaries
mkdir -p server/dist-fips
@echo "Setting up FIPS build environment..."

# Docker authentication is handled by CI (setup-chainctl)
@if ! docker manifest inspect $(FIPS_IMAGE) >/dev/null 2>&1; then \
echo "Docker authentication failed. Ensure setup-chainctl configured Docker authentication."; \
echo "Trying fallback authentication if credentials are available..."; \
if [ -n "$(CHAINGUARD_DEV_USERNAME)" ] && [ -n "$(CHAINGUARD_DEV_TOKEN)" ]; then \
echo "Using username/token authentication..."; \
echo "$(CHAINGUARD_DEV_TOKEN)" | docker login cgr.dev --username "$(CHAINGUARD_DEV_USERNAME)" --password-stdin; \
else \
echo "Warning: No authentication available. FIPS build may fail."; \
fi; \
else \
echo "✅ Docker authentication is working"; \
fi

# Create local cache directory for CI/ACT compatibility
mkdir -p $(PWD)/.build-cache

# Try FIPS build with error handling
@if docker run --rm \
--entrypoint="" \
-v $(PWD):/plugin \
-v $(PWD)/.build-cache:/root/.cache \
-w /plugin/server \
$(FIPS_IMAGE) \
sh -c "CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags fips -ldflags '$(LDFLAGS)' -trimpath -buildvcs=false -o dist-fips/plugin-linux-amd64-fips"; then \
echo "FIPS plugin server build completed: server/dist-fips/plugin-linux-amd64-fips"; \
else \
echo "FIPS build failed - likely authentication issue with $(FIPS_IMAGE)"; \
echo "Creating placeholder to indicate FIPS build was attempted but failed"; \
echo "FIPS_BUILD_FAILED" > server/dist-fips/FIPS_BUILD_FAILED.txt; \
exit 1; \
fi
endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does cgr.dev/mattermost.com/go-msft-fips:1.24 support cross-compilation to linux/arm64 with CGO enabled, or is it amd64-only?

💡 Result:

No, cgr.dev/mattermost.com/go-msft-fips:1.24 does not support cross-compilation to linux/arm64 with CGO enabled. It is amd64-only. The image is used as a builder in Mattermost FIPS Dockerfiles with CGO_ENABLED=1 explicitly set for FIPS compliance via Microsoft's Go build with systemcrypto and OpenSSL CGO bindings. Mattermost's FIPS builds explicitly limit to linux/amd64 ("only build-linux-amd64 for fips", "skip arm for FIPS"), skipping arm64. Chainguard FIPS images like mattermost-fips list sizes only for x86_64 (amd64), with no arm64 variants. FIPS Go requires CGO for OpenSSL, and cross-compilation with CGO to arm64 needs cross-compiled OpenSSL libraries, which this specialized Chainguard image likely lacks—consistent with amd64-only builds in practice.

Citations:


FIPS build scope is intentionally limited to linux-amd64 by the FIPS tooling; document this constraint and improve error messaging.

The server-fips target only produces plugin-linux-amd64-fips because the underlying FIPS Docker image (cgr.dev/mattermost.com/go-msft-fips:1.24) is amd64-only. Cross-compilation to arm64 is not supported by this image—FIPS Go requires CGO bindings for OpenSSL/systemcrypto, and the image lacks cross-compiled libraries for non-x86_64 platforms. This is a known architectural decision in Mattermost FIPS builds.

To prevent silent failures when admins install the FIPS bundle on non-amd64 hosts:

  1. Document the amd64-only scope in a comment above this target or in the PR description.
  2. Ensure the server surfaces a clear error if the host architecture doesn't match the available FIPS binary.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 132 - 172, The server-fips target produces only an
amd64 binary (server/dist-fips/plugin-linux-amd64-fips) because the FIPS Docker
image (FIPS_IMAGE) is amd64-only; add a short comment above the server-fips
target documenting this amd64-only constraint and why cross-compilation isn't
supported. Also add an explicit host-architecture check in the server-fips
recipe (use uname -m or similar) and abort early with a clear error message if
the host is not x86_64, writing the FIPS_BUILD_FAILED.txt placeholder and
exiting non-zero so admins installing the bundle see a deterministic failure;
reference the server-fips target, FIPS_IMAGE, output filename, and
FIPS_BUILD_FAILED.txt when making the changes.

Comment thread Makefile
Comment on lines +140 to +152
# Docker authentication is handled by CI (setup-chainctl)
@if ! docker manifest inspect $(FIPS_IMAGE) >/dev/null 2>&1; then \
echo "Docker authentication failed. Ensure setup-chainctl configured Docker authentication."; \
echo "Trying fallback authentication if credentials are available..."; \
if [ -n "$(CHAINGUARD_DEV_USERNAME)" ] && [ -n "$(CHAINGUARD_DEV_TOKEN)" ]; then \
echo "Using username/token authentication..."; \
echo "$(CHAINGUARD_DEV_TOKEN)" | docker login cgr.dev --username "$(CHAINGUARD_DEV_USERNAME)" --password-stdin; \
else \
echo "Warning: No authentication available. FIPS build may fail."; \
fi; \
else \
echo "✅ Docker authentication is working"; \
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.

⚠️ Potential issue | 🟡 Minor

Fallback Docker login expands as Make variables, not shell env.

$(CHAINGUARD_DEV_USERNAME) and $(CHAINGUARD_DEV_TOKEN) are Make variables and will be expanded at parse time — if they aren't passed on the make command line they evaluate to empty strings, so both [ -n "" ] tests always fail silently and the "Warning: No authentication available" branch is taken regardless of what's in the CI environment. To read the CI-provided env vars, escape with $$:

🛡️ Suggested fix
-		if [ -n "$(CHAINGUARD_DEV_USERNAME)" ] && [ -n "$(CHAINGUARD_DEV_TOKEN)" ]; then \
+		if [ -n "$$CHAINGUARD_DEV_USERNAME" ] && [ -n "$$CHAINGUARD_DEV_TOKEN" ]; then \
 			echo "Using username/token authentication..."; \
-			echo "$(CHAINGUARD_DEV_TOKEN)" | docker login cgr.dev --username "$(CHAINGUARD_DEV_USERNAME)" --password-stdin; \
+			echo "$$CHAINGUARD_DEV_TOKEN" | docker login cgr.dev --username "$$CHAINGUARD_DEV_USERNAME" --password-stdin; \
 		else \

Since CI is expected to use setup-chainctl for auth, this is low severity — but as-written the fallback is dead code.

📝 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
# Docker authentication is handled by CI (setup-chainctl)
@if ! docker manifest inspect $(FIPS_IMAGE) >/dev/null 2>&1; then \
echo "Docker authentication failed. Ensure setup-chainctl configured Docker authentication."; \
echo "Trying fallback authentication if credentials are available..."; \
if [ -n "$(CHAINGUARD_DEV_USERNAME)" ] && [ -n "$(CHAINGUARD_DEV_TOKEN)" ]; then \
echo "Using username/token authentication..."; \
echo "$(CHAINGUARD_DEV_TOKEN)" | docker login cgr.dev --username "$(CHAINGUARD_DEV_USERNAME)" --password-stdin; \
else \
echo "Warning: No authentication available. FIPS build may fail."; \
fi; \
else \
echo "✅ Docker authentication is working"; \
fi
# Docker authentication is handled by CI (setup-chainctl)
`@if` ! docker manifest inspect $(FIPS_IMAGE) >/dev/null 2>&1; then \
echo "Docker authentication failed. Ensure setup-chainctl configured Docker authentication."; \
echo "Trying fallback authentication if credentials are available..."; \
if [ -n "$$CHAINGUARD_DEV_USERNAME" ] && [ -n "$$CHAINGUARD_DEV_TOKEN" ]; then \
echo "Using username/token authentication..."; \
echo "$$CHAINGUARD_DEV_TOKEN" | docker login cgr.dev --username "$$CHAINGUARD_DEV_USERNAME" --password-stdin; \
else \
echo "Warning: No authentication available. FIPS build may fail."; \
fi; \
else \
echo "✅ Docker authentication is working"; \
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 140 - 152, The fallback Docker-login branch in the
Makefile is using Makefile expansion
($(CHAINGUARD_DEV_USERNAME)/$(CHAINGUARD_DEV_TOKEN)) instead of reading the CI
shell environment, so the tests [ -n "$(CHAINGUARD_DEV_USERNAME)" ] always fail;
change those Make-variable references to shell env references by escaping the
dollar sign (use $$CHAINGUARD_DEV_USERNAME and $$CHAINGUARD_DEV_TOKEN wherever
they appear in the conditional and the docker login command) so the shell
invoked by the recipe reads the CI-provided environment variables at runtime.

Comment thread Makefile
Comment on lines +283 to +296
## Builds both normal and FIPS distributions.
.PHONY: dist-all
dist-all: clean
@echo "==> Building both normal and FIPS distributions in parallel..."
$(MAKE) dist
@if $(MAKE) dist-fips; then \
echo "==> Both distributions built successfully:"; \
echo " Normal: dist/$$(./build/bin/manifest id)-$$(./build/bin/manifest version).tar.gz"; \
echo " FIPS: dist-fips/$$(./build/bin/manifest id)-$$(./build/bin/manifest version)-fips.tar.gz"; \
else \
echo "==> FIPS build failed, continuing with normal distribution only:"; \
echo " Normal: dist/$$(./build/bin/manifest id)-$$(./build/bin/manifest version).tar.gz"; \
echo " FIPS: Build failed - check Docker/credentials"; \
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.

⚠️ Potential issue | 🟡 Minor

dist-all builds the webapp twice and mislabels work as parallel.

Two issues in this recipe:

  1. Sequential, not parallel. The comment says "Building both normal and FIPS distributions in parallel…" but $(MAKE) dist and $(MAKE) dist-fips are invoked back-to-back. Update the message, or run them with -j if parallel is really desired (harder — both targets mutate webapp/pack and server/dist).
  2. Duplicate webapp build. dist depends on webapp, and dist-fips also depends on webapp (Line 281). The webapp target has no staleness check, so npm run build + npm run pack run twice on every dist-all. Since dist-all: clean has already wiped webapp/node_modules, you also pay one npm install cost. On CI this roughly doubles the webapp portion of the build.

Recommended: drop webapp from dist-fips's prerequisites and rely on dist-all ordering, or refactor the webapp target to be idempotent. And update the echo:

♻️ Suggested
-## Builds and bundles the FIPS plugin.
-.PHONY: dist-fips
-dist-fips: apply server-fips webapp bundle-fips
+## Builds and bundles the FIPS plugin. Assumes `webapp` has already been built
+## (e.g. when invoked via `dist-all`). Run `make dist` first if invoking standalone.
+.PHONY: dist-fips
+dist-fips: apply server-fips bundle-fips
-	`@echo` "==> Building both normal and FIPS distributions in parallel..."
+	`@echo` "==> Building normal and FIPS distributions sequentially..."
 	$(MAKE) dist
 	`@if` $(MAKE) dist-fips; then \

Also note: because dist-all: clean removes webapp/node_modules, the Cache webapp node modules / Setup webapp npm deps steps in ci.yml are effectively no-ops — see my comment on ci.yml Lines 76–108.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 283 - 296, The dist-all target currently claims a
parallel build but invokes $(MAKE) dist and $(MAKE) dist-fips sequentially and
causes the webapp to be built twice because both dist and dist-fips depend on
the webapp target; fix by either (A) adjusting the echo to remove the "parallel"
claim and keep sequential invocation, or (B) avoid duplicate webapp builds by
removing webapp from the prerequisites of the dist-fips target (so dist-all runs
dist then dist-fips reusing the already-built webapp) or by making the webapp
target idempotent; update the dist-all message string accordingly and ensure
references to webapp/pack and server/dist remain correct when you remove the
duplicate prerequisite.

Comment thread server/api/api.go
setResponseHeader(w, "Content-Type", "application/json")
w.WriteHeader(code)
fmt.Fprint(w, message)
_, _ = fmt.Fprint(w, message)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Indentation is off on the changed line.

Line 214 uses extra leading tabs compared to surrounding lines in the same function, breaking formatting. Since gofmt/goimports were removed from server/.golangci.yml in this PR, this is no longer caught by CI — please restore the normal one-tab indent.

🔧 Proposed fix
-			_, _ = fmt.Fprint(w, message)
+	_, _ = fmt.Fprint(w, message)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/api.go` at line 214, The line containing "fmt.Fprint(w, message)"
has extra leading tabs and should be indented to match the surrounding function
block (single tab per indent level); locate the statement (fmt.Fprint(w,
message)) in server/api/api.go and replace the extra tabs so it uses the same
one-tab indentation as the adjacent lines, then run gofmt/goimports (or ensure
formatting matches gofmt style) so the file's indentation is consistent.

if mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil {
enableBoardsDeletion = true
}
enableBoardsDeletion := mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Line 90: Data retention is enabled by pointer presence, not configured value (risk of unintended deletions).

EnableBoardsDeletion != nil becomes true even when the setting is explicitly false. This can incorrectly set EnableDataRetention to true.

Suggested fix
-	enableBoardsDeletion := mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil
+	enableBoardsDeletion := mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil &&
+		*mmconfig.DataRetentionSettings.EnableBoardsDeletion
📝 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
enableBoardsDeletion := mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil
enableBoardsDeletion := mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil &&
*mmconfig.DataRetentionSettings.EnableBoardsDeletion
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/boards/configuration.go` at line 90, The code treats
DataRetentionSettings.EnableBoardsDeletion as enabled if the pointer is non-nil,
causing false to be treated as true; change the logic that sets
enableBoardsDeletion (and any use assigning EnableDataRetention) to check both
that mmconfig.DataRetentionSettings.EnableBoardsDeletion is non-nil and its
dereferenced value is true (i.e., use a nil-safe boolean check like
mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil &&
*mmconfig.DataRetentionSettings.EnableBoardsDeletion). Update any related
assignments or flags that rely on enableBoardsDeletion to use this corrected
check.

Comment on lines 283 to 300
case methodGet:
response, err = reqClient.DoAPIGet(url, "")
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()
case methodPost:
response, err = reqClient.DoAPIPost(url, tc.body)
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()
case methodPatch:
response, err = reqClient.DoAPIPatch(url, tc.body)
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()
case methodPut:
response, err = reqClient.DoAPIPut(url, tc.body)
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()
case methodDelete:
response, err = reqClient.DoAPIDelete(url, tc.body)
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()
}

require.Equal(t, tc.expectedStatusCode, response.StatusCode, tc.identifier())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard response before deferring body close to avoid nil-pointer panics.

If a request fails with response == nil, each branch-level defer can panic and hide the real failure.

💡 Proposed fix
 			switch tc.method {
 			case methodGet:
 				response, err = reqClient.DoAPIGet(url, "")
-				defer func() { _ = response.Body.Close() }()
 			case methodPost:
 				response, err = reqClient.DoAPIPost(url, tc.body)
-				defer func() { _ = response.Body.Close() }()
 			case methodPatch:
 				response, err = reqClient.DoAPIPatch(url, tc.body)
-				defer func() { _ = response.Body.Close() }()
 			case methodPut:
 				response, err = reqClient.DoAPIPut(url, tc.body)
-				defer func() { _ = response.Body.Close() }()
 			case methodDelete:
 				response, err = reqClient.DoAPIDelete(url, tc.body)
-				defer func() { _ = response.Body.Close() }()
 			}
+			require.NotNil(t, response, tc.identifier())
+			defer func() { _ = response.Body.Close() }()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/integrationtests/permissions_test.go` around lines 283 - 300, The
per-branch defers call response.Body.Close() unguarded and can panic if the
request failed and response is nil; update each branch (where
DoAPIGet/DoAPIPost/DoAPIPatch/DoAPIPut/DoAPIDelete are called) to only defer
response.Body.Close() after verifying response != nil (or after checking err ==
nil), e.g. call the API, check err/response and then if response != nil defer
response.Body.Close(), before asserting tc.expectedStatusCode using
response.StatusCode.

Comment on lines +34 to 49
err = th.f.DB().Get(&boardDirectMessage, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
require.NoError(t, err)
require.Equal(t, "user-one", boardDirectMessage.Created_By)
require.Equal(t, "team-one", boardDirectMessage.Team_ID)

th.f.RunInterceptor(18)

th.f.DB().Get(&boardGroupChannel, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
err = th.f.DB().Get(&boardGroupChannel, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
require.NoError(t, err)
require.Equal(t, "user-one", boardGroupChannel.Created_By)
require.Equal(t, "team-three", boardGroupChannel.Team_ID)

th.f.DB().Get(&boardDirectMessage, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
err = th.f.DB().Get(&boardDirectMessage, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
require.NoError(t, err)
require.Equal(t, "user-one", boardDirectMessage.Created_By)
require.Equal(t, "team-three", boardDirectMessage.Team_ID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP "board-direct-message|board-group-channel" server/services/store/sqlstore/migrationstests/

Repository: mattermost/mattermost-plugin-boards

Length of output: 1361


Fix copy/paste bug: boardDirectMessage should query a different board.

Lines 34 and 46 both query WHERE id = 'board-group-channel', so boardDirectMessage holds the same data as boardGroupChannel. The variable name suggests this should test a distinct direct message board, but currently the second set of assertions (lines 46–49) provide no meaningful coverage. Update the second query to use the correct board ID (likely 'board-direct-message' or similar) to properly exercise both board types.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/services/store/sqlstore/migrationstests/deleted_membership_boards_migration_test.go`
around lines 34 - 49, The test has a copy/paste bug: both DB queries for
boardDirectMessage use id = 'board-group-channel' so boardDirectMessage and
boardGroupChannel are identical; update the second th.f.DB().Get call that fills
boardDirectMessage (the one after th.f.RunInterceptor(18)) to query the correct
direct-message board id (e.g., "board-direct-message") so that
boardDirectMessage reflects the direct message board and the subsequent
assertions on boardDirectMessage.Created_By and Team_ID validate the intended
board type.


os.Setenv("RUDDER_KEY", "mock-test-rudder-key")
os.Setenv("RUDDER_DATAPLANE_URL", server.URL)
_ = os.Setenv("RUDDER_DATAPLANE_URL", server.URL)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing RUDDER_KEY can prevent telemetry from sending and hang this test.

Line 55 sets only RUDDER_DATAPLANE_URL, but telemetry config requires both values; otherwise checkMockRudderServer can block waiting for an event.

💡 Proposed fix
 func TestTelemetry(t *testing.T) {
 	receiveChan, server := mockServer()
+	defer server.Close()
 
-	_ = os.Setenv("RUDDER_DATAPLANE_URL", server.URL)
+	t.Setenv("RUDDER_KEY", "test-rudder-key")
+	t.Setenv("RUDDER_DATAPLANE_URL", server.URL)
📝 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
_ = os.Setenv("RUDDER_DATAPLANE_URL", server.URL)
func TestTelemetry(t *testing.T) {
receiveChan, server := mockServer()
defer server.Close()
t.Setenv("RUDDER_KEY", "test-rudder-key")
t.Setenv("RUDDER_DATAPLANE_URL", server.URL)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/telemetry/telemetry_test.go` at line 55, The test sets only
RUDDER_DATAPLANE_URL which leaves telemetry missing RUDDER_KEY and can make
checkMockRudderServer block; set a dummy RUDDER_KEY environment variable in the
test (alongside RUDDER_DATAPLANE_URL) so telemetry is initialized and the
checkMockRudderServer() path can proceed; update telemetry_test.go to set
os.Setenv("RUDDER_KEY", "<dummy-key>") before invoking checkMockRudderServer
(and optionally restore or unset the env var after the test).

Comment on lines 29 to +31
resp, _ := http.Post(url, "application/json", bytes.NewBuffer(json)) //nolint:gosec
_, _ = io.ReadAll(resp.Body)
resp.Body.Close()
_ = resp.Body.Close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pre-existing nil-deref risk around the touched line.

http.Post ignores the returned error on line 29 and then unconditionally dereferences resp.Body on lines 30–31. If the POST fails (DNS, connection refused, timeout, malformed URL), resp will be nil and this will panic inside the webhook loop, killing the caller's goroutine.

Since this close is being modified anyway, consider handling the error and nil-checking resp while you're here.

🛡️ Suggested fix
 	for _, url := range wh.config.WebhookUpdate {
-		resp, _ := http.Post(url, "application/json", bytes.NewBuffer(json)) //nolint:gosec
-		_, _ = io.ReadAll(resp.Body)
-		_ = resp.Body.Close()
-
-		wh.logger.Debug("webhook.NotifyUpdate", mlog.String("url", url))
+		resp, err := http.Post(url, "application/json", bytes.NewBuffer(json)) //nolint:gosec
+		if err != nil {
+			wh.logger.Error("webhook.NotifyUpdate: POST failed", mlog.String("url", url), mlog.Err(err))
+			continue
+		}
+		_, _ = io.ReadAll(resp.Body)
+		_ = resp.Body.Close()
+
+		wh.logger.Debug("webhook.NotifyUpdate", mlog.String("url", url))
 	}
📝 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
resp, _ := http.Post(url, "application/json", bytes.NewBuffer(json)) //nolint:gosec
_, _ = io.ReadAll(resp.Body)
resp.Body.Close()
_ = resp.Body.Close()
resp, err := http.Post(url, "application/json", bytes.NewBuffer(json)) //nolint:gosec
if err != nil {
wh.logger.Error("webhook.NotifyUpdate: POST failed", mlog.String("url", url), mlog.Err(err))
continue
}
_, _ = io.ReadAll(resp.Body)
_ = resp.Body.Close()
wh.logger.Debug("webhook.NotifyUpdate", mlog.String("url", url))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/webhook/webhook.go` around lines 29 - 31, The http.Post call
in the webhook sender ignores its returned error and immediately dereferences
resp.Body (the http.Post call and the subsequent io.ReadAll/resp.Body.Close),
which can nil-deref if the POST fails; update the code around the http.Post call
so you capture and handle the returned error, check that resp != nil before
reading/closing the body, log or return/continue on error, and only call
io.ReadAll and defer resp.Body.Close after confirming resp is non-nil (place the
defer immediately after the nil-check). Ensure you update the symbols in this
file: the http.Post call, the resp variable, io.ReadAll usage, and
resp.Body.Close handling inside the webhook sender loop/function.

@agarciamontoro

Copy link
Copy Markdown
Member

@CodeRabbit ignore

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
✅ Actions performed

Reviews paused.

@agarciamontoro

Copy link
Copy Markdown
Member

@coderabbitai ignore

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
✅ Actions performed

Reviews paused.

agarciamontoro and others added 2 commits April 17, 2026 16:50
Bumping mattermost-plugin-boards to Patch version 9.2.5

# Conflicts:
#	.github/workflows/ci.yml
#	server/app/export.go
#	server/integrationtests/clienttestlib.go
#	server/integrationtests/permissions_test.go
#	server/model/team.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants