Skip to content

refactor(release): lean releaseTrain modules - #372

Merged
YosefHayim merged 1 commit into
mainfrom
refactor/release/release-train
Aug 7, 2026
Merged

refactor(release): lean releaseTrain modules#372
YosefHayim merged 1 commit into
mainfrom
refactor/release/release-train

Conversation

@YosefHayim

@YosefHayim YosefHayim commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Deslop src/core/releaseTrain/* and the thin CLI facade while keeping train car guards.
  • Share errorMessage, extract pure release-gate / presentation / marketing-version / live-train helpers, rename resolve/build stems (planTrainCars, createTrainRuntime, loadTargetTrain).
  • Add colocated business tests for gate, slug, status detail, usable marketing version, and train-record helpers; refresh docs test badges.

Before / after

Area Before After
Failures local instanceof Error / String(cause) shared errorMessage
Gate logic inline hold/force/approved flags in advanceTrain pure isReleaseGateOpen + isNativeFailure / isNativeApprovedOrReleased (tested)
Naming buildTrainRuntime, resolveTrainCars, resolveTarget createTrainRuntime, planTrainCars, loadTargetTrain
Presentation detail assembly buried in carStatusLine pure carStatusDetail / trainAppSlug
Guards car discriminators unchanged behavior (kept, slightly tightened form)

Gate

pnpm typecheck && pnpm lint && pnpm lint:style && pnpm docs:check && pnpm test && pnpm build

All six green. Unit: 2096 passing.

Notes

  • Path-scoped to releaseTrain + CLI registration only.
  • No behavior change to hold gate, OTA followers, or terminal-car rules.
  • Do not merge (orchestrator audits later).

Fixes #350


Summary by cubic

Leaned the releaseTrain modules by extracting pure helpers and unifying error handling with errorMessage, with no behavior changes. Adds focused tests and refreshes docs/test badges. Fixes #350.

  • Refactors
    • Renamed APIs: buildTrainRuntimecreateTrainRuntime, resolveTrainCarsplanTrainCars, resolveTargetloadTargetTrain, ResolveCarsInputTrainCarPlanInput.
    • Extracted helpers: release gate isReleaseGateOpen, isNativeFailure, isNativeApprovedOrReleased; presentation carStatusDetail, trainAppSlug; marketing-version usableMarketingVersion, marketingVersionMissingMessage; store-builds isUsableIosStoreBuild, isStoredAndroidBuild; records isLiveTrain, safeTrainId.
    • Consistent failures: command/orchestrator/builder/record now map errors via errorMessage for clearer messages.
    • Tests: added colocated tests for gate logic, slugging, status detail, input validation, and record helpers.

Written for commit cf11321. Summary will update on new commits.

Review in cubic

@YosefHayim YosefHayim added the domain:release Public release, rollout, release trains label Aug 6, 2026
@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: cf11321

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codeant-ai

codeant-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 1302f07 Aug 06, 2026 · 23:43 23:46

@codeant-ai

codeant-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@YosefHayim, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 699f9106-6719-43a3-ab5a-ee5052eeac63

📥 Commits

Reviewing files that changed from the base of the PR and between 9cfd610 and cf11321.

📒 Files selected for processing (12)
  • src/cli/commands/releaseTrain.ts
  • src/core/releaseTrain/builder.test.ts
  • src/core/releaseTrain/builder.ts
  • src/core/releaseTrain/command.test.ts
  • src/core/releaseTrain/command.ts
  • src/core/releaseTrain/engine.test.ts
  • src/core/releaseTrain/engine.ts
  • src/core/releaseTrain/guards.ts
  • src/core/releaseTrain/orchestrator.test.ts
  • src/core/releaseTrain/orchestrator.ts
  • src/core/releaseTrain/record.test.ts
  • src/core/releaseTrain/record.ts

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.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Aug 6, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Refactor release-train modules with shared error handling and pure helpers

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Extract pure release-train helpers (gate logic, presentation, marketing version, record helpers).
• Standardize failure messaging via shared errorMessage and rename builder/engine entrypoints.
• Add targeted unit tests and refresh docs/test-count badges.
Diagram

graph TD
  A["CLI release-train"] --> B["releaseTrain command"] --> C["planTrainCars"] --> D["advanceTrain gate"] --> E["createTrainRuntime"] --> F{{"Store/OTA services"}}
  B --> G[("Train records")]
  B --> H["errorMessage"]
  D --> H
  E --> H
  G --> H

  subgraph Legend
    direction LR
    _svc["Module/Service"] ~~~ _db[("Persistence")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The chosen approach—extracting pure helpers (gate, planner, presentation, record utilities) and centralizing unknown-cause formatting via errorMessage—is well-suited for improving testability and reducing duplication without changing runtime behavior. Alternatives like introducing a third-party slugify/error-formatting dependency or refactoring deeper into a new package boundary add operational/dependency cost without clear payoff for this scoped cleanup.

Files changed (23) +399 / -240

Refactor (7) +195 / -171
releaseTrain.tsSimplify CLI option assembly for release-train +15/-11

Simplify CLI option assembly for release-train

• Replaces the spread-based optional options construction with a single typed options object and explicit conditional assignments, preserving behavior while improving readability and type clarity.

src/cli/commands/releaseTrain.ts

builder.tsRename builder entrypoint and extract pure helper functions +63/-56

Rename builder entrypoint and extract pure helper functions

• Introduces createTrainRuntime (rename from buildTrainRuntime), centralizes runtime failure messages via errorMessage, and extracts testable helpers for store-build selection and marketing-version handling. Also streamlines attemptTransport to accept Effects directly and renames memoized client loaders for consistency.

src/core/releaseTrain/builder.ts

command.tsRefactor release-train command with pure helpers and renamed APIs +36/-39

Refactor release-train command with pure helpers and renamed APIs

• Adopts shared errorMessage formatting, extracts trainAppSlug and carStatusDetail, and rewires naming to createTrainRuntime/planTrainCars/loadTargetTrain. Also makes platform filter application explicit when building the car-plan input.

src/core/releaseTrain/command.ts

engine.tsRename resolveTrainCars to planTrainCars and tighten types +28/-27

Rename resolveTrainCars to planTrainCars and tighten types

• Renames the car-planning API, converts plan types to Readonly/readonly arrays, and clarifies docstrings. Also tightens androidCarState input type and makes empty-check explicit.

src/core/releaseTrain/engine.ts

guards.tsTighten and simplify release-train type guards +10/-9

Tighten and simplify release-train type guards

• Converts simple guards to concise expressions and slightly clarifies guard docstrings while preserving discriminator behavior and terminal-state logic.

src/core/releaseTrain/guards.ts

orchestrator.tsExtract and export gate helpers; standardize failure messages +32/-19

Extract and export gate helpers; standardize failure messages

• Replaces ad-hoc failure stringification with shared errorMessage, extracts gate decision logic into pure exported helpers, and slightly tightens OTA publish preconditions by avoiding optional chaining ambiguity.

src/core/releaseTrain/orchestrator.ts

record.tsExtract record helpers and standardize persistence failure messages +11/-10

Extract record helpers and standardize persistence failure messages

• Exports isLiveTrain and safeTrainId for reuse/testing, switches recordFailure to use shared errorMessage (with fallback), and reuses isLiveTrain when selecting the latest record.

src/core/releaseTrain/record.ts

Tests (5) +193 / -58
builder.test.tsAdd unit coverage for runtime builder pure helpers +62/-23

Add unit coverage for runtime builder pure helpers

• Updates tests to use createTrainRuntime and adds new assertions for iOS store-build usability, Android stored-build matching, marketing-version error messaging, and version normalization.

src/core/releaseTrain/builder.test.ts

command.test.tsTest slugging and presentation detail helpers +42/-2

Test slugging and presentation detail helpers

• Adds tests for trainAppSlug, validates schema behavior for explicit undefined optionals, and covers carStatusDetail behavior for native and OTA cars (including omission when no detail exists).

src/core/releaseTrain/command.test.ts

engine.test.tsRename and expand car-planning tests +22/-10

Rename and expand car-planning tests

• Updates tests from resolveTrainCars to planTrainCars and keeps coverage around platform selection and OTA follower inclusion rules, with some readability refactors.

src/core/releaseTrain/engine.test.ts

orchestrator.test.tsAdd tests for extracted release-gate helpers +28/-0

Add tests for extracted release-gate helpers

• Introduces unit tests for isNativeFailure/isNativeApprovedOrReleased and isReleaseGateOpen to validate hold/force/failure/approval combinations independently of the reconcile flow.

src/core/releaseTrain/orchestrator.test.ts

record.test.tsAdd tests for record helper functions +39/-23

Add tests for record helper functions

• Adds unit tests for isLiveTrain and safeTrainId, and refactors the test record factory for readability.

src/core/releaseTrain/record.test.ts

Documentation (11) +11 / -11
README.de.mdUpdate CI test badge count +1/-1

Update CI test badge count

• Refreshes the displayed Vitest passing-test count in the German README badge to match current CI.

README.de.md

README.es.mdUpdate CI test badge count +1/-1

Update CI test badge count

• Refreshes the displayed Vitest passing-test count in the Spanish README badge to match current CI.

README.es.md

README.fr.mdUpdate CI test badge count +1/-1

Update CI test badge count

• Refreshes the displayed Vitest passing-test count in the French README badge to match current CI.

README.fr.md

README.ja.mdUpdate CI test badge count +1/-1

Update CI test badge count

• Refreshes the displayed Vitest passing-test count in the Japanese README badge to match current CI.

README.ja.md

README.ko.mdUpdate CI test badge count +1/-1

Update CI test badge count

• Refreshes the displayed Vitest passing-test count in the Korean README badge to match current CI.

README.ko.md

README.mdUpdate CI test badge count +1/-1

Update CI test badge count

• Refreshes the displayed Vitest passing-test count in the main README badge to match current CI.

README.md

README.pt-BR.mdUpdate CI test badge count +1/-1

Update CI test badge count

• Refreshes the displayed Vitest passing-test count in the Portuguese (Brazil) README badge to match current CI.

README.pt-BR.md

README.ru.mdUpdate CI test badge count +1/-1

Update CI test badge count

• Refreshes the displayed Vitest passing-test count in the Russian README badge to match current CI.

README.ru.md

README.zh-CN.mdUpdate CI test badge count +1/-1

Update CI test badge count

• Refreshes the displayed Vitest passing-test count in the Simplified Chinese README badge to match current CI.

README.zh-CN.md

commands.mdRefresh command-reference test count +1/-1

Refresh command-reference test count

• Updates the command reference header line to reflect the new number of passing tests.

docs/commands.md

llms.txtRefresh documented test count +1/-1

Refresh documented test count

• Updates the test-count reference in llms.txt to reflect current CI totals.

llms.txt

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +72 to 78
if (!planInput.noOta && planInput.hasCloudStorage) {
ota = platforms.map((platform) => ({
platform,
channel: input.channel,
runtimeVersion: input.runtimeVersion,
channel: planInput.channel,
runtimeVersion: planInput.runtimeVersion,
}));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: OTA followers are created for every declared platform whenever cloud storage is configured, before native submission succeeds. If startTrain records a native submission failure, the corresponding OTA remains pending, while advanceTrain only publishes it when that native car is exactly released; because failed native cars are terminal and never transition to released, the train remains permanently in progress and watch mode cannot settle. Do not create the follower until the native leg is successfully submitted, or mark/cancel the follower when its native leg fails. [incomplete implementation]

Severity Level: Major ⚠️
- ❌ OTA-enabled trains remain permanently in progress after submission failure.
-`release-train status --watch` can poll indefinitely.
- ⚠️ Failed native legs leave orphaned pending OTA followers.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/core/releaseTrain/engine.ts
**Line:** 72:78
**Comment:**
	*Incomplete Implementation: OTA followers are created for every declared platform whenever cloud storage is configured, before native submission succeeds. If `startTrain` records a native submission failure, the corresponding OTA remains pending, while `advanceTrain` only publishes it when that native car is exactly `released`; because failed native cars are terminal and never transition to released, the train remains permanently in progress and watch mode cannot settle. Do not create the follower until the native leg is successfully submitted, or mark/cancel the follower when its native leg fails.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +46 to 64
const releaseTrainOptions: {
app?: string;
profile: string;
platform?: string;
ota: boolean;
hold?: boolean;
channel: string;
runtimeVersion?: string;
watch?: boolean;
json?: boolean;
} = {};
if (commandOptions.app !== undefined) optionalOptions.app = commandOptions.app;
if (commandOptions.platform !== undefined) optionalOptions.platform = commandOptions.platform;
if (commandOptions.hold !== undefined) optionalOptions.hold = commandOptions.hold;
if (commandOptions.runtimeVersion !== undefined)
optionalOptions.runtimeVersion = commandOptions.runtimeVersion;
if (commandOptions.watch !== undefined) optionalOptions.watch = commandOptions.watch;
if (commandOptions.json !== undefined) optionalOptions.json = commandOptions.json;
const releaseTrainOptions = {
env: string[];
includeLocal: boolean;
} = {
profile: commandOptions.profile,
ota: commandOptions.ota,
channel: commandOptions.channel,
env: commandOptions.env,
includeLocal: commandOptions.includeLocal,
...optionalOptions,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The CLI registers the shared --print-env option through addEnvFlags, but this newly constructed options object omits printEnv. As a result, invoking release-train --print-env silently discards the user's request and executes the train normally instead of printing the resolved environment or rejecting the unsupported option. Preserve the flag if release-train supports it, or stop registering it for this command. [api mismatch]

Severity Level: Major ⚠️
-`release-train --print-env` runs the train command unexpectedly.
- ⚠️ CI/debug environment inspection output is unavailable.
- ⚠️ Users may trigger status, startup, or store API work accidentally.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/cli/commands/releaseTrain.ts
**Line:** 46:64
**Comment:**
	*Api Mismatch: The CLI registers the shared `--print-env` option through `addEnvFlags`, but this newly constructed options object omits `printEnv`. As a result, invoking `release-train --print-env` silently discards the user's request and executes the train normally instead of printing the resolved environment or rejecting the unsupported option. Preserve the flag if release-train supports it, or stop registering it for this command.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +244 to +253
let versionCandidate = appDescriptor.version;
if (versionCandidate === undefined) {
const storeVersion = yield* attemptTransport(
'read App Store version',
ascClient.getLatestMarketingVersion(bundleId),
);
if (storeVersion !== null) versionString = storeVersion;
if (storeVersion !== null) versionCandidate = storeVersion;
}
if (versionString === undefined) {
const marketingVersion = usableMarketingVersion(versionCandidate);
if (marketingVersion === null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: A configured empty version never falls back to App Store Connect because the lookup is guarded only by versionCandidate === undefined. usableMarketingVersion then rejects the empty value and the train fails even when getLatestMarketingVersion could provide a valid version. Treat an empty configured version as absent before performing the fallback lookup. [incorrect condition logic]

Severity Level: Major ⚠️
- ❌ iOS release-train startup fails with an empty configured version.
- ⚠️ Valid App Store marketing-version fallback is skipped.
- ⚠️ Developers receive a failure despite an available store version.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/core/releaseTrain/builder.ts
**Line:** 244:253
**Comment:**
	*Incorrect Condition Logic: A configured empty version never falls back to App Store Connect because the lookup is guarded only by `versionCandidate === undefined`. `usableMarketingVersion` then rejects the empty value and the train fails even when `getLatestMarketingVersion` could provide a valid version. Treat an empty configured version as absent before performing the fallback lookup.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 23 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/core/releaseTrain/record.ts">

<violation number="1" location="src/core/releaseTrain/record.ts:73">
P3: For non-Error `cause` values, `errorMessage` returns `String(cause)` ("null", "undefined", "[object Object]"), which replaces the previous informative `${operation} failed.` fallback. Consider falling back to the operation label whenever the shared message yields a non-descriptive value, so users still see which operation failed.</violation>
</file>

<file name="src/core/releaseTrain/command.ts">

<violation number="1" location="src/core/releaseTrain/command.ts:97">
P3: The new `trainFailure` derives its message via the shared `errorMessage(cause)` for any non-Error cause. Because `errorMessage` falls back to `String(cause)`, a non-Error cause (a rejected object, or an undefined/null cause) now surfaces as literal 'undefined', 'null', or '[object Object]' in the release-train failure channel instead of the friendlier `${operation} failed.` fallback that the old code produced. The `message.length === 0` guard only catches empty strings, not these stringified placeholders. Consider guarding on a truthy/non-generic result, e.g. only use `errorMessage(cause)` when it yields a meaningful message and otherwise fall back to '${operation} failed.'</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment on lines +73 to +74
let message = errorMessage(cause);
if (message.length === 0) message = `${operation} failed.`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: For non-Error cause values, errorMessage returns String(cause) ("null", "undefined", "[object Object]"), which replaces the previous informative ${operation} failed. fallback. Consider falling back to the operation label whenever the shared message yields a non-descriptive value, so users still see which operation failed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/core/releaseTrain/record.ts, line 73:

<comment>For non-Error `cause` values, `errorMessage` returns `String(cause)` ("null", "undefined", "[object Object]"), which replaces the previous informative `${operation} failed.` fallback. Consider falling back to the operation label whenever the shared message yields a non-descriptive value, so users still see which operation failed.</comment>

<file context>
@@ -62,21 +63,25 @@ export const makeTrainRecordFailure = Data.tagged<TrainRecordFailure>('TrainReco
 const recordFailure = (operation: string, cause: unknown): TrainRecordFailure => {
-  let message = `${operation} failed.`;
-  if (cause instanceof Error) message = cause.message;
+  let message = errorMessage(cause);
+  if (message.length === 0) message = `${operation} failed.`;
   return makeTrainRecordFailure({ operation, message, cause });
</file context>
Suggested change
let message = errorMessage(cause);
if (message.length === 0) message = `${operation} failed.`;
let message = errorMessage(cause);
if (message.length === 0 || message === 'null' || message === 'undefined' || message === '[object Object]')
message = `${operation} failed.`;

let message = fallbackMessage;
if (message === undefined && cause instanceof Error) message = cause.message;
if (message === undefined) message = `${operation} failed.`;
if (message === undefined) message = errorMessage(cause);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The new trainFailure derives its message via the shared errorMessage(cause) for any non-Error cause. Because errorMessage falls back to String(cause), a non-Error cause (a rejected object, or an undefined/null cause) now surfaces as literal 'undefined', 'null', or '[object Object]' in the release-train failure channel instead of the friendlier ${operation} failed. fallback that the old code produced. The message.length === 0 guard only catches empty strings, not these stringified placeholders. Consider guarding on a truthy/non-generic result, e.g. only use errorMessage(cause) when it yields a meaningful message and otherwise fall back to '${operation} failed.'

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/core/releaseTrain/command.ts, line 97:

<comment>The new `trainFailure` derives its message via the shared `errorMessage(cause)` for any non-Error cause. Because `errorMessage` falls back to `String(cause)`, a non-Error cause (a rejected object, or an undefined/null cause) now surfaces as literal 'undefined', 'null', or '[object Object]' in the release-train failure channel instead of the friendlier `${operation} failed.` fallback that the old code produced. The `message.length === 0` guard only catches empty strings, not these stringified placeholders. Consider guarding on a truthy/non-generic result, e.g. only use `errorMessage(cause)` when it yields a meaningful message and otherwise fall back to '${operation} failed.'</comment>

<file context>
@@ -87,60 +88,62 @@ type PreparedTrain = Readonly<{
   let message = fallbackMessage;
-  if (message === undefined && cause instanceof Error) message = cause.message;
-  if (message === undefined) message = `${operation} failed.`;
+  if (message === undefined) message = errorMessage(cause);
+  if (message.length === 0) message = `${operation} failed.`;
   return makeReleaseTrainCommandFailure({ operation, message, cause });
</file context>

@YosefHayim
YosefHayim force-pushed the refactor/release/release-train branch from 1302f07 to 3d35f7f Compare August 7, 2026 10:47
Share errorMessage, extract pure release-gate / presentation / marketing-
version helpers, rename resolve stems (planTrainCars, createTrainRuntime,
loadTargetTrain), and keep car guards. Add colocated business tests for
gate, slug, status detail, and train-record helpers; refresh docs badges.

Fixes #350
@YosefHayim
YosefHayim force-pushed the refactor/release/release-train branch from 3d35f7f to cf11321 Compare August 7, 2026 10:48
@YosefHayim
YosefHayim merged commit 3853412 into main Aug 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain:release Public release, rollout, release trains size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(release): lean releaseTrain modules

1 participant