Skip to content

fix: data races in tests#440

Open
alnr wants to merge 1 commit into
masterfrom
alnr/fix-race
Open

fix: data races in tests#440
alnr wants to merge 1 commit into
masterfrom
alnr/fix-race

Conversation

@alnr
Copy link
Copy Markdown
Collaborator

@alnr alnr commented Jun 3, 2026

Calling cmdA.AddCommand(cmdB) modifies both cmdA and cmdB and hence is not goroutine safe.

Since we extensively test most commands and subcommands, we cannot have global var ... = &cobra.Command{...} declarations.

This diff is a lot more palatable when ignoring whitespace.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Docker build target to compile container images.
  • Tests

    • Enhanced test suite with race-condition detection to improve code quality and reliability.
    • Added concurrent command execution tests to validate stability under parallel operations.
  • Chores

    • Internal refactoring of command initialization patterns for improved code maintainability.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors all Cobra command definitions from package-level variables with init()-based registration to factory functions, eliminating global state races during concurrent command instantiation. Monorepo subcommands use scratch variables populated per-invocation to safely bind flags. Testing and CI now include race-condition detection.

Changes

Cobra command factory refactoring for concurrent safety

Layer / File(s) Summary
Root command factory foundation
cmd/version.go, cmd/dev.go, cmd/nodev.go, cmd/root.go, cmd/root_race_test.go
NewVersionCmd() and newDevCommands() factories replace package variables. Root command wires subcommands via factory calls. New concurrent test spawns 16 goroutines creating/executing fresh root commands to detect races.
Monorepo subcommand with concurrency safeguards
cmd/dev/ci/monorepo/main.go, run.go, changes.go, components.go
Package-level scratch variables (rootDirectory, verbose, debug, pr, revisionRange, branch, changesMode, gitOpts) are populated in PersistentPreRun from local flag variables. NewCommand() builds fresh command instances with local flags bound to scratch variables at invocation time, preventing concurrent writes during construction.
Dev command hierarchy
cmd/dev/main.go, cmd/dev/headers/main.go, cmd/dev/headers/copyright.go, cmd/dev/headers/cp.go, cmd/dev/markdown/main.go, cmd/dev/markdown/render.go, cmd/dev/schema/main.go, cmd/dev/schema/render_version.go, cmd/dev/swagger/main.go, cmd/dev/swagger/sanitize.go
Each module defines NewCommand() to construct its root command and register subcommands via factories. Headers (copyright, cp), markdown (render), schema (render-version), and swagger (sanitize) subcommands are now built by factory functions instead of package variables.
CI utility commands
cmd/dev/ci/main.go, cmd/dev/ci/deps/main.go, cmd/dev/ci/deps/url.go, cmd/dev/ci/github/main.go, cmd/dev/ci/github/env.go, cmd/dev/ci/orbs/main.go, cmd/dev/ci/orbs/bump.go
CI root NewCommand() registers deps, github, monorepo, orbs subcommands. Deps, github, and orbs modules define NewCommand() or newXxxCmd() factories for url, env, and bump subcommands respectively.
Release and newsletter chains
cmd/dev/openapi/main.go, cmd/dev/openapi/migrate.go, cmd/dev/release/main.go, cmd/dev/release/compile.go, cmd/dev/release/publish.go, cmd/dev/release/notify/main.go, cmd/dev/release/notify/draft.go, cmd/dev/release/notify/send.go, cmd/dev/newsletter/main.go, cmd/dev/newsletter/draft.go, cmd/dev/newsletter/send.go
OpenAPI NewCommand() registers migrate subcommand with JSON patch rendering/application logic moved into the factory. Release root registers notify, compile, publish subcommands. Notify and newsletter roots each register send and draft subcommands via factories. All command metadata, flag binding, and execution logic now encapsulated in factory functions.
CI and build infrastructure
.github/workflows/ci.yaml, Makefile
CI test job now runs go test -race ./... to detect concurrent races. Makefile replaces unused post-release target with docker target that builds oryd/ory:latest-sqlite image from .docker/Dockerfile-build.

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description clearly explains the problem (AddCommand modifies both commands, causing races), the solution (replace global variables with factory functions), and includes helpful context about reviewing with whitespace ignored. However, it is missing a Related Issue reference and the required checklist items. Add a Related Issue reference (e.g., issue or design document link) and complete the required checklist items (CLA signature, security confirmation, tests added, etc.).
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: data races in tests' directly matches the core objective: eliminating data races that occur when global Cobra command variables are modified by AddCommand() during concurrent test execution.
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.

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

✨ 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 alnr/fix-race

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
cmd/dev/ci/monorepo/run.go (1)

94-96: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use mode parameter instead of package-level runMode in error message.

The error message references the package-level scratch variable runMode instead of the function parameter mode. This is inconsistent with the rest of the function and could produce misleading error messages in tests that call runWrapper directly.

Proposed fix
 	default:
-		return fmt.Errorf("unknown runMode: %s", runMode)
+		return fmt.Errorf("unknown runMode: %s", mode)
 	}
🤖 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 `@cmd/dev/ci/monorepo/run.go` around lines 94 - 96, The default case in
runWrapper currently uses the package-level variable runMode in the error
string; update the error to reference the function parameter mode instead.
Locate the switch/default inside runWrapper and change the fmt.Errorf("unknown
runMode: %s", runMode) call to fmt.Errorf("unknown runMode: %s", mode) so the
error message reflects the actual argument passed to runWrapper rather than the
package-level scratch variable.
🧹 Nitpick comments (1)
cmd/dev/ci/monorepo/components.go (1)

17-19: ⚡ Quick win

Remove the componentMode package-level scratch variable and switch on localComponentMode directly.

componentMode is only used inside newComponentsCmd()’s Run closure in cmd/dev/ci/monorepo/components.go, so the package-level mutable state is unnecessary. Eliminating it avoids any residual shared-state risk while keeping the behavior the same.

♻️ Proposed change
-// Package-level scratch variable for the `components` subcommand, populated
-// from a per-command local in Run so concurrent construction does not race.
-var componentMode string
-
 func newComponentsCmd() *cobra.Command {
 	var localComponentMode string
 	c := &cobra.Command{
 		Use:   "components",
 		Short: "List components based on mode.",
 		Long:  `List components based on mode by reading dependency configs and displaying the dependency graph.`,
 		Run: func(cmd *cobra.Command, args []string) {
-			componentMode = localComponentMode
-
 			var graph ComponentGraph
 			_, _ = graph.getComponentGraph(rootDirectory)
 
-			switch componentMode {
+			switch localComponentMode {
 			case "affected":
 				affectedComponents := getAffectedComponents(&graph)
 				displayComponents(affectedComponents)
 			case "all":
 				allComponents := graph.components
 				displayComponents(allComponents)
 			case "changed":
 				changedComponents := getChangedComponents(&graph)
 				displayComponents(changedComponents)
 			case "involved":
 				involvedComponents := getInvolvedComponents(&graph)
 				displayComponents(involvedComponents)
 			default:
-				log.Fatalf("Unknown ListMode '%s'", componentMode)
+				log.Fatalf("Unknown ListMode '%s'", localComponentMode)
 			}
 		},
 	}
🤖 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 `@cmd/dev/ci/monorepo/components.go` around lines 17 - 19, Remove the
package-level variable componentMode and use the local variable
localComponentMode directly inside the Run closure of newComponentsCmd; update
any switch/case or conditional that references componentMode to instead
reference localComponentMode (inside newComponentsCmd and its Run), and delete
the now-unused componentMode declaration to eliminate unnecessary package-level
mutable state.
🤖 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 `@cmd/dev/ci/github/env.go`:
- Around line 54-59: The guard incorrectly treats an unset/empty
SWAGGER_SPEC_IGNORE_PKGS as non-empty because strings.Split("", ",") yields
[""], causing export of "-x " entries; update the logic around reading
SWAGGER_SPEC_IGNORE_PKGS (the os.Getenv call and ignorePkgs handling) to first
check if the env var is non-empty (or filter out empty strings after
strings.Split) before building the "-x " list and calling fmt.Printf, ensuring
ignorePkgs contains only non-empty package names before joining and printing.

In `@cmd/dev/ci/monorepo/main.go`:
- Line 18: Remove the unused scratch variable "branch" and its flag binding from
the monorepo command or wire it into downstream logic; specifically, delete the
"branch string" declaration and the corresponding "--branch" PersistentFlag
setup in main.go (the PersistentPreRun populates it) OR propagate the variable
into the existing flow (e.g., pass branch into the functions used by
run.go/changes.go or use it inside isPR()/change-detection logic) so the value
is consumed rather than unused.

---

Outside diff comments:
In `@cmd/dev/ci/monorepo/run.go`:
- Around line 94-96: The default case in runWrapper currently uses the
package-level variable runMode in the error string; update the error to
reference the function parameter mode instead. Locate the switch/default inside
runWrapper and change the fmt.Errorf("unknown runMode: %s", runMode) call to
fmt.Errorf("unknown runMode: %s", mode) so the error message reflects the actual
argument passed to runWrapper rather than the package-level scratch variable.

---

Nitpick comments:
In `@cmd/dev/ci/monorepo/components.go`:
- Around line 17-19: Remove the package-level variable componentMode and use the
local variable localComponentMode directly inside the Run closure of
newComponentsCmd; update any switch/case or conditional that references
componentMode to instead reference localComponentMode (inside newComponentsCmd
and its Run), and delete the now-unused componentMode declaration to eliminate
unnecessary package-level mutable state.
🪄 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 Plus

Run ID: a9f04a2a-9a62-468a-86d5-2ec0fa88be67

📥 Commits

Reviewing files that changed from the base of the PR and between 3ef403b and 88bcaba.

📒 Files selected for processing (39)
  • .github/workflows/ci.yaml
  • Makefile
  • cmd/dev.go
  • cmd/dev/ci/deps/main.go
  • cmd/dev/ci/deps/url.go
  • cmd/dev/ci/github/env.go
  • cmd/dev/ci/github/main.go
  • cmd/dev/ci/main.go
  • cmd/dev/ci/monorepo/changes.go
  • cmd/dev/ci/monorepo/components.go
  • cmd/dev/ci/monorepo/main.go
  • cmd/dev/ci/monorepo/run.go
  • cmd/dev/ci/orbs/bump.go
  • cmd/dev/ci/orbs/main.go
  • cmd/dev/headers/copyright.go
  • cmd/dev/headers/cp.go
  • cmd/dev/headers/main.go
  • cmd/dev/main.go
  • cmd/dev/markdown/main.go
  • cmd/dev/markdown/render.go
  • cmd/dev/newsletter/draft.go
  • cmd/dev/newsletter/main.go
  • cmd/dev/newsletter/send.go
  • cmd/dev/openapi/main.go
  • cmd/dev/openapi/migrate.go
  • cmd/dev/release/compile.go
  • cmd/dev/release/main.go
  • cmd/dev/release/notify/draft.go
  • cmd/dev/release/notify/main.go
  • cmd/dev/release/notify/send.go
  • cmd/dev/release/publish.go
  • cmd/dev/schema/main.go
  • cmd/dev/schema/render_version.go
  • cmd/dev/swagger/main.go
  • cmd/dev/swagger/sanitize.go
  • cmd/nodev.go
  • cmd/root.go
  • cmd/root_race_test.go
  • cmd/version.go
💤 Files with no reviewable changes (1)
  • Makefile

Comment thread cmd/dev/ci/github/env.go
Comment on lines +54 to +59
if ignorePkgs := strings.Split(os.Getenv("SWAGGER_SPEC_IGNORE_PKGS"), ","); len(ignorePkgs) > 0 {
for k, p := range ignorePkgs {
ignorePkgs[k] = "-x " + p
}
fmt.Printf(`export SWAGGER_SPEC_IGNORE_PKGS='%s'`, strings.Join(ignorePkgs, " "))
}
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 | ⚡ Quick win

Guard is always true when SWAGGER_SPEC_IGNORE_PKGS is unset.

strings.Split("", ",") returns [""] (length 1), so len(ignorePkgs) > 0 never short-circuits. When the env var is empty/unset, this emits export SWAGGER_SPEC_IGNORE_PKGS='-x ', which is not the intended no-op. This is pre-existing logic carried over by the refactor, but worth fixing while it's being touched.

🐛 Proposed fix
-			if ignorePkgs := strings.Split(os.Getenv("SWAGGER_SPEC_IGNORE_PKGS"), ","); len(ignorePkgs) > 0 {
+			if raw := os.Getenv("SWAGGER_SPEC_IGNORE_PKGS"); raw != "" {
+				ignorePkgs := strings.Split(raw, ",")
 				for k, p := range ignorePkgs {
 					ignorePkgs[k] = "-x " + p
 				}
 				fmt.Printf(`export SWAGGER_SPEC_IGNORE_PKGS='%s'`, strings.Join(ignorePkgs, " "))
 			}
📝 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
if ignorePkgs := strings.Split(os.Getenv("SWAGGER_SPEC_IGNORE_PKGS"), ","); len(ignorePkgs) > 0 {
for k, p := range ignorePkgs {
ignorePkgs[k] = "-x " + p
}
fmt.Printf(`export SWAGGER_SPEC_IGNORE_PKGS='%s'`, strings.Join(ignorePkgs, " "))
}
if raw := os.Getenv("SWAGGER_SPEC_IGNORE_PKGS"); raw != "" {
ignorePkgs := strings.Split(raw, ",")
for k, p := range ignorePkgs {
ignorePkgs[k] = "-x " + p
}
fmt.Printf(`export SWAGGER_SPEC_IGNORE_PKGS='%s'`, strings.Join(ignorePkgs, " "))
}
🤖 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 `@cmd/dev/ci/github/env.go` around lines 54 - 59, The guard incorrectly treats
an unset/empty SWAGGER_SPEC_IGNORE_PKGS as non-empty because strings.Split("",
",") yields [""], causing export of "-x " entries; update the logic around
reading SWAGGER_SPEC_IGNORE_PKGS (the os.Getenv call and ignorePkgs handling) to
first check if the env var is non-empty (or filter out empty strings after
strings.Split) before building the "-x " list and calling fmt.Printf, ensuring
ignorePkgs contains only non-empty package names before joining and printing.

debug bool
pr string
revisionRange string
branch string
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify if `branch` is used anywhere in the monorepo package
rg -n '\bbranch\b' cmd/dev/ci/monorepo/

Repository: ory/cli

Length of output: 259


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files in cmd/dev/ci/monorepo:"
fd -t f "" cmd/dev/ci/monorepo/ | sort

echo
echo "Occurrences of branch/localBranch in package:"
rg -n --hidden --no-ignore-vcs '\b(branch|localBranch)\b' cmd/dev/ci/monorepo/

echo
echo "Occurrences of the \"branch\" flag string in package:"
rg -n --hidden --no-ignore-vcs '"branch"' cmd/dev/ci/monorepo/

Repository: ory/cli

Length of output: 1356


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search for concurrent execution patterns in the package (goroutines/channels):"
rg -n --hidden --no-ignore-vcs '\bgo\s+\w|make\(|chan\s' cmd/dev/ci/monorepo/

Repository: ory/cli

Length of output: 469


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' cmd/dev/ci/monorepo/main.go

Repository: ory/cli

Length of output: 1770


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Also search for how "localRoot/localPR/localRev" are used vs "localBranch"
rg -n --hidden --no-ignore-vcs '\blocal(PR|Root|Rev|Branch)\b' cmd/dev/ci/monorepo/main.go

Repository: ory/cli

Length of output: 614


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --hidden --no-ignore-vcs '\b(rootDirectory|verbose|debug|pr|revisionRange)\b' cmd/dev/ci/monorepo/

Repository: ory/cli

Length of output: 4697


Unused scratch variable branch in monorepo command

branch is declared and set from the --branch persistent flag (PersistentPreRun) but is never read anywhere in cmd/dev/ci/monorepo/—only pr is consumed (via isPR()) while other scratch vars (rootDirectory, verbose, debug, revisionRange) are used in run.go/changes.go. Remove branch (and the --branch flag binding) or wire it into the downstream logic.

🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 18-18: var branch is unused

(unused)

🤖 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 `@cmd/dev/ci/monorepo/main.go` at line 18, Remove the unused scratch variable
"branch" and its flag binding from the monorepo command or wire it into
downstream logic; specifically, delete the "branch string" declaration and the
corresponding "--branch" PersistentFlag setup in main.go (the PersistentPreRun
populates it) OR propagate the variable into the existing flow (e.g., pass
branch into the functions used by run.go/changes.go or use it inside
isPR()/change-detection logic) so the value is consumed rather than unused.

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