Fix potential crash: validate --all-builds with --output#766
Conversation
WalkthroughAdds mutual-exclusion validation to CLI argument parsing: disallow Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant CLI as configExporter/cli
participant Parser as parseArguments
participant Runner as runEntry
User->>CLI: invoke with flags
CLI->>Parser: parseArguments(argv)
alt conflicting flags
Parser-->>CLI: throw Error("mutually exclusive")
CLI->>Runner: catch Error
Runner->>CLI: print error & return non-zero
else valid args
Parser-->>CLI: return ExportOptions
CLI->>Runner: proceed with execution
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20–30 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
Code Review - PR #766SummaryThis PR successfully addresses issue #738 by preventing a potential runtime crash when using Strengths1. Correct Problem Identification
2. Consistent Implementation
3. Error Handling Improvements
4. Test Coverage
Observations & Suggestions1. Test Assertion Strategy (Minor) expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining("Config file")
)This is pragmatic given the test environment, but it couples the test to unrelated error behavior. The test comment acknowledges this limitation, which is good documentation. Alternative approach (optional): Mock the file system or create a minimal config file to test the happy path more directly. However, the current approach is acceptable for validating that the specific validation error doesn't occur. 2. TypeScript Type Safety (Enhancement Opportunity - Not blocking) Consider (in a future PR): Create a 3. Code Organization (Non-issue) Code Quality Checklist
Verification Questions
RecommendationLGTM with confidence ✅ This is a well-implemented fix that:
The suggestions above are minor enhancements for future consideration and don't block merging this PR. Review conducted by Claude Code following project guidelines from CLAUDE.md |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
test/package/configExporter.test.js (1)
197-235: Add coverage for two more invalid flag combos.
--all-buildswith--stdoutcurrently crashes (missing validation). Add a test to lock desired behavior.--stdoutwith--outputis ambiguous; recommend validation + test.Apply this test diff:
@@ describe("argument validation", () => { @@ test("rejects --all-builds with --output", async () => { const exitCode = await run(["--all-builds", "--output=config.yml"]) expect(exitCode).toBe(1) expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining( "--all-builds and --output are mutually exclusive" ) ) }) + test("rejects --all-builds with --stdout", async () => { + const exitCode = await run(["--all-builds", "--stdout"]) + expect(exitCode).toBe(1) + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining( + "--all-builds and --stdout are mutually exclusive" + ) + ) + }) + + test("rejects --stdout with --output", async () => { + const exitCode = await run(["--stdout", "--output=config.yml"]) + expect(exitCode).toBe(1) + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("--stdout and --output are mutually exclusive") + ) + }) + test("allows --all-builds with --save-dir", async () => { @@ }) })package/configExporter/cli.ts (3)
75-83: Graceful parse error handling: nice; consider definite-assignment tweak.With strict TS,
let options: ExportOptionsplus an earlyreturnincatchcan still trigger “used before assigned” in some setups. Two lightweight fixes:
- Use a definite assignment:
let options!: ExportOptions, or- Narrow post-try:
let options: ExportOptions | undefined; … if (!options) return 1;then useoptions!below.Keeps the two-phase error handling intact.
237-245: Defaults/doc mismatch for single-file output.Help says “default: yaml for files, inspect for stdout” and “annotate default with … file output”, but
applyDefaultssetsformat=inspectandannotate=falsewhenoutputis used. Recommend aligning:
- Files (dir or single file): default
format=yaml,annotate=true.- Stdout: default
format=inspect,annotate=false.Example implementation for
applyDefaults:function applyDefaults(options: ExportOptions): ExportOptions { const updated = { ...options } if (updated.stdout) { if (updated.format === undefined) updated.format = "inspect" if (updated.annotate === undefined) updated.annotate = false } else { // File outputs: either --save-dir or --output if (updated.format === undefined) updated.format = "yaml" if (updated.annotate === undefined) updated.annotate = true } // Default saveDir only for multi-file mode if (!updated.stdout && !updated.output && !updated.saveDir) { updated.saveDir = resolve(process.cwd(), "shakapacker-config-exports") } return updated }
705-705: Defensive: avoid!onsaveDirinrunAllBuildsCommand.Even with validation, a future regression could reintroduce an undefined
saveDir. Prefer a guard/fallback:const targetDir = resolvedOptions.saveDir ?? resolve(process.cwd(), "shakapacker-config-exports")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
package/configExporter/cli.ts(3 hunks)test/package/configExporter.test.js(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
package/configExporter/cli.ts (1)
package/configExporter/types.ts (1)
ExportOptions(1-25)
test/package/configExporter.test.js (1)
package/configExporter/cli.ts (1)
run(74-153)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: claude-review
- GitHub Check: Generator specs (ubuntu-latest, 2.7, gemfiles/Gemfile-rails.7.0.x)
🔇 Additional comments (2)
test/package/configExporter.test.js (1)
2-2: Good call importingrunfor black‑box CLI testing.This keeps validation logic testable without spawning a process.
package/configExporter/cli.ts (1)
157-157: Good:exitProcess(false)improves testability and lets us catch validation errors.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
test/package/configExporter.test.js (1)
210-232: Consider verifying error messages, not just exit behavior.The tests only check that
process.exitis called, but don't verify the error messages users will see. Since the validation errors include helpful guidance (e.g., "Use --save-dir instead"), it would be valuable to verify these messages are correct.You could capture stderr or yargs output to verify error messages:
test("rejects --all-builds with --output with helpful message", () => { const { parseArguments } = require("../../package/configExporter/cli") // Capture stderr const stderrWrite = jest.spyOn(process.stderr, 'write').mockImplementation() expect(() => { parseArguments(["--all-builds", "--output=config.yml"]) }).toThrow("process.exit called") const output = stderrWrite.mock.calls.map(call => call[0]).join('') expect(output).toContain("--all-builds and --output are mutually exclusive") expect(output).toContain("Use --save-dir instead") stderrWrite.mockRestore() })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
package/configExporter/cli.ts(2 hunks)test/package/configExporter.test.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- package/configExporter/cli.ts
🧰 Additional context used
🧬 Code graph analysis (1)
test/package/configExporter.test.js (1)
package/configExporter/cli.ts (1)
parseArguments(147-406)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Testing (ubuntu-latest, 20.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.1, gemfiles/Gemfile-rails.7.0.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.0, gemfiles/Gemfile-rails.7.0.x)
- GitHub Check: Generator specs (ubuntu-latest, 2.7, gemfiles/Gemfile-rails.7.1.x)
- GitHub Check: Generator specs (ubuntu-latest, 2.7, gemfiles/Gemfile-rails.7.0.x)
- GitHub Check: Testing (ubuntu-latest, 2.7, gemfiles/Gemfile-rails.7.1.x)
- GitHub Check: test
Add validation to prevent --all-builds from being used with --output, which would cause a runtime crash when saveDir is undefined. Changes: - Add yargs validation to reject --all-builds with --output - Update parseArguments to use exitProcess(false) for testability - Add error handling for yargs validation errors in run() - Add tests for the new validation Fixes #738 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add two additional validations to prevent runtime crashes: - Reject --all-builds with --stdout (would crash on saveDir!) - Reject --stdout with --output (ambiguous output mode) These complement the existing --all-builds with --output validation. All three cases would result in saveDir being undefined when the runAllBuildsCommand function expects it to be set. Changes: - Add validation for --all-builds with --stdout - Add validation for --stdout with --output - Add tests for both new validations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
CI was failing because existing tests expected parseArguments to be exported and to call process.exit on validation errors. My previous changes broke this by: 1. Not exporting parseArguments 2. Using exitProcess(false) which changed error handling behavior Changes: - Export parseArguments function for testability - Remove exitProcess(false) to maintain existing behavior - Simplify run() function - let parseArguments handle validation - Update test/package/configExporter.test.js to match existing patterns - Use process.exit mocking like test/package/configExporter/cli.test.js 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add test to verify that --all-builds is allowed when used with --save-dir, complementing the existing negative tests that verify the mutually exclusive combinations are rejected. This ensures the validation logic correctly allows the intended usage pattern while blocking the problematic combinations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
e27f3ec to
ca89c62
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
package/configExporter/cli.ts (1)
720-742: Validate saveDir in --all-builds path (security consistency).runAllBuildsCommand bypasses the safeResolvePath checks performed in run(), so a user-supplied --save-dir is not validated here. Mirror run()’s path validation to avoid accepting symlinks or paths outside the app root.
Apply this diff:
@@ - // Apply defaults - const resolvedOptions = applyDefaults(options) + // Apply defaults + const resolvedOptions = applyDefaults(options) + + // Validate saveDir for security (mirror run()) + if (resolvedOptions.saveDir) { + safeResolvePath(appRoot, resolvedOptions.saveDir) + } @@ - const targetDir = resolvedOptions.saveDir! // Set by applyDefaults + const targetDir = resolvedOptions.saveDir! // Validated aboveAlso applies to: 739-741
🧹 Nitpick comments (3)
package/configExporter/cli.ts (2)
720-742: Enforce annotate requires YAML in --all-builds path (UX parity).run() rejects --annotate with non-YAML formats, but runAllBuildsCommand doesn’t. Align behavior to avoid surprising differences.
Apply this diff:
@@ - const resolvedOptions = applyDefaults(options) + const resolvedOptions = applyDefaults(options) + + // Keep behavior consistent with run(): annotation is YAML-only + if (resolvedOptions.annotate && resolvedOptions.format !== "yaml") { + throw new Error( + "Annotation requires YAML format. Use --no-annotate or --format=yaml." + ) + }
165-181: Consider yargs.exitProcess(false) + fail handler for testability and catchability.Today parseArguments triggers process.exit on validation errors, so run() cannot catch them and tests must mock process.exit. Switching to thrown errors enables asserting exact messages and lets run()’s try/catch handle yargs failures.
Proposed diff:
- const argv = yargs(args) + const argv = yargs(args) + .exitProcess(false) @@ - .strict() - .parseSync() + .strict() + .fail((msg, err) => { + throw err || new Error(msg) + }) + .parseSync()If you adopt this, update tests to assert on the actual error messages instead of intercepting process.exit.
Also applies to: 404-406
test/package/configExporter.test.js (1)
286-331: Solid coverage for invalid combos; add-on suggestion if switching to thrown errors.The tests correctly assert failures for the three invalid pairs and permit --all-builds with --save-dir. If you adopt exitProcess(false)+fail, assert on the specific error text instead of “process.exit called” to verify messaging.
Example (post-change):
expect(() => parseArguments(["--all-builds", "--output=config.yml"])) .toThrow("--all-builds and --output are mutually exclusive. Use --save-dir instead.")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
package/configExporter/cli.ts(1 hunks)test/package/configExporter.test.js(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/package/configExporter.test.js (1)
package/configExporter/cli.ts (1)
parseArguments(165-439)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
- GitHub Check: Testing (ubuntu-latest, 3.1, gemfiles/Gemfile-rails.7.2.x)
- GitHub Check: Testing (ubuntu-latest, 3.0, gemfiles/Gemfile-rails.6.0.x)
- GitHub Check: Testing (ubuntu-latest, 2.7, gemfiles/Gemfile-rails.6.0.x)
- GitHub Check: claude-review
- GitHub Check: Generator specs (ubuntu-latest, 3.4, gemfiles/Gemfile-rails.7.2.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.1, gemfiles/Gemfile-rails.7.2.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.3, gemfiles/Gemfile-rails.7.0.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.3, gemfiles/Gemfile-rails.8.0.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.1, gemfiles/Gemfile-rails.7.0.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.2, gemfiles/Gemfile-rails.8.0.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.0, gemfiles/Gemfile-rails.7.0.x)
- GitHub Check: test
- GitHub Check: Generator specs (ubuntu-latest, 3.2, gemfiles/Gemfile-rails.7.2.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.3, gemfiles/Gemfile-rails.7.1.x)
- GitHub Check: Generator specs (ubuntu-latest, 2.7, gemfiles/Gemfile-rails.7.0.x)
- GitHub Check: Generator specs (ubuntu-latest, 3.4, gemfiles/Gemfile-rails.7.1.x)
- GitHub Check: Generator specs (ubuntu-latest, 2.7, gemfiles/Gemfile-rails.7.1.x)
🔇 Additional comments (1)
package/configExporter/cli.ts (1)
355-369: Good catch: comprehensive mutual-exclusion validation added.Now covers --all-builds+--output, --all-builds+--stdout, and --stdout+--output. This prevents the prior crash path and ambiguous modes.
|
@claude review this PR |
|
Claude finished @justin808's task —— View job PR Review for #766: Fix potential crash with --all-builds validationSummaryThis PR successfully addresses issue #738 by preventing a potential runtime crash when using Recommendation: ✅ APPROVE All CI checks are passing, and the implementation is solid. Detailed AnalysisProblem StatementThe const targetDir = resolvedOptions.saveDir! // Set by applyDefaultsHowever, Solution Quality ✅1. Validation Logic (cli.ts:355-369) Three new validations were added to the // Prevents crash from undefined saveDir
if (argv["all-builds"] && argv.output) {
throw new Error("--all-builds and --output are mutually exclusive. Use --save-dir instead.")
}
// Also prevents undefined saveDir
if (argv["all-builds"] && argv.stdout) {
throw new Error("--all-builds and --stdout are mutually exclusive. Use --save-dir instead.")
}
// Prevents ambiguous output mode
if (argv.stdout && argv.output) {
throw new Error("--stdout and --output are mutually exclusive. Use one or the other.")
}Strengths:
2. Test Coverage (test/package/configExporter.test.js:286-331) Four comprehensive tests added: test("rejects --all-builds with --output")
test("rejects --all-builds with --stdout")
test("rejects --stdout with --output")
test("allows --all-builds with --save-dir") // Positive caseStrengths:
Minor observation: Tests only verify that CLAUDE.md Compliance ✅
Changelog AssessmentAccording to CLAUDE.md, this is a user-visible bug fix (prevents a crash) and should have a changelog entry. Suggested changelog entry (to be added under - **Fix potential crash when using `--all-builds` with incompatible output flags**. [PR #766](https://github.com/shakacode/shakapacker/pull/766) by [justin808](https://github.com/justin808). Fixes [#738](https://github.com/shakacode/shakapacker/issues/738).
- Validates that `--all-builds` is not used with `--output` or `--stdout` (use `--save-dir` instead)
- Validates that `--stdout` and `--output` are not used together
- Prevents runtime crash from undefined `saveDir` in `runAllBuildsCommand`Code QualityPositive Observations
Additional Context from Review CommentsCodeRabbit's reviews provide some suggestions but don't identify blocking issues:
These are all good suggestions for future improvements but are out of scope for this focused bug fix PR. CI Status ✅All checks passing:
Minor Observations (Non-blocking)1. Test Error Message VerificationThe tests verify that expect(() => {
parseArguments(["--all-builds", "--output=config.yml"])
}).toThrow("process.exit called")Potential enhancement: If adopting expect(() => parseArguments(["--all-builds", "--output=config.yml"]))
.toThrow("--all-builds and --output are mutually exclusive")This is not blocking since the implementation is clearly correct. 2. Type Safety ConsiderationWhile this PR prevents the crash through runtime validation, TypeScript still allows the unsafe pattern via the non-null assertion Commit HistoryThe PR has 4 well-structured commits:
The iterative approach shows good response to review feedback (particularly commit 4 addressing CodeRabbit's suggestion). Final Recommendation✅ APPROVE - This PR is ready to merge. Strengths
Required Before Merge
Optional Future Enhancements (Separate PRs)
Great work fixing this crash! The implementation is solid and the tests give good confidence that the fix works correctly. |
Summary
Fixes #738 - Prevents a potential runtime crash when using
--all-buildswith--outputby adding proper validation.Problem
The
runAllBuildsCommandfunction (line 691) uses a non-null assertion onsaveDir:However,
applyDefaultsonly setssaveDirwhen!options.stdout && !options.output. This means when--outputis used with--all-builds,saveDirremainsundefined, causing a potential runtime crash.Solution
Added validation in the yargs
.check()block to reject the invalid combination of flags:Additional Changes
parseArgumentsto useexitProcess(false)for better testabilityrun()functionTest Plan
--all-buildswith--outputis rejected--all-buildswith--save-dirworks correctly🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
--all-buildswith--outputor--stdout, and prevents--stdoutwith--output. Error messages now direct users to use--save-dirand exit on invalid combinations.Tests