From 01a6eeafdca4cbbba6e7865bec4929713db68459 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 16:57:21 +0000 Subject: [PATCH 1/6] chore(biome): flag console in package source Package code logs through the logger: core's, or version's own utils/logging.ts. Biome's noConsole now enforces that in packages/*/src, except the loggers themselves and the CLI command layer (command.ts, commands/), which owns the terminal: the JSON result on stdout and the final error message on stderr. The version package's error paths dumped stack traces and git output with console.error next to log(..., 'error'); they now go through log, which writes the error level to stderr in both modes, so the stream is unchanged. Three outputs that aren't diagnostics keep console behind a suppression: version's --json result, preview's dry-run comment, and the RELEASEKIT_DEBUG prompt dump (folded into one write, same bytes), which must print regardless of log level or --quiet. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e --- biome.jsonc | 19 +++++++++++++++++++ packages/notes/src/llm/messages.ts | 6 ++++-- packages/release/src/preview/preview.ts | 1 + .../version/src/core/versionCalculator.ts | 5 +++-- packages/version/src/core/versionEngine.ts | 11 ++++++----- packages/version/src/git/commands.ts | 13 +++++++------ packages/version/src/utils/jsonOutput.ts | 1 + 7 files changed, 41 insertions(+), 15 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index c733a74d..0d4cc715 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -109,6 +109,25 @@ } } } + }, + { + // Package code logs through the logger (core's, or version's utils/logging.ts). Exempt are the + // loggers themselves and the CLI command layer, which owns the terminal: the JSON result on + // stdout, the final error message on stderr. + "includes": [ + "packages/*/src/**", + "!packages/core/src/logger.ts", + "!packages/version/src/utils/logging.ts", + "!packages/*/src/command.ts", + "!packages/*/src/commands/**" + ], + "linter": { + "rules": { + "suspicious": { + "noConsole": "error" + } + } + } } ], "assist": { diff --git a/packages/notes/src/llm/messages.ts b/packages/notes/src/llm/messages.ts index 5c9a57c5..b3c14e10 100644 --- a/packages/notes/src/llm/messages.ts +++ b/packages/notes/src/llm/messages.ts @@ -12,9 +12,11 @@ export interface CompleteResult { // example release notes from external repos — don't enable in shared CI logs. export function debugLogMessages(providerName: string, messages: LLMMessage[]): void { if (process.env.RELEASEKIT_DEBUG !== '1') return; - console.error(`[RELEASEKIT_DEBUG] ${providerName} messages (${messages.length}):`); + const lines = [`[RELEASEKIT_DEBUG] ${providerName} messages (${messages.length}):`]; for (const msg of messages) { const preview = msg.content.length > 300 ? `${msg.content.slice(0, 300)}…` : msg.content; - console.error(` [${msg.role}]: ${preview}`); + lines.push(` [${msg.role}]: ${preview}`); } + // biome-ignore lint/suspicious/noConsole: RELEASEKIT_DEBUG is its own opt-in, so the dump prints whatever the log level or --quiet. + console.error(lines.join('\n')); } diff --git a/packages/release/src/preview/preview.ts b/packages/release/src/preview/preview.ts index 4d85657e..131be842 100644 --- a/packages/release/src/preview/preview.ts +++ b/packages/release/src/preview/preview.ts @@ -210,6 +210,7 @@ export async function runPreview(options: PreviewOptions): Promise { if (!context || !forge) { // Dry-run mode or GitHub context unavailable — print to stdout + // biome-ignore lint/suspicious/noConsole: the comment body is the command's output here, not a diagnostic. console.log(commentBody); return; } diff --git a/packages/version/src/core/versionCalculator.ts b/packages/version/src/core/versionCalculator.ts index e73ff118..97c1b69e 100644 --- a/packages/version/src/core/versionCalculator.ts +++ b/packages/version/src/core/versionCalculator.ts @@ -3,6 +3,7 @@ */ import { cwd } from 'node:process'; +import { inspect } from 'node:util'; import { sanitizePackageName, shouldMatchPackageTargets } from '@releasekit/core'; import { Bumper } from 'conventional-recommended-bump'; import type { ReleaseType } from 'semver'; @@ -487,7 +488,7 @@ async function calculateVersionInner(config: Config, options: VersionOptions): P log(`Error in conventional commits calculation`, 'debug'); // Handle errors during conventional bump calculation log(`Failed to calculate version for ${name || 'project'}`, 'error'); - console.error(error); + log(inspect(error), 'error'); // Check if the error is specifically due to no tags found by underlying git commands if (error instanceof Error && error.message.includes('No names found')) { log('No tags found, proceeding with initial version calculation (if applicable).', 'info'); @@ -502,7 +503,7 @@ async function calculateVersionInner(config: Config, options: VersionOptions): P log(`Error in version calculation`, 'debug'); // Handle errors during conventional bump calculation log(`Failed to calculate version for ${name || 'project'}`, 'error'); - console.error(error); + log(inspect(error), 'error'); // Check if the error is specifically due to no tags found by underlying git commands if (error instanceof Error && error.message.includes('No names found')) { log('No tags found, proceeding with initial version calculation (if applicable).', 'info'); diff --git a/packages/version/src/core/versionEngine.ts b/packages/version/src/core/versionEngine.ts index e775bd4b..9b587828 100644 --- a/packages/version/src/core/versionEngine.ts +++ b/packages/version/src/core/versionEngine.ts @@ -1,6 +1,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { cwd } from 'node:process'; +import { inspect } from 'node:util'; import { getPackagesSync, type Package, type Packages } from '@manypkg/get-packages'; import { filterPackagesByConfig, parseCargoToml, parsePubspec } from '@releasekit/config'; import { isPrivatePackageJson, shouldMatchPackageTargets } from '@releasekit/core'; @@ -589,7 +590,7 @@ export class VersionEngine { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log(`Failed to get packages information: ${errorMessage}`, 'error'); - console.error(error); + log(inspect(error), 'error'); // Throw a more specific error for better error handling upstream throw createVersionError(VersionErrorCode.WORKSPACE_ERROR, errorMessage); @@ -617,11 +618,11 @@ export class VersionEngine { // Enhanced error logging for GitError if (error instanceof GitError) { - console.error('Git error details:'); + log('Git error details:', 'error'); if (error.message.includes('Command failed:')) { const cmdOutput = error.message.split('Command failed:')[1]; if (cmdOutput) { - console.error('Command output:', cmdOutput.trim()); + log(`Command output: ${cmdOutput.trim()}`, 'error'); } } } @@ -630,8 +631,8 @@ export class VersionEngine { log(`Version engine failed: ${errorMessage}`, 'error'); if (error instanceof Error && error.stack) { - console.error('Error stack trace:'); - console.error(error.stack); + log('Error stack trace:', 'error'); + log(error.stack, 'error'); } } throw error; diff --git a/packages/version/src/git/commands.ts b/packages/version/src/git/commands.ts index c68c87ad..f31fc2f3 100644 --- a/packages/version/src/git/commands.ts +++ b/packages/version/src/git/commands.ts @@ -1,4 +1,5 @@ import { cwd } from 'node:process'; +import { inspect } from 'node:util'; import { createGitCli, type Git } from '@releasekit/git'; import { createGitError, GitError, GitErrorCode } from '../errors/gitError.js'; import { addTag, setCommitMessage } from '../utils/jsonOutput.js'; @@ -152,8 +153,8 @@ export async function gitProcess(options: GitProcessOptions, git: Git = createGi log(`Git process error: ${errorMessage}`, 'error'); if (err instanceof Error && err.stack) { - console.error('Git process stack trace:'); - console.error(err.stack); + log('Git process stack trace:', 'error'); + log(err.stack, 'error'); } throw createGitError(GitErrorCode.GIT_PROCESS_ERROR, errorMessage); @@ -218,18 +219,18 @@ export async function createGitCommitAndTag( // Enhanced error logging if (error instanceof Error) { // Log the full stack trace for debugging - console.error('Git operation error details:'); - console.error(error.stack || error.message); + log('Git operation error details:', 'error'); + log(error.stack || error.message, 'error'); // Extract and log command output if available if (errorMessage.includes('Command failed:')) { const cmdOutput = errorMessage.split('Command failed:')[1]; if (cmdOutput) { - console.error('Git command output:', cmdOutput.trim()); + log(`Git command output: ${cmdOutput.trim()}`, 'error'); } } } else { - console.error('Unknown git error:', error); + log(`Unknown git error: ${inspect(error)}`, 'error'); } throw new GitError(`Git operation failed: ${errorMessage}`, GitErrorCode.GIT_ERROR); diff --git a/packages/version/src/utils/jsonOutput.ts b/packages/version/src/utils/jsonOutput.ts index 3676222b..e04fab23 100644 --- a/packages/version/src/utils/jsonOutput.ts +++ b/packages/version/src/utils/jsonOutput.ts @@ -291,6 +291,7 @@ export function getJsonData(): VersionOutput { */ export function printJsonOutput(): void { if (_jsonOutputMode) { + // biome-ignore lint/suspicious/noConsole: the JSON result is the command's stdout output, not a diagnostic. console.log(JSON.stringify(_jsonData, null, 2)); } } From 099a7a41a2ae01e873f6b7c936c75dedabb16880 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 17:37:39 +0000 Subject: [PATCH 2/6] test(version): stop the strategy unit tests running real cargo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit versionStrategies.spec.ts mocks node:fs, so the Cargo.lock lookup in syncCargoLockfile finds a lock and the real sync shells out to `cargo update --workspace --offline` — 78 times per run. On a cold CI runner that pushes "should update all packages to the same version" past its 5s timeout; it failed that way on #668 and #671. Mock the sync like the other collaborators. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e --- packages/version/test/unit/core/versionStrategies.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/version/test/unit/core/versionStrategies.spec.ts b/packages/version/test/unit/core/versionStrategies.spec.ts index 150b2b75..ffea0ad7 100644 --- a/packages/version/test/unit/core/versionStrategies.spec.ts +++ b/packages/version/test/unit/core/versionStrategies.spec.ts @@ -17,6 +17,8 @@ import * as jsonOutput from '../../../src/utils/jsonOutput.js'; import * as logging from '../../../src/utils/logging.js'; // Mock dependencies +// fs is mocked, so the Cargo.lock lookup finds a lock and the real sync would shell out to cargo. +vi.mock('../../../src/cargo/cargoLock.js'); vi.mock('../../../src/git/commands.js'); vi.mock('../../../src/git/tagsAndBranches.js'); vi.mock('../../../src/git/tagVerification.js'); From 62671a8d3918feff0131c28104047f29e8aad4a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 17:50:56 +0000 Subject: [PATCH 3/6] test(version): drop the comment on the cargoLock mock Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e --- packages/version/test/unit/core/versionStrategies.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/version/test/unit/core/versionStrategies.spec.ts b/packages/version/test/unit/core/versionStrategies.spec.ts index ffea0ad7..6b88dc8c 100644 --- a/packages/version/test/unit/core/versionStrategies.spec.ts +++ b/packages/version/test/unit/core/versionStrategies.spec.ts @@ -17,7 +17,6 @@ import * as jsonOutput from '../../../src/utils/jsonOutput.js'; import * as logging from '../../../src/utils/logging.js'; // Mock dependencies -// fs is mocked, so the Cargo.lock lookup finds a lock and the real sync would shell out to cargo. vi.mock('../../../src/cargo/cargoLock.js'); vi.mock('../../../src/git/commands.js'); vi.mock('../../../src/git/tagsAndBranches.js'); From 7cb3f7e92205f653d4007d4b110b51954d073fc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 18:32:11 +0000 Subject: [PATCH 4/6] style: add braces to every single-statement body Same sweep as #679, run on this branch so merging main resolves cleanly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e --- packages/config/src/load.ts | 36 +++- packages/config/src/merge.ts | 20 ++- .../config/test/unit/generate-schema.spec.ts | 8 +- packages/core/src/cargo.ts | 8 +- packages/core/src/changelogRefs.ts | 32 +++- packages/core/src/dependencyGraph.ts | 56 +++++-- packages/core/src/logger.ts | 16 +- packages/core/src/marker.ts | 8 +- packages/core/src/prerequisites.ts | 4 +- packages/core/test/unit/cargo.spec.ts | 4 +- packages/forge/src/errors.ts | 4 +- packages/forge/src/fake.ts | 16 +- packages/forge/src/github.ts | 48 ++++-- packages/forge/src/types.ts | 4 +- packages/forge/test/unit/github.spec.ts | 4 +- packages/git/src/errors.ts | 12 +- packages/git/src/fake.ts | 12 +- packages/git/src/git.ts | 75 ++++++--- packages/git/test/unit/git.spec.ts | 4 +- packages/notes/src/command.ts | 16 +- packages/notes/src/core/pipeline.ts | 28 +++- packages/notes/src/errors/index.ts | 4 +- .../notes/src/input/conventional-changelog.ts | 8 +- packages/notes/src/input/manual.ts | 8 +- packages/notes/src/llm/anthropic.ts | 4 +- packages/notes/src/llm/base.ts | 4 +- packages/notes/src/llm/context/prFetcher.ts | 12 +- packages/notes/src/llm/correctiveRetry.ts | 4 +- packages/notes/src/llm/examples/fetcher.ts | 20 ++- packages/notes/src/llm/examples/parser.ts | 24 ++- packages/notes/src/llm/messages.ts | 4 +- packages/notes/src/llm/ollama.ts | 4 +- packages/notes/src/llm/openai-compatible.ts | 4 +- packages/notes/src/llm/openai.ts | 4 +- packages/notes/src/llm/retryable.ts | 28 +++- packages/notes/src/llm/scopes.ts | 28 +++- packages/notes/src/llm/tasks/categorize.ts | 8 +- .../src/llm/tasks/enhance-and-categorize.ts | 19 ++- packages/notes/src/llm/tasks/shared.ts | 36 +++- packages/notes/src/monorepo/aggregator.ts | 4 +- packages/notes/src/output/markdown.ts | 52 ++++-- packages/notes/src/templates/loader.ts | 12 +- packages/notes/src/utils/concurrency.ts | 4 +- packages/notes/src/utils/json.ts | 8 +- packages/notes/src/utils/retry.ts | 4 +- packages/notes/test/eval/assertions.ts | 12 +- packages/notes/test/unit/llm.spec.ts | 20 ++- packages/notes/test/unit/llm/cache.spec.ts | 4 +- packages/notes/test/unit/llm/tasks.spec.ts | 40 +++-- packages/notes/test/unit/retry.spec.ts | 8 +- packages/notes/test/unit/templates.spec.ts | 4 +- .../notes/test/unit/utils/concurrency.spec.ts | 4 +- packages/publish/src/command.ts | 8 +- packages/publish/src/registry/cargo.ts | 16 +- packages/publish/src/registry/dispatcher.ts | 8 +- packages/publish/src/registry/npm.ts | 22 ++- packages/publish/src/registry/pub.ts | 4 +- packages/publish/src/stages/git-push.ts | 24 ++- packages/publish/src/stages/github-release.ts | 36 +++- packages/publish/src/stages/prepare.ts | 8 +- packages/publish/src/types.ts | 4 +- packages/publish/src/utils/cargo.ts | 4 +- packages/publish/src/utils/exec.ts | 4 +- packages/publish/src/utils/npm-env.ts | 4 +- packages/publish/src/utils/package-manager.ts | 12 +- packages/publish/src/utils/pub.ts | 8 +- packages/publish/src/utils/publish-retry.ts | 8 +- .../test/integration/publish-e2e.spec.ts | 16 +- .../test/unit/registry/dispatcher.spec.ts | 11 +- .../unit/stages/npm-publish-order.spec.ts | 4 +- .../release/src/backfill/github-release.ts | 16 +- packages/release/src/backfill/reconstruct.ts | 12 +- packages/release/src/backfill/repo-url.ts | 14 +- .../release/src/commands/backfill-command.ts | 12 +- packages/release/src/commands/changed.ts | 4 +- packages/release/src/commands/emitResult.ts | 8 +- packages/release/src/commands/gate-command.ts | 12 +- .../release/src/commands/labels-command.ts | 4 +- packages/release/src/draft/draft.ts | 4 +- packages/release/src/duration.ts | 24 ++- .../src/failure-report/failure-report.ts | 12 +- packages/release/src/failure-report/post.ts | 16 +- packages/release/src/gate/gate.ts | 16 +- packages/release/src/git.ts | 8 +- packages/release/src/github.ts | 8 +- packages/release/src/label-definitions.ts | 4 +- packages/release/src/label-utils.ts | 28 +++- packages/release/src/preview/detect.ts | 12 +- packages/release/src/preview/format.ts | 63 +++++-- packages/release/src/preview/merge.ts | 12 +- packages/release/src/preview/preview.ts | 42 +++-- packages/release/src/preview/refresh.ts | 16 +- packages/release/src/release.ts | 40 +++-- .../release/src/standing-pr/authorization.ts | 28 +++- .../src/standing-pr/changelog-region.ts | 67 ++++++-- .../release/src/standing-pr/notes-region.ts | 12 +- .../src/standing-pr/selection-region.ts | 135 +++++++++++---- .../release/src/standing-pr/standing-pr.ts | 158 +++++++++++++----- .../release/src/standing-pr/summary-region.ts | 36 +++- .../release/test/unit/authorization.spec.ts | 7 +- .../test/unit/commands/emitResult.spec.ts | 4 +- .../test/unit/failure-report-post.spec.ts | 7 +- packages/release/test/unit/gate.spec.ts | 12 +- packages/release/test/unit/refresh.spec.ts | 4 +- .../release/test/unit/standing-pr.spec.ts | 22 ++- packages/version/src/core/baselineResolver.ts | 12 +- packages/version/src/core/groupResolution.ts | 12 +- packages/version/src/core/groupStrategy.ts | 68 ++++++-- .../version/src/core/prerequisiteScope.ts | 10 +- .../version/src/core/versionCalculator.ts | 12 +- packages/version/src/core/versionEngine.ts | 64 +++++-- .../version/src/core/versionStrategies.ts | 20 ++- packages/version/src/core/workspaceGraph.ts | 4 +- packages/version/src/git/tagsAndBranches.ts | 8 +- .../version/src/package/packageProcessor.ts | 4 +- packages/version/src/utils/formatting.ts | 8 +- packages/version/src/utils/jsonOutput.ts | 76 ++++++--- packages/version/src/utils/logging.ts | 4 +- packages/version/src/utils/versionAction.ts | 4 +- packages/version/src/workspace.ts | 4 +- .../test/unit/core/groupStrategy.spec.ts | 80 ++++++--- .../test/unit/core/versionCalculator.spec.ts | 120 +++++++++---- .../test/unit/core/versionStrategies.spec.ts | 16 +- .../unit/package/packageProcessor.spec.ts | 20 ++- .../test/unit/utils/versionUtils.spec.ts | 116 +++++++++---- packages/version/test/utils/cli.ts | 4 +- scripts/examples-smoke/build-fixture.ts | 21 ++- .../examples-smoke/generate-smoke-workflow.ts | 20 ++- scripts/generate-config-docs.ts | 56 +++++-- scripts/generate-schema.ts | 28 +++- scripts/run-action.mjs | 48 ++++-- scripts/test-harness/index.mjs | 20 ++- scripts/test-harness/run-action-local.mjs | 8 +- test/integration/examples-smoke.spec.ts | 4 +- 134 files changed, 2075 insertions(+), 690 deletions(-) diff --git a/packages/config/src/load.ts b/packages/config/src/load.ts index 34b2ab45..237b4de3 100644 --- a/packages/config/src/load.ts +++ b/packages/config/src/load.ts @@ -45,13 +45,23 @@ function isRecord(value: unknown): value is Record { * keys — which would otherwise turn off a user's file output without a word. */ function assertNoRemovedReleaseNotesFields(config: unknown): void { - if (!isRecord(config) || !isRecord(config.notes) || !isRecord(config.notes.releaseNotes)) return; + if (!isRecord(config) || !isRecord(config.notes) || !isRecord(config.notes.releaseNotes)) { + return; + } const rn = config.notes.releaseNotes; const removed: string[] = []; - if ('mode' in rn) removed.push('mode'); - if ('directory' in rn) removed.push('directory'); - if (typeof rn.file === 'string') removed.push('file (string)'); - if (removed.length === 0) return; + if ('mode' in rn) { + removed.push('mode'); + } + if ('directory' in rn) { + removed.push('directory'); + } + if (typeof rn.file === 'string') { + removed.push('file (string)'); + } + if (removed.length === 0) { + return; + } throw new ConfigError( `notes.releaseNotes no longer supports ${removed.join(', ')}. Release notes go to the GitHub ` + 'release body by default; for in-repo per-version files set notes.releaseNotes.file to ' + @@ -66,12 +76,16 @@ function assertNoRemovedReleaseNotesFields(config: unknown): void { * generated JSON schema, where editors/CI reject the same unknown keys). */ function assertNoRemovedVersionFields(config: unknown): void { - if (!isRecord(config) || !isRecord(config.version)) return; + if (!isRecord(config) || !isRecord(config.version)) { + return; + } const v = config.version; const removed = ['versionStrategy', 'branchPatterns', 'defaultReleaseType', 'updateInternalDependencies'].filter( (k) => k in v, ); - if (removed.length === 0) return; + if (removed.length === 0) { + return; + } throw new ConfigError( `version no longer supports ${removed.join(', ')}. Branch-pattern versioning was never functional — ` + 'use Conventional Commits or --bump for version bumps. For coupled packages use version.groups; to ' + @@ -86,7 +100,9 @@ function assertNoRemovedVersionFields(config: unknown): void { * silently stripping these (which also diverges from the JSON schema's unknown-key rejection). */ function assertNoRemovedTopLevelFields(config: unknown): void { - if (!isRecord(config)) return; + if (!isRecord(config)) { + return; + } if (isRecord(config.ci)) { const removed = ['autoRelease', 'skipPatterns', 'minChanges'].filter((k) => k in (config.ci as object)); if (removed.length > 0) { @@ -154,7 +170,9 @@ export function loadVersionConfig(options?: LoadOptions): VersionConfig | undefi export function loadPublishConfig(options?: LoadOptions): PublishConfig | undefined { const config = loadConfig(options); - if (!config.publish) return undefined; + if (!config.publish) { + return undefined; + } const mergedGit = mergeGitConfig(config.git, config.publish.git); diff --git a/packages/config/src/merge.ts b/packages/config/src/merge.ts index 96e6b49b..6e663e9e 100644 --- a/packages/config/src/merge.ts +++ b/packages/config/src/merge.ts @@ -1,7 +1,9 @@ import type { GitConfig, PublishGitConfig } from './schema.js'; export function mergeGitConfig(topLevel?: GitConfig, packageLevel?: PublishGitConfig): GitConfig | undefined { - if (!topLevel && !packageLevel) return undefined; + if (!topLevel && !packageLevel) { + return undefined; + } const base: GitConfig = topLevel ?? { remote: 'origin', @@ -9,7 +11,9 @@ export function mergeGitConfig(topLevel?: GitConfig, packageLevel?: PublishGitCo pushMethod: 'auto', }; - if (!packageLevel) return base; + if (!packageLevel) { + return base; + } return { remote: packageLevel.remote ?? base.remote, @@ -27,9 +31,15 @@ export function deepMerge>( target: T | undefined, source: Partial | undefined, ): T | undefined { - if (!target && !source) return undefined; - if (!target) return source as T; - if (!source) return target; + if (!target && !source) { + return undefined; + } + if (!target) { + return source as T; + } + if (!source) { + return target; + } const result = { ...target }; diff --git a/packages/config/test/unit/generate-schema.spec.ts b/packages/config/test/unit/generate-schema.spec.ts index 3907c4c8..22061c68 100644 --- a/packages/config/test/unit/generate-schema.spec.ts +++ b/packages/config/test/unit/generate-schema.spec.ts @@ -37,7 +37,9 @@ describe('generate-schema', () => { let objectsChecked = 0; while (stack.length > 0) { const node = stack.pop(); - if (!node || typeof node !== 'object') continue; + if (!node || typeof node !== 'object') { + continue; + } const obj = node as Record; if (obj.type === 'object') { objectsChecked++; @@ -52,7 +54,9 @@ describe('generate-schema', () => { violations.push('object node has additionalProperties: true (silent leniency)'); } } - for (const value of Object.values(obj)) stack.push(value); + for (const value of Object.values(obj)) { + stack.push(value); + } } expect(violations).toEqual([]); expect(objectsChecked).toBeGreaterThan(10); diff --git a/packages/core/src/cargo.ts b/packages/core/src/cargo.ts index 1895e4d1..e7f5e95f 100644 --- a/packages/core/src/cargo.ts +++ b/packages/core/src/cargo.ts @@ -29,7 +29,9 @@ export function findCargoLockfile(startDir: string): string | undefined { while (true) { const lockPath = path.join(dir, 'Cargo.lock'); const hasLock = fs.existsSync(lockPath); - if (hasLock && !nearestLock) nearestLock = lockPath; + if (hasLock && !nearestLock) { + nearestLock = lockPath; + } const cargoToml = path.join(dir, 'Cargo.toml'); if (fs.existsSync(cargoToml) && isWorkspaceManifest(cargoToml)) { @@ -38,7 +40,9 @@ export function findCargoLockfile(startDir: string): string | undefined { } const parent = path.dirname(dir); - if (parent === dir) break; // reached the filesystem root + if (parent === dir) { + break; // reached the filesystem root + } dir = parent; } return nearestLock; diff --git a/packages/core/src/changelogRefs.ts b/packages/core/src/changelogRefs.ts index 0a8c150a..ff257f47 100644 --- a/packages/core/src/changelogRefs.ts +++ b/packages/core/src/changelogRefs.ts @@ -17,16 +17,22 @@ export type ChangelogRefsMode = 'strip' | 'escape' | 'link'; */ export function parseGitHubOwnerRepo(repoUrl: string): { owner: string; repo: string } | null { const scpMatch = repoUrl.match(/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/); - if (scpMatch) return { owner: scpMatch[1]!, repo: scpMatch[2]! }; + if (scpMatch) { + return { owner: scpMatch[1]!, repo: scpMatch[2]! }; + } try { const url = new URL(repoUrl); - if (url.hostname !== 'github.com') return null; + if (url.hostname !== 'github.com') { + return null; + } const parts = url.pathname .replace(/^\//, '') .replace(/\.git$/, '') .split('/'); - if (parts.length >= 2 && parts[0] && parts[1]) return { owner: parts[0], repo: parts[1] }; + if (parts.length >= 2 && parts[0] && parts[1]) { + return { owner: parts[0], repo: parts[1] }; + } } catch { // not a parseable URL } @@ -60,14 +66,18 @@ export function renderIssueRefs( repoUrl: string | null, prNumber?: string, ): string { - if (mode === 'strip') return ''; + if (mode === 'strip') { + return ''; + } const ids = issueIds ?? []; // prNumber is normally already in issueIds; fold it in defensively so no path can silently drop it. const allIds = prNumber && !ids.some((id) => Number(id.replace(/^#/, '')) === Number(prNumber.replace(/^#/, ''))) ? [prNumber, ...ids] : ids; - if (allIds.length === 0) return ''; + if (allIds.length === 0) { + return ''; + } const ownerRepo = mode === 'link' && repoUrl ? parseGitHubOwnerRepo(repoUrl) : null; @@ -76,7 +86,9 @@ export function renderIssueRefs( const pr = prNumber.replace(/^#/, ''); const prPart = `PR [#${pr}](https://github.com/${owner}/${repo}/pull/${pr})`; const closes = allIds.map((raw) => raw.replace(/^#/, '')).filter((num) => Number(num) !== Number(pr)); - if (closes.length === 0) return `(${prPart})`; + if (closes.length === 0) { + return `(${prPart})`; + } const closesPart = closes.map((num) => `[#${num}](https://github.com/${owner}/${repo}/issues/${num})`).join(', '); return `(${prPart} · closes ${closesPart})`; } @@ -144,10 +156,14 @@ export function neutralizeDescriptionRefs( CODE_SPAN_OR_DESC_REF, (match, lead: string | undefined, parenNum?: string, bareNum?: string) => { const num = parenNum ?? bareNum; - if (num === undefined) return match; // inline-code span — leave untouched + if (num === undefined) { + return match; // inline-code span — leave untouched + } const ws = lead ?? ''; // A ref already shown in the appended label is redundant here; `strip` drops every ref. - if (appended.has(Number(num)) || mode === 'strip') return ''; + if (appended.has(Number(num)) || mode === 'strip') { + return ''; + } const inner = ownerRepo ? `[#${num}](https://github.com/${ownerRepo.owner}/${ownerRepo.repo}/issues/${num})` : `\\#${num}`; diff --git a/packages/core/src/dependencyGraph.ts b/packages/core/src/dependencyGraph.ts index 7da923e7..473d1add 100644 --- a/packages/core/src/dependencyGraph.ts +++ b/packages/core/src/dependencyGraph.ts @@ -45,13 +45,19 @@ export function buildDependencyGraph(packages: GraphPackage[]): WorkspaceDepende const deps = new Map>(); const dependents = new Map>(); for (const p of packages) { - if (!deps.has(p.name)) deps.set(p.name, new Set()); - if (!dependents.has(p.name)) dependents.set(p.name, new Set()); + if (!deps.has(p.name)) { + deps.set(p.name, new Set()); + } + if (!dependents.has(p.name)) { + dependents.set(p.name, new Set()); + } } for (const p of packages) { for (const d of p.deps) { // Ignore self-edges and dependencies outside the workspace (the ∩-workspace filter). - if (d === p.name || !known.has(d)) continue; + if (d === p.name || !known.has(d)) { + continue; + } deps.get(p.name)?.add(d); dependents.get(d)?.add(p.name); } @@ -65,9 +71,13 @@ export function buildDependencyGraph(packages: GraphPackage[]): WorkspaceDepende const stack = [...(deps.get(name) ?? [])]; while (stack.length > 0) { const cur = stack.pop(); - if (cur === undefined || cur === name || out.has(cur)) continue; + if (cur === undefined || cur === name || out.has(cur)) { + continue; + } out.add(cur); - for (const d of deps.get(cur) ?? []) stack.push(d); + for (const d of deps.get(cur) ?? []) { + stack.push(d); + } } return out; }; @@ -78,7 +88,11 @@ export function buildDependencyGraph(packages: GraphPackage[]): WorkspaceDepende const indegree = new Map(); for (const n of subset) { let degree = 0; - for (const dep of deps.get(n) ?? []) if (inSubset.has(dep)) degree++; + for (const dep of deps.get(n) ?? []) { + if (inSubset.has(dep)) { + degree++; + } + } indegree.set(n, degree); } // Seed with nodes that have no in-subset dependencies, preserving input order for stability. @@ -87,18 +101,28 @@ export function buildDependencyGraph(packages: GraphPackage[]): WorkspaceDepende const emitted = new Set(); while (ready.length > 0) { const n = ready.shift(); - if (n === undefined || emitted.has(n)) continue; + if (n === undefined || emitted.has(n)) { + continue; + } result.push(n); emitted.add(n); for (const dependent of dependents.get(n) ?? []) { - if (!inSubset.has(dependent) || emitted.has(dependent)) continue; + if (!inSubset.has(dependent) || emitted.has(dependent)) { + continue; + } const next = (indegree.get(dependent) ?? 1) - 1; indegree.set(dependent, next); - if (next === 0) ready.push(dependent); + if (next === 0) { + ready.push(dependent); + } } } // Cycle members never reach indegree 0 — append them in input order rather than dropping them. - for (const n of subset) if (!emitted.has(n)) result.push(n); + for (const n of subset) { + if (!emitted.has(n)) { + result.push(n); + } + } return result; }; @@ -107,17 +131,23 @@ export function buildDependencyGraph(packages: GraphPackage[]): WorkspaceDepende if (cycle === undefined) { // A full toposort that can't emit every node has a cycle. const indegree = new Map(); - for (const p of packages) indegree.set(p.name, deps.get(p.name)?.size ?? 0); + for (const p of packages) { + indegree.set(p.name, deps.get(p.name)?.size ?? 0); + } const ready = packages.filter((p) => indegree.get(p.name) === 0).map((p) => p.name); let emittedCount = 0; while (ready.length > 0) { const n = ready.shift(); - if (n === undefined) break; + if (n === undefined) { + break; + } emittedCount++; for (const dependent of dependents.get(n) ?? []) { const next = (indegree.get(dependent) ?? 1) - 1; indegree.set(dependent, next); - if (next === 0) ready.push(dependent); + if (next === 0) { + ready.push(dependent); + } } } cycle = emittedCount !== known.size; diff --git a/packages/core/src/logger.ts b/packages/core/src/logger.ts index 5169100f..d194f3ed 100644 --- a/packages/core/src/logger.ts +++ b/packages/core/src/logger.ts @@ -64,14 +64,20 @@ export function getLogLevel(): LogLevel { function shouldLog(level: LogLevel): boolean { // JSON mode no longer suppresses stderr logging — JSON output goes to stdout, // logs go to stderr, so they don't interfere with each other. - if (quietMode && level !== 'error') return false; + if (quietMode && level !== 'error') { + return false; + } // Debug messages are shown when DEBUG env var is set or log level allows it - if (level === 'debug' && (process.env.DEBUG === 'true' || process.env.DEBUG === '1')) return true; + if (level === 'debug' && (process.env.DEBUG === 'true' || process.env.DEBUG === '1')) { + return true; + } return LOG_LEVELS[level] <= LOG_LEVELS[currentLevel]; } export function log(message: string, level: LogLevel = 'info'): void { - if (!shouldLog(level)) return; + if (!shouldLog(level)) { + return; + } const formatted = COLORS[level](`${PREFIXES[level]} ${message}`); @@ -91,7 +97,9 @@ export function info(message: string): void { } export function success(message: string): void { - if (!shouldLog('info')) return; + if (!shouldLog('info')) { + return; + } console.error(chalk.green(`[SUCCESS] ${message}`)); } diff --git a/packages/core/src/marker.ts b/packages/core/src/marker.ts index af91d6cf..bc9d61af 100644 --- a/packages/core/src/marker.ts +++ b/packages/core/src/marker.ts @@ -14,10 +14,14 @@ /** Linear extract of the trimmed text between `open` and `close`. */ function sliceBetween(body: string, open: string, close: string, restIfNoClose: boolean): string | undefined { const start = body.indexOf(open); - if (start === -1) return undefined; + if (start === -1) { + return undefined; + } const from = start + open.length; const end = body.indexOf(close, from); - if (end === -1) return restIfNoClose ? body.slice(from).trim() : undefined; + if (end === -1) { + return restIfNoClose ? body.slice(from).trim() : undefined; + } return body.slice(from, end).trim(); } diff --git a/packages/core/src/prerequisites.ts b/packages/core/src/prerequisites.ts index 1bbfcbaf..e0272b88 100644 --- a/packages/core/src/prerequisites.ts +++ b/packages/core/src/prerequisites.ts @@ -53,7 +53,9 @@ export function resolvePrerequisites( } } } - for (const name of prereqSet) targetSet.add(name); + for (const name of prereqSet) { + targetSet.add(name); + } // Order by the FULL release set's dependency order, then filter to the derived subset — so a // prerequisite that depends on an explicit target sorts after it. Ordering the prerequisite slice diff --git a/packages/core/test/unit/cargo.spec.ts b/packages/core/test/unit/cargo.spec.ts index bd2da50d..1769e6ec 100644 --- a/packages/core/test/unit/cargo.spec.ts +++ b/packages/core/test/unit/cargo.spec.ts @@ -14,7 +14,9 @@ describe('findCargoLockfile', () => { } afterEach(() => { - for (const dir of tmpDirs) fs.rmSync(dir, { recursive: true, force: true }); + for (const dir of tmpDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } tmpDirs.length = 0; }); diff --git a/packages/forge/src/errors.ts b/packages/forge/src/errors.ts index 0b72c0de..dfcee186 100644 --- a/packages/forge/src/errors.ts +++ b/packages/forge/src/errors.ts @@ -7,7 +7,9 @@ export function forgeErrorStatus(error: unknown): number | undefined { if (error && typeof error === 'object' && 'status' in error) { const { status } = error as { status?: unknown }; - if (typeof status === 'number') return status; + if (typeof status === 'number') { + return status; + } } return undefined; } diff --git a/packages/forge/src/fake.ts b/packages/forge/src/fake.ts index 77ca9eb8..6d78d6ba 100644 --- a/packages/forge/src/fake.ts +++ b/packages/forge/src/fake.ts @@ -193,8 +193,12 @@ export class FakeForge implements Forge { this.updatedIssues.push({ issueNumber, changes }); const existing = this.issues[issueNumber]; if (existing) { - if (changes.body !== undefined) existing.body = changes.body; - if (changes.title !== undefined) existing.title = changes.title; + if (changes.body !== undefined) { + existing.body = changes.body; + } + if (changes.title !== undefined) { + existing.title = changes.title; + } } // Closing drops it from open-issue discovery. if (changes.state === 'closed') { @@ -226,7 +230,9 @@ export class FakeForge implements Forge { async updateComment(commentId: number, body: string): Promise { this.updatedComments.push({ commentId, body }); const comment = this.comments.find((c) => c.id === commentId); - if (comment) comment.body = body; + if (comment) { + comment.body = body; + } } async upsertMarkerComment(prNumber: number, marker: string, body: string): Promise { @@ -246,7 +252,9 @@ export class FakeForge implements Forge { async createLabel(label: NewLabel): Promise { this.createdLabels.push(label); - if (this.labelNames.includes(label.name)) return 'exists'; + if (this.labelNames.includes(label.name)) { + return 'exists'; + } this.labelNames.push(label.name); return 'created'; } diff --git a/packages/forge/src/github.ts b/packages/forge/src/github.ts index b95ee3fd..2ddf1bd5 100644 --- a/packages/forge/src/github.ts +++ b/packages/forge/src/github.ts @@ -42,8 +42,12 @@ function labelNames(labels: LabelLike[] | undefined): string[] { /** Whether a createLabel error is GitHub's idempotent "label already exists" 422. */ function isAlreadyExistsError(error: unknown): boolean { - if (typeof error !== 'object' || error === null) return false; - if ((error as { status?: number }).status !== 422) return false; + if (typeof error !== 'object' || error === null) { + return false; + } + if ((error as { status?: number }).status !== 422) { + return false; + } // A 422 for "already exists" carries an errors[].code === 'already_exists'. Other 422s (name too // long, disallowed characters, …) are real validation failures and must surface to the caller. const errors = (error as { response?: { data?: { errors?: Array<{ code?: string }> } } }).response?.data?.errors; @@ -111,7 +115,9 @@ export class GitHubForge implements Forge { for (const pr of data) { prs.push({ number: pr.number, headRef: pr.head.ref, draft: pr.draft ?? false, baseSha: pr.base.sha }); } - if (data.length < 100) break; + if (data.length < 100) { + break; + } } return prs; } @@ -166,7 +172,9 @@ export class GitHubForge implements Forge { }); for await (const response of iterator) { for (const issue of response.data) { - if (!issue.pull_request) return { number: issue.number, url: issue.html_url }; + if (!issue.pull_request) { + return { number: issue.number, url: issue.html_url }; + } } } return null; @@ -178,14 +186,18 @@ export class GitHubForge implements Forge { // Prefer `role_name` — it distinguishes maintain/triage, which the coarse `permission` // ('admin'|'write'|'read'|'none') collapses. Fall back to `permission` for older API shapes. const role = data.role_name as RepoPermission | undefined; - if (role && REPO_PERMISSIONS.includes(role)) return role; + if (role && REPO_PERMISSIONS.includes(role)) { + return role; + } return (data.permission as RepoPermission) ?? 'none'; } catch (error) { // 404 = not a collaborator / unknown user → genuinely no permission. Any other error (403 from a // mis-scoped token, 429 rate limit, 5xx, network) means we CAN'T determine permission — surface // it rather than silently reporting 'none', which would fail the gate closed for every real // actor (even admins) with no diagnostic. - if (forgeErrorStatus(error) === 404) return 'none'; + if (forgeErrorStatus(error) === 404) { + return 'none'; + } throw error; } } @@ -199,7 +211,9 @@ export class GitHubForge implements Forge { // 404 = not a member. Any other error (403 = the token lacks org-read scope, network, …) can't // be answered — surface it (the caller's gate wrapper warns and fails closed) rather than // silently reporting "not a member", which is indistinguishable from a real non-member. - if (forgeErrorStatus(error) === 404) return false; + if (forgeErrorStatus(error) === 404) { + return false; + } throw error; } } @@ -216,13 +230,17 @@ export class GitHubForge implements Forge { let humanMatch: ForgeComment | null = null; for await (const response of iterator) { for (const comment of response.data) { - if (!comment.body?.startsWith(marker)) continue; + if (!comment.body?.startsWith(marker)) { + continue; + } const found: ForgeComment = { id: comment.id, body: comment.body, user: comment.user ? { login: comment.user.login, type: comment.user.type } : undefined, }; - if (isBotComment(found)) return found; + if (isBotComment(found)) { + return found; + } humanMatch ??= found; } } @@ -256,7 +274,9 @@ export class GitHubForge implements Forge { per_page: 100, }); for await (const response of iterator) { - for (const label of response.data) names.push(label.name); + for (const label of response.data) { + names.push(label.name); + } } return names; } @@ -266,7 +286,9 @@ export class GitHubForge implements Forge { await this.octokit.rest.issues.createLabel({ ...this.base, ...label }); return 'created'; } catch (error) { - if (isAlreadyExistsError(error)) return 'exists'; + if (isAlreadyExistsError(error)) { + return 'exists'; + } throw error; } } @@ -286,7 +308,9 @@ export class GitHubForge implements Forge { for (const r of data) { releases.push({ tagName: r.tag_name, draft: r.draft, prerelease: r.prerelease, body: r.body ?? '' }); } - if (data.length < 100) break; + if (data.length < 100) { + break; + } } return releases; } diff --git a/packages/forge/src/types.ts b/packages/forge/src/types.ts index 7c342f3d..b2f35a27 100644 --- a/packages/forge/src/types.ts +++ b/packages/forge/src/types.ts @@ -99,7 +99,9 @@ export interface ForgeComment { */ export function isBotComment(comment: Pick): boolean { const user = comment.user; - if (!user) return true; + if (!user) { + return true; + } return user.type === 'Bot' || (user.login !== undefined && user.login.endsWith('[bot]')); } diff --git a/packages/forge/test/unit/github.spec.ts b/packages/forge/test/unit/github.spec.ts index 1ebed68c..d7054626 100644 --- a/packages/forge/test/unit/github.spec.ts +++ b/packages/forge/test/unit/github.spec.ts @@ -72,7 +72,9 @@ function makeOctokit() { function pages(...batches: T[][]) { return { async *[Symbol.asyncIterator]() { - for (const data of batches) yield { data }; + for (const data of batches) { + yield { data }; + } }, }; } diff --git a/packages/git/src/errors.ts b/packages/git/src/errors.ts index 50d682e3..58d2ac2f 100644 --- a/packages/git/src/errors.ts +++ b/packages/git/src/errors.ts @@ -46,7 +46,9 @@ export class GitError extends Error { export function gitExitCode(error: unknown): number | undefined { if (error && typeof error === 'object' && 'code' in error) { const { code } = error as { code?: unknown }; - if (typeof code === 'number') return code; + if (typeof code === 'number') { + return code; + } } return undefined; } @@ -57,7 +59,9 @@ export function gitExitCode(error: unknown): number | undefined { * that otherwise swallows a non-zero exit — because it is a configuration failure, not a "no" answer. */ export function isCommandNotFound(error: unknown): boolean { - if (!error || typeof error !== 'object') return false; + if (!error || typeof error !== 'object') { + return false; + } const { code } = error as { code?: unknown }; return code === 'ENOENT'; } @@ -68,6 +72,8 @@ export function isCommandNotFound(error: unknown): boolean { * push from an ordinary non-zero exit and report it as a timeout. */ export function isExecTimeout(error: unknown): boolean { - if (!error || typeof error !== 'object') return false; + if (!error || typeof error !== 'object') { + return false; + } return (error as { killed?: unknown }).killed === true; } diff --git a/packages/git/src/fake.ts b/packages/git/src/fake.ts index f0fccb7c..fd28d568 100644 --- a/packages/git/src/fake.ts +++ b/packages/git/src/fake.ts @@ -76,8 +76,12 @@ export interface FakeGitSeed { } function ancestryHas(ancestors: FakeGitSeed['ancestors'], ancestor: string, ref: string): boolean { - if (!ancestors) return false; - if (typeof ancestors === 'function') return ancestors(ancestor, ref); + if (!ancestors) { + return false; + } + if (typeof ancestors === 'function') { + return ancestors(ancestor, ref); + } return (ancestors[ref] ?? []).includes(ancestor); } @@ -210,7 +214,9 @@ export class FakeGit implements Git { async tag(name: string, opts: GitTagOptions = {}): Promise { this.tagged.push({ name, message: opts.message }); - if (!this.tagsList.includes(name)) this.tagsList.push(name); + if (!this.tagsList.includes(name)) { + this.tagsList.push(name); + } } async fetch(remote: string, _opts: GitFetchOptions = {}): Promise { diff --git a/packages/git/src/git.ts b/packages/git/src/git.ts index 468ffac4..37866d50 100644 --- a/packages/git/src/git.ts +++ b/packages/git/src/git.ts @@ -163,7 +163,9 @@ export class GitCli implements Git { async listTags(opts: GitListTagsOptions = {}): Promise { const args = ['tag']; - if (opts.sort) args.push(`--sort=${opts.sort}`); + if (opts.sort) { + args.push(`--sort=${opts.sort}`); + } const { stdout } = await this.exec(args, { cwd: this.cwd(opts.cwd) }); return splitLines(stdout); } @@ -203,18 +205,30 @@ export class GitCli implements Git { async countCommits(range: string, opts: GitCountCommitsOptions = {}): Promise { assertNotOption(range, 'range'); const args = ['rev-list', '--count', range]; - if (opts.path) args.push('--', opts.path); + if (opts.path) { + args.push('--', opts.path); + } const { stdout } = await this.exec(args, { cwd: this.cwd(opts.cwd) }); return Number.parseInt(stdout.trim(), 10) || 0; } async log(opts: GitLogOptions): Promise { - if (opts.range !== undefined) assertNotOption(opts.range, 'range'); + if (opts.range !== undefined) { + assertNotOption(opts.range, 'range'); + } const args = ['log']; - if (opts.format !== undefined) args.push(`--format=${opts.format}`); - if (opts.extraArgs) args.push(...opts.extraArgs); - if (opts.range !== undefined) args.push(opts.range); - if (opts.paths && opts.paths.length > 0) args.push('--', ...opts.paths); + if (opts.format !== undefined) { + args.push(`--format=${opts.format}`); + } + if (opts.extraArgs) { + args.push(...opts.extraArgs); + } + if (opts.range !== undefined) { + args.push(opts.range); + } + if (opts.paths && opts.paths.length > 0) { + args.push('--', ...opts.paths); + } const { stdout } = await this.exec(args, { cwd: this.cwd(opts.cwd) }); return stdout; } @@ -230,10 +244,16 @@ export class GitCli implements Git { async forEachRef(opts: GitForEachRefOptions): Promise { // `format`/`sort` are flag values by design; `pattern` is a positional ref, so guard it like // every other user-controlled positional (a leading-`-` pattern would be read as an option). - if (opts.pattern !== undefined) assertNotOption(opts.pattern, 'pattern'); + if (opts.pattern !== undefined) { + assertNotOption(opts.pattern, 'pattern'); + } const args = ['for-each-ref', `--format=${opts.format}`]; - if (opts.sort) args.push(`--sort=${opts.sort}`); - if (opts.pattern) args.push(opts.pattern); + if (opts.sort) { + args.push(`--sort=${opts.sort}`); + } + if (opts.pattern) { + args.push(opts.pattern); + } const { stdout } = await this.exec(args, { cwd: this.cwd(opts.cwd) }); return splitLines(stdout); } @@ -262,8 +282,12 @@ export class GitCli implements Git { async commit(message: string, opts: GitCommitOptions = {}): Promise { const args = ['commit', '-m', message]; - if (opts.skipHooks) args.push('--no-verify'); - if (opts.paths && opts.paths.length > 0) args.push('--', ...opts.paths); + if (opts.skipHooks) { + args.push('--no-verify'); + } + if (opts.paths && opts.paths.length > 0) { + args.push('--', ...opts.paths); + } await this.exec(args, { cwd: this.cwd(opts.cwd) }); } @@ -292,22 +316,35 @@ export class GitCli implements Git { async status(opts: GitStatusOptions = {}): Promise { const args = ['status']; - if (opts.porcelain) args.push('--porcelain'); - if (opts.paths && opts.paths.length > 0) args.push('--', ...opts.paths); + if (opts.porcelain) { + args.push('--porcelain'); + } + if (opts.paths && opts.paths.length > 0) { + args.push('--', ...opts.paths); + } const { stdout } = await this.exec(args, { cwd: this.cwd(opts.cwd) }); return stdout; } async push(opts: GitPushOptions): Promise { assertNotOption(opts.remote, 'remote'); - if (opts.ref !== undefined) assertNotOption(opts.ref, 'ref'); + if (opts.ref !== undefined) { + assertNotOption(opts.ref, 'ref'); + } const args = ['push']; - if (opts.forceWithLease) args.push('--force-with-lease'); - else if (opts.force) args.push('--force'); - if (opts.tags) args.push('--tags'); + if (opts.forceWithLease) { + args.push('--force-with-lease'); + } else if (opts.force) { + args.push('--force'); + } + if (opts.tags) { + args.push('--tags'); + } // `--end-of-options`: remote/ref can never be parsed as git options (`--receive-pack=…` → RCE). args.push('--end-of-options', opts.remote); - if (opts.ref !== undefined) args.push(opts.ref); + if (opts.ref !== undefined) { + args.push(opts.ref); + } // A push can hang on a stalled network forever; a hard timeout preserves the safety the publish // package previously got from its async exec wrapper. execFile kills the child on expiry. await this.exec(args, { cwd: this.cwd(opts.cwd), timeout: opts.timeoutMs ?? DEFAULT_PUSH_TIMEOUT_MS }); diff --git a/packages/git/test/unit/git.spec.ts b/packages/git/test/unit/git.spec.ts index edff51cc..3c98864c 100644 --- a/packages/git/test/unit/git.spec.ts +++ b/packages/git/test/unit/git.spec.ts @@ -23,7 +23,9 @@ function makeRunner(responses: { stdout?: string; stderr?: string; reject?: unkn const calls: Call[] = []; const run: GitRunner = vi.fn(async (args, options) => { calls.push({ args, options }); - if (responses.reject !== undefined) throw responses.reject; + if (responses.reject !== undefined) { + throw responses.reject; + } return { stdout: responses.stdout ?? '', stderr: responses.stderr ?? '' }; }); return { run, calls }; diff --git a/packages/notes/src/command.ts b/packages/notes/src/command.ts index 78085119..b49143b5 100644 --- a/packages/notes/src/command.ts +++ b/packages/notes/src/command.ts @@ -37,7 +37,9 @@ export function createNotesCommand(): Command { .option('-q, --quiet', 'Suppress non-error output') .action(async (options) => { setVerbosity(options.verbose); - if (options.quiet) setQuietMode(true); + if (options.quiet) { + setQuietMode(true); + } try { const loadedConfig = loadConfig({ cwd: process.cwd(), configPath: options.config }); @@ -113,9 +115,15 @@ export function createNotesCommand(): Command { model: existingLlm?.model, ...(existingLlm ?? {}), }; - if (options.llmProvider) llm.provider = options.llmProvider; - if (options.llmModel) llm.model = options.llmModel; - if (options.llmBaseUrl) llm.baseURL = options.llmBaseUrl; + if (options.llmProvider) { + llm.provider = options.llmProvider; + } + if (options.llmModel) { + llm.model = options.llmModel; + } + if (options.llmBaseUrl) { + llm.baseURL = options.llmBaseUrl; + } if (options.llmTasks) { const taskNames = (options.llmTasks as string).split(',').map((t: string) => t.trim()); llm.tasks = { diff --git a/packages/notes/src/core/pipeline.ts b/packages/notes/src/core/pipeline.ts index d4ca5942..adf29022 100644 --- a/packages/notes/src/core/pipeline.ts +++ b/packages/notes/src/core/pipeline.ts @@ -47,9 +47,13 @@ function findCompoundTagSepPos(tag: string): number { // Digit after dash — confirm semver-like by requiring ≥2 dots in the remainder let dots = 0; for (let i = afterDash; i < tag.length; i++) { - if (tag[i] === '.') dots++; + if (tag[i] === '.') { + dots++; + } + } + if (dots >= 2) { + return pos; } - if (dots >= 2) return pos; } pos = tag.lastIndexOf('-', pos - 1); } @@ -59,7 +63,9 @@ function findCompoundTagSepPos(tag: string): number { /** True when position `i` in `tag` begins a semver version: a digit, or a `v` immediately followed by a digit. */ function isVersionStartAt(tag: string, i: number): boolean { const c = tag.charCodeAt(i); - if (c >= 48 && c <= 57) return true; + if (c >= 48 && c <= 57) { + return true; + } if (tag[i] === 'v') { const n = tag.charCodeAt(i + 1); return n >= 48 && n <= 57; @@ -130,7 +136,9 @@ export function buildOrderedCategories( ): EnhancedCategory[] { const order = configCategories?.map((c) => c.name) ?? []; const mapped = rawCategories.map((c) => ({ name: c.category, entries: c.entries })); - if (order.length === 0) return mapped; + if (order.length === 0) { + return mapped; + } return mapped.sort((a, b) => { const ai = order.indexOf(a.name); const bi = order.indexOf(b.name); @@ -506,7 +514,9 @@ export async function runPipeline( } else { writeMarkdown(fileName, contexts, config, dryRun, fmtOpts); } - if (!dryRun) files.push(fileName); + if (!dryRun) { + files.push(fileName); + } } if (mode === 'packages' || mode === 'both') { @@ -545,7 +555,9 @@ export async function runPipeline( return renderTemplate(templatePath, docCtx, releaseNotesConfig.templates.engine as TemplateEngine | undefined) .content; } - if (ctx.enhanced?.releaseNotes) return ctx.enhanced.releaseNotes; + if (ctx.enhanced?.releaseNotes) { + return ctx.enhanced.releaseNotes; + } return formatVersion(ctx, fmtOpts); }; @@ -624,7 +636,9 @@ async function writeMonorepoFiles( const cwd = process.cwd(); const detected = detectMonorepo(cwd); - if (!detected.isMonorepo) return []; + if (!detected.isMonorepo) { + return []; + } const monoFiles = writeMonorepoChangelogs( contexts, diff --git a/packages/notes/src/errors/index.ts b/packages/notes/src/errors/index.ts index 6bee391c..1da789f0 100644 --- a/packages/notes/src/errors/index.ts +++ b/packages/notes/src/errors/index.ts @@ -39,7 +39,9 @@ export class LLMError extends NotesError { constructor(message: string, options?: { retryable?: boolean; cause?: unknown }) { super(message); // Chain the original provider/SDK error so its stack survives (the message only carries its text). - if (options?.cause !== undefined) this.cause = options.cause; + if (options?.cause !== undefined) { + this.cause = options.cause; + } this.retryable = options?.retryable; } } diff --git a/packages/notes/src/input/conventional-changelog.ts b/packages/notes/src/input/conventional-changelog.ts index 9a3fe4f4..e3a4d282 100644 --- a/packages/notes/src/input/conventional-changelog.ts +++ b/packages/notes/src/input/conventional-changelog.ts @@ -65,7 +65,9 @@ function extractIssueIds(text: string): { description: string; issueIds: string[ function parseEntry(line: string, sectionType: ChangelogEntry['type']): ChangelogEntry | null { const bulletMatch = line.match(BULLET); - if (!bulletMatch) return null; + if (!bulletMatch) { + return null; + } let rawText = bulletMatch[1] ?? ''; let scope: string | undefined; @@ -126,7 +128,9 @@ function parseVersionBlocks(content: string): VersionBlock[] { continue; } - if (!currentBlock) continue; + if (!currentBlock) { + continue; + } const sectionMatch = line.match(SECTION_HEADER); if (sectionMatch) { diff --git a/packages/notes/src/input/manual.ts b/packages/notes/src/input/manual.ts index 4c1bfec5..9dd48a13 100644 --- a/packages/notes/src/input/manual.ts +++ b/packages/notes/src/input/manual.ts @@ -20,7 +20,9 @@ export function parseManualInput(json: string): ChangelogInput { if (Array.isArray(input.packages)) { for (const pkg of input.packages) { - if (typeof pkg !== 'object' || pkg === null) continue; + if (typeof pkg !== 'object' || pkg === null) { + continue; + } const p = pkg as Record; @@ -97,7 +99,9 @@ function normalizeType(type: unknown): import('../core/types.js').ChangelogType if (typeof type === 'string') { const mapped = typeMap[type.toLowerCase()]; - if (mapped) return mapped; + if (mapped) { + return mapped; + } } return 'changed'; diff --git a/packages/notes/src/llm/anthropic.ts b/packages/notes/src/llm/anthropic.ts index 17cbd15f..7a1371d2 100644 --- a/packages/notes/src/llm/anthropic.ts +++ b/packages/notes/src/llm/anthropic.ts @@ -101,7 +101,9 @@ export class AnthropicProvider extends BaseLLMProvider { return { content: textBlock.text }; } catch (error) { - if (error instanceof LLMError) throw error; + if (error instanceof LLMError) { + throw error; + } if (signal.aborted) { throw new LLMError(`Anthropic request timed out after ${this.getTimeout(options)}ms`, { retryable: true }); } diff --git a/packages/notes/src/llm/base.ts b/packages/notes/src/llm/base.ts index 4fbecbb8..b260b5ea 100644 --- a/packages/notes/src/llm/base.ts +++ b/packages/notes/src/llm/base.ts @@ -20,7 +20,9 @@ export abstract class BaseLLMProvider implements LLMProvider { // - above the timer ceiling (2^31-1 ms ≈ 24.8 days) → clamp, not throw/overflow. A huge value // means "effectively no timeout", so clamping preserves intent rather than dropping to 60s. const MAX_TIMEOUT_MS = 2_147_483_647; - if (!Number.isFinite(timeout) || timeout < 1) return LLM_DEFAULTS.timeout; + if (!Number.isFinite(timeout) || timeout < 1) { + return LLM_DEFAULTS.timeout; + } return Math.min(Math.trunc(timeout), MAX_TIMEOUT_MS); } diff --git a/packages/notes/src/llm/context/prFetcher.ts b/packages/notes/src/llm/context/prFetcher.ts index 13805fa5..ba1f777e 100644 --- a/packages/notes/src/llm/context/prFetcher.ts +++ b/packages/notes/src/llm/context/prFetcher.ts @@ -29,10 +29,14 @@ function stripDetailsTags(input: string): string { const tagEnd = lower.indexOf('>', i); i = tagEnd === -1 ? input.length : tagEnd + 1; } else if (lower.startsWith('', i)) { - if (depth > 0) depth--; + if (depth > 0) { + depth--; + } i += 10; } else { - if (depth === 0) result += input[i]; + if (depth === 0) { + result += input[i]; + } i++; } } @@ -47,7 +51,9 @@ function sanitiseBody(raw: string): string { } function truncateBody(body: string): string { - if (body.length <= BODY_CAP) return body; + if (body.length <= BODY_CAP) { + return body; + } // Prefer truncating at a heading boundary const headingPos = body.lastIndexOf('\n#', BODY_CAP); diff --git a/packages/notes/src/llm/correctiveRetry.ts b/packages/notes/src/llm/correctiveRetry.ts index be1611b7..1ef65feb 100644 --- a/packages/notes/src/llm/correctiveRetry.ts +++ b/packages/notes/src/llm/correctiveRetry.ts @@ -27,7 +27,9 @@ export async function withCorrectiveRetry( const result = await call(messages, attempt === 0); const validation = validate(result); - if (validation.valid) return validation.value; + if (validation.valid) { + return validation.value; + } lastError = validation.error; diff --git a/packages/notes/src/llm/examples/fetcher.ts b/packages/notes/src/llm/examples/fetcher.ts index 4acc0b03..e5b3cc8f 100644 --- a/packages/notes/src/llm/examples/fetcher.ts +++ b/packages/notes/src/llm/examples/fetcher.ts @@ -42,7 +42,9 @@ function readCache(key: string, latestTag: string, count: number): Example[] | n const entry: CacheEntry = JSON.parse(raw); // Invalidate if the tag changed or the cached count is smaller than requested // (the user may have increased `examples` config since the last run). - if (entry.latestTag !== latestTag || entry.count < count) return null; + if (entry.latestTag !== latestTag || entry.count < count) { + return null; + } return entry.examples; } catch { return null; @@ -70,7 +72,9 @@ function matchesBareVersion(tagName: string): boolean { export async function fetchExamples(options: FetchExamplesOptions): Promise { const { owner, repo, packageName, count, githubToken, isMonorepo } = options; - if (count === 0) return []; + if (count === 0) { + return []; + } const token = githubToken ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN; if (!token) { @@ -104,13 +108,19 @@ export async function fetchExamples(options: FetchExamplesOptions): Promise 0) writeCache(key, latestTag, count, examples); + if (examples.length > 0) { + writeCache(key, latestTag, count, examples); + } debug(`Fetched ${examples.length} examples for ${packageName}`); return examples; } catch (error) { diff --git a/packages/notes/src/llm/examples/parser.ts b/packages/notes/src/llm/examples/parser.ts index e9d55313..28e5f232 100644 --- a/packages/notes/src/llm/examples/parser.ts +++ b/packages/notes/src/llm/examples/parser.ts @@ -11,7 +11,9 @@ export function parseReleaseBodyToExample(markdown: string, version: string): Ex for (const raw of lines) { const line = raw.trim(); - if (!line) continue; + if (!line) { + continue; + } // ## / ### / #### Heading → new category (h1 skipped as document title) const headingMatch = /^#{2,4} (.+)/.exec(line); @@ -19,7 +21,9 @@ export function parseReleaseBodyToExample(markdown: string, version: string): Ex currentCategory = headingMatch[1]!.trim(); continue; } - if (line.startsWith('#')) continue; + if (line.startsWith('#')) { + continue; + } // Bullet entry if (BULLET_RE.test(line)) { @@ -29,18 +33,26 @@ export function parseReleaseBodyToExample(markdown: string, version: string): Ex description: leadInMatch ? text.slice(leadInMatch[0].length) : text, category: currentCategory, }; - if (leadInMatch) entry.leadIn = leadInMatch[1]; - if (/\*\*BREAKING\*\*/i.test(text)) entry.breaking = true; + if (leadInMatch) { + entry.leadIn = leadInMatch[1]; + } + if (/\*\*BREAKING\*\*/i.test(text)) { + entry.breaking = true; + } entries.push(entry); } } - if (entries.length === 0) return null; + if (entries.length === 0) { + return null; + } return { version, entries }; } export function renderExamplesBlock(examples: Example[]): string { - if (examples.length === 0) return ''; + if (examples.length === 0) { + return ''; + } // These examples are built from past release bodies and re-fed as few-shot input, and release-notes // output becomes a future example — a self-poisoning loop. Escape every interpolated field so an diff --git a/packages/notes/src/llm/messages.ts b/packages/notes/src/llm/messages.ts index b3c14e10..84d0a829 100644 --- a/packages/notes/src/llm/messages.ts +++ b/packages/notes/src/llm/messages.ts @@ -11,7 +11,9 @@ export interface CompleteResult { // Opt-in via RELEASEKIT_DEBUG=1. Output may contain fetched PR bodies and // example release notes from external repos — don't enable in shared CI logs. export function debugLogMessages(providerName: string, messages: LLMMessage[]): void { - if (process.env.RELEASEKIT_DEBUG !== '1') return; + if (process.env.RELEASEKIT_DEBUG !== '1') { + return; + } const lines = [`[RELEASEKIT_DEBUG] ${providerName} messages (${messages.length}):`]; for (const msg of messages) { const preview = msg.content.length > 300 ? `${msg.content.slice(0, 300)}…` : msg.content; diff --git a/packages/notes/src/llm/ollama.ts b/packages/notes/src/llm/ollama.ts index 06a76258..809be773 100644 --- a/packages/notes/src/llm/ollama.ts +++ b/packages/notes/src/llm/ollama.ts @@ -129,7 +129,9 @@ export class OllamaProvider extends BaseLLMProvider { return { content }; } catch (error) { - if (error instanceof LLMError) throw error; + if (error instanceof LLMError) { + throw error; + } if (signal.aborted) { throw new LLMError(`Ollama request timed out after ${this.getTimeout(options)}ms`, { retryable: true }); } diff --git a/packages/notes/src/llm/openai-compatible.ts b/packages/notes/src/llm/openai-compatible.ts index faae2965..b3a18ea4 100644 --- a/packages/notes/src/llm/openai-compatible.ts +++ b/packages/notes/src/llm/openai-compatible.ts @@ -63,7 +63,9 @@ export class OpenAICompatibleProvider extends BaseLLMProvider { return { content }; } catch (error) { - if (error instanceof LLMError) throw error; + if (error instanceof LLMError) { + throw error; + } if (signal.aborted) { throw new LLMError(`${this.name} request timed out after ${this.getTimeout(options)}ms`, { retryable: true }); } diff --git a/packages/notes/src/llm/openai.ts b/packages/notes/src/llm/openai.ts index aab8b556..264bcbe6 100644 --- a/packages/notes/src/llm/openai.ts +++ b/packages/notes/src/llm/openai.ts @@ -91,7 +91,9 @@ export class OpenAIProvider extends BaseLLMProvider { return { content }; } catch (error) { - if (error instanceof LLMError) throw error; + if (error instanceof LLMError) { + throw error; + } if (signal.aborted) { throw new LLMError(`OpenAI request timed out after ${this.getTimeout(options)}ms`, { retryable: true }); } diff --git a/packages/notes/src/llm/retryable.ts b/packages/notes/src/llm/retryable.ts index 9d2f555d..e983bbfd 100644 --- a/packages/notes/src/llm/retryable.ts +++ b/packages/notes/src/llm/retryable.ts @@ -19,9 +19,15 @@ const NETWORK_ERROR_CODES = new Set([ * auth, permission, validation, or not-found error that retrying can't fix — fail fast. */ export function isRetryableStatus(status: number): boolean { - if (status === 408 || status === 409 || status === 425 || status === 429) return true; - if (status >= 500) return true; - if (status >= 400) return false; + if (status === 408 || status === 409 || status === 425 || status === 429) { + return true; + } + if (status >= 500) { + return true; + } + if (status >= 400) { + return false; + } // Non-error status (shouldn't reach here for a thrown error) — retry rather than silently drop. return true; } @@ -33,10 +39,16 @@ export function isRetryableStatus(status: number): boolean { * fail-fast path is an explicit non-retryable 4xx status. */ export function isRetryableProviderError(error: unknown): boolean { - if (!error || typeof error !== 'object') return true; + if (!error || typeof error !== 'object') { + return true; + } const e = error as { status?: unknown; code?: unknown; name?: unknown }; - if (typeof e.status === 'number') return isRetryableStatus(e.status); - if (typeof e.code === 'string' && NETWORK_ERROR_CODES.has(e.code)) return true; + if (typeof e.status === 'number') { + return isRetryableStatus(e.status); + } + if (typeof e.code === 'string' && NETWORK_ERROR_CODES.has(e.code)) { + return true; + } // Everything else — abort/timeout error names (AbortError, TimeoutError, APIConnectionError, // APIConnectionTimeoutError) and unknown shapes alike — defaults to retryable; the only fail-fast // path is the explicit non-retryable 4xx status handled above. @@ -48,6 +60,8 @@ export function isRetryableProviderError(error: unknown): boolean { * any other error (unclassified) is retried by default. */ export function isRetryableLLMError(error: unknown): boolean { - if (error instanceof LLMError) return error.retryable ?? true; + if (error instanceof LLMError) { + return error.retryable ?? true; + } return true; } diff --git a/packages/notes/src/llm/scopes.ts b/packages/notes/src/llm/scopes.ts index ceab39ac..4d3e0772 100644 --- a/packages/notes/src/llm/scopes.ts +++ b/packages/notes/src/llm/scopes.ts @@ -54,9 +54,15 @@ export function resolveAllowedScopes( categories?: LLMCategory[], packageNames?: string[], ): string[] | null { - if (!scopeConfig || scopeConfig.mode === 'unrestricted') return null; - if (scopeConfig.mode === 'none') return []; - if (scopeConfig.mode === 'packages') return expandPackageScopes(packageNames ?? []); + if (!scopeConfig || scopeConfig.mode === 'unrestricted') { + return null; + } + if (scopeConfig.mode === 'none') { + return []; + } + if (scopeConfig.mode === 'packages') { + return expandPackageScopes(packageNames ?? []); + } if (scopeConfig.mode === 'restricted') { const explicit = scopeConfig.rules?.allowed ?? []; @@ -65,7 +71,9 @@ export function resolveAllowedScopes( if (categories) { const fromCategories = getAllowedScopesFromCategories(categories); for (const scopes of fromCategories.values()) { - for (const s of scopes) all.add(s); + for (const s of scopes) { + all.add(s); + } } } @@ -84,8 +92,12 @@ export function validateScope( allowedScopes: string[] | null, caseSensitive = false, ): string | undefined { - if (!scope || allowedScopes === null) return scope; - if (allowedScopes.length === 0) return undefined; + if (!scope || allowedScopes === null) { + return scope; + } + if (allowedScopes.length === 0) { + return undefined; + } const normalise = (s: string) => (caseSensitive ? s : s.toLowerCase()); const isAllowed = allowedScopes.some((a) => normalise(a) === normalise(scope)); @@ -107,7 +119,9 @@ export function validateEntryScopes( packageNames?: string[], ): ScopeValidationResult { const allowedScopes = resolveAllowedScopes(scopeConfig, categories, packageNames); - if (allowedScopes === null) return { valid: true, entries, errors: [] }; + if (allowedScopes === null) { + return { valid: true, entries, errors: [] }; + } const caseSensitive = scopeConfig?.rules?.caseSensitive ?? false; const action = scopeConfig?.rules?.invalidScopeAction ?? 'remove'; diff --git a/packages/notes/src/llm/tasks/categorize.ts b/packages/notes/src/llm/tasks/categorize.ts index ab522319..00776b26 100644 --- a/packages/notes/src/llm/tasks/categorize.ts +++ b/packages/notes/src/llm/tasks/categorize.ts @@ -55,7 +55,9 @@ export function createCategorizeValidator( return (result) => { const parsed = parseLLMResult(result); - if (!parsed.ok) return { valid: false, error: parsed.error }; + if (!parsed.ok) { + return { valid: false, error: parsed.error }; + } const zodResult = CategorizeOutputSchema.safeParse(parsed.data); if (!zodResult.success) { @@ -74,7 +76,9 @@ export function createCategorizeValidator( zodResult.data.entries, context.categories?.map((c) => c.name), ); - if (categoryError) return { valid: false, error: categoryError }; + if (categoryError) { + return { valid: false, error: categoryError }; + } // Apply scopes from LLM response const withScopes: ChangelogEntry[] = cleanEntries.map((entry, i) => { diff --git a/packages/notes/src/llm/tasks/enhance-and-categorize.ts b/packages/notes/src/llm/tasks/enhance-and-categorize.ts index 34abbcba..5e8b0374 100644 --- a/packages/notes/src/llm/tasks/enhance-and-categorize.ts +++ b/packages/notes/src/llm/tasks/enhance-and-categorize.ts @@ -74,7 +74,9 @@ export function createEnhanceAndCategorizeValidator( return (result) => { // Parse JSON (structured output or text fallback) const parsed = parseLLMResult(result); - if (!parsed.ok) return { valid: false, error: parsed.error }; + if (!parsed.ok) { + return { valid: false, error: parsed.error }; + } // Validate structure const zodResult = EnhanceAndCategorizeOutputSchema.safeParse(parsed.data); @@ -99,12 +101,16 @@ export function createEnhanceAndCategorizeValidator( zodResult.data.entries, context.categories?.map((c) => c.name), ); - if (categoryError) return { valid: false, error: categoryError }; + if (categoryError) { + return { valid: false, error: categoryError }; + } // Build enhanced entries const enhancedEntries: ChangelogEntry[] = entries.map((original, i) => { const llmEntry = zodResult.data.entries[i]; - if (!llmEntry) return original; + if (!llmEntry) { + return original; + } return { ...original, description: llmEntry.description, @@ -143,8 +149,11 @@ export function createEnhanceAndCategorizeValidator( function mergeCategories(target: CategorizedEntries[], incoming: CategorizedEntries[]): void { for (const cat of incoming) { const existing = target.find((c) => c.category === cat.category); - if (existing) existing.entries.push(...cat.entries); - else target.push({ category: cat.category, entries: [...cat.entries] }); + if (existing) { + existing.entries.push(...cat.entries); + } else { + target.push({ category: cat.category, entries: [...cat.entries] }); + } } } diff --git a/packages/notes/src/llm/tasks/shared.ts b/packages/notes/src/llm/tasks/shared.ts index 3957fecc..94a0b17c 100644 --- a/packages/notes/src/llm/tasks/shared.ts +++ b/packages/notes/src/llm/tasks/shared.ts @@ -15,7 +15,9 @@ export function escBody(s: string): string { } export function renderPRBlocks(entry: ChangelogEntry): string { - if (!entry.context?.prs.length) return ''; + if (!entry.context?.prs.length) { + return ''; + } return entry.context.prs .map( (pr) => `${pr.body ? `\n${escBody(pr.body)}\n` : ''}`, @@ -39,8 +41,12 @@ export const INSTRUCTION_HIERARCHY = */ export function renderEntry(entry: ChangelogEntry, index: number): string { const attrs = [`index="${index}"`, `type="${escAttr(entry.type)}"`]; - if (entry.scope) attrs.push(`scope="${escAttr(entry.scope)}"`); - if (entry.breaking) attrs.push('breaking="true"'); + if (entry.scope) { + attrs.push(`scope="${escAttr(entry.scope)}"`); + } + if (entry.breaking) { + attrs.push('breaking="true"'); + } const prBlocks = renderPRBlocks(entry); const body = `${escBody(entry.description)}${prBlocks ? `\n${prBlocks}` : ''}`; return `\n${body}\n`; @@ -63,8 +69,12 @@ export function groupByCategory( for (let i = 0; i < llmEntries.length; i++) { const category = llmEntries[i]?.category ?? 'Unknown'; const entry = entries[i]; - if (!entry) continue; - if (!categoryMap.has(category)) categoryMap.set(category, []); + if (!entry) { + continue; + } + if (!categoryMap.has(category)) { + categoryMap.set(category, []); + } categoryMap.get(category)!.push(entry); } const result: CategorizedEntries[] = []; @@ -80,7 +90,9 @@ export function groupByCategory( * it returns the task-specific free-form `fallback` instruction. */ export function buildCategorySection(categories: LLMCategory[] | undefined, fallback: string): string { - if (!categories || categories.length === 0) return fallback; + if (!categories || categories.length === 0) { + return fallback; + } const lines = categories.map((c) => { const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(', ')}.` : ''; return `- "${c.name}": ${c.description}${scopeInfo}`; @@ -94,7 +106,9 @@ export function buildCategorySection(categories: LLMCategory[] | undefined, fall * The scope-source resolution stays in each task — only the final rendering is shared. */ export function renderScopeInstruction(pairs: Array<{ name: string; scopes: string[] }>, entrySuffix: string): string { - if (pairs.length === 0) return ''; + if (pairs.length === 0) { + return ''; + } const parts = pairs.map((p) => `For "${p.name}"${entrySuffix}, use a scope from: ${p.scopes.join(', ')}.`); return `\n${parts.join('\n')}\nOnly use scopes from these predefined lists. Set scope to null if no scope applies.`; } @@ -136,9 +150,13 @@ export function checkCategoryNames( llmEntries: Array<{ category: string }>, categoryNames: string[] | undefined, ): string | null { - if (!categoryNames?.length) return null; + if (!categoryNames?.length) { + return null; + } const invalid = llmEntries.filter((e) => !categoryNames.includes(e.category)).map((e) => e.category); - if (invalid.length === 0) return null; + if (invalid.length === 0) { + return null; + } const unique = [...new Set(invalid)]; return `Unknown categories: ${unique.join(', ')}. Valid categories: ${categoryNames.join(', ')}`; } diff --git a/packages/notes/src/monorepo/aggregator.ts b/packages/notes/src/monorepo/aggregator.ts index e54cf5c0..bfe1cc06 100644 --- a/packages/notes/src/monorepo/aggregator.ts +++ b/packages/notes/src/monorepo/aggregator.ts @@ -123,7 +123,9 @@ function buildPackageDirMap(rootPath: string, packagesPath: string): Map(); for (const entry of entries) { - if (entry.scope) scopeCounts.set(entry.scope, (scopeCounts.get(entry.scope) ?? 0) + 1); + if (entry.scope) { + scopeCounts.set(entry.scope, (scopeCounts.get(entry.scope) ?? 0) + 1); + } } const groupedScopes = new Set(); for (const [scope, count] of scopeCounts) { - if (count > 1) groupedScopes.add(scope); + if (count > 1) { + groupedScopes.add(scope); + } } if (groupedScopes.size === 0) { @@ -157,7 +163,9 @@ function formatCategorySection(name: string, entries: ChangelogEntry[], refOpts: function rerouteBreakingEntries(categories: EnhancedCategory[]): EnhancedCategory[] { const hasBreaking = categories.some((c) => c.entries.some((e) => e.breaking)); - if (!hasBreaking) return categories; + if (!hasBreaking) { + return categories; + } const breakingEntries: ChangelogEntry[] = []; const stripped = categories.map((c) => ({ @@ -180,7 +188,9 @@ function rerouteBreakingEntries(categories: EnhancedCategory[]): EnhancedCategor } function applyOrder(categories: EnhancedCategory[], order: string[]): EnhancedCategory[] { - if (order.length === 0) return categories; + if (order.length === 0) { + return categories; + } return [...categories].sort((a, b) => { const ai = order.indexOf(a.name); const bi = order.indexOf(b.name); @@ -199,18 +209,26 @@ function parseMdLink(text: string): { label: string; url: string } | null { let search = 0; while (search < text.length) { const bracketOpen = text.indexOf('[', search); - if (bracketOpen === -1) return null; + if (bracketOpen === -1) { + return null; + } const bracketClose = text.indexOf(']', bracketOpen + 1); - if (bracketClose === -1) return null; + if (bracketClose === -1) { + return null; + } if (text[bracketClose + 1] !== '(') { search = bracketClose + 1; continue; } const parenClose = text.indexOf(')', bracketClose + 2); - if (parenClose === -1) return null; + if (parenClose === -1) { + return null; + } const label = text.slice(bracketOpen + 1, bracketClose); const url = text.slice(bracketClose + 2, parenClose); - if (/^https?:\/\//.test(url)) return { label, url }; + if (/^https?:\/\//.test(url)) { + return { label, url }; + } search = parenClose + 1; } return null; @@ -223,7 +241,9 @@ function extractLinksFromPRBodies(entries: ChangelogEntry[], marker: string): Li for (const pr of entry.context?.prs ?? []) { for (const line of pr.body.split('\n')) { const trimmed = line.trim(); - if (!trimmed.startsWith(marker)) continue; + if (!trimmed.startsWith(marker)) { + continue; + } const rest = trimmed.slice(marker.length).trim(); const mdLink = parseMdLink(rest); if (mdLink) { @@ -245,7 +265,9 @@ function extractLinksFromPRBodies(entries: ChangelogEntry[], marker: string): Li } function resolveLinks(entries: ChangelogEntry[], linksConfig: LinksConfig | undefined): LinkItem[] { - if (!linksConfig) return []; + if (!linksConfig) { + return []; + } const items = linksConfig.items ?? []; const discovered = linksConfig.fromPRBodyMarker ? extractLinksFromPRBodies(entries, linksConfig.fromPRBodyMarker) @@ -308,7 +330,9 @@ export function formatVersion(context: TemplateContext, options?: FormatVersionO categories = applyOrder(categories, options?.categoryOrder ?? []); for (const category of categories) { - if (category.entries.length === 0) continue; + if (category.entries.length === 0) { + continue; + } lines.push(...formatCategorySection(category.name, category.entries, refOpts)); } @@ -325,7 +349,9 @@ export function formatVersion(context: TemplateContext, options?: FormatVersionO const grouped = groupEntriesByType(context.entries); for (const [type, entries] of grouped) { - if (entries.length === 0) continue; + if (entries.length === 0) { + continue; + } lines.push(`### ${TYPE_LABELS[type]}`); for (const entry of entries) { diff --git a/packages/notes/src/templates/loader.ts b/packages/notes/src/templates/loader.ts index 5eb72d94..e347f570 100644 --- a/packages/notes/src/templates/loader.ts +++ b/packages/notes/src/templates/loader.ts @@ -154,9 +154,15 @@ export function renderComposable( } function detectEngineFromFiles(_dir: string, files: string[]): TemplateEngine | null { - if (files.some((f) => f.endsWith('.liquid'))) return 'liquid'; - if (files.some((f) => f.endsWith('.hbs') || f.endsWith('.handlebars'))) return 'handlebars'; - if (files.some((f) => f.endsWith('.ejs'))) return 'ejs'; + if (files.some((f) => f.endsWith('.liquid'))) { + return 'liquid'; + } + if (files.some((f) => f.endsWith('.hbs') || f.endsWith('.handlebars'))) { + return 'handlebars'; + } + if (files.some((f) => f.endsWith('.ejs'))) { + return 'ejs'; + } return null; } diff --git a/packages/notes/src/utils/concurrency.ts b/packages/notes/src/utils/concurrency.ts index d6e476c6..315277d0 100644 --- a/packages/notes/src/utils/concurrency.ts +++ b/packages/notes/src/utils/concurrency.ts @@ -18,7 +18,9 @@ export async function mapWithConcurrency( // fresh work after the first failure (in-flight calls still settle; Promise.all surfaces the error). while (!failed) { const i = next++; - if (i >= items.length) return; + if (i >= items.length) { + return; + } try { results[i] = await fn(items[i] as T, i); } catch (err) { diff --git a/packages/notes/src/utils/json.ts b/packages/notes/src/utils/json.ts index e97ce113..0b460a95 100644 --- a/packages/notes/src/utils/json.ts +++ b/packages/notes/src/utils/json.ts @@ -17,10 +17,14 @@ export function extractJsonFromResponse(response: string): string { // which in practice means: first delimiter to last matching delimiter, ignoring any // preamble or postamble text the model may have added. const objectMatch = stripped.match(/\{[\s\S]*\}/); - if (objectMatch) return objectMatch[0]; + if (objectMatch) { + return objectMatch[0]; + } const arrayMatch = stripped.match(/\[[\s\S]*\]/); - if (arrayMatch) return arrayMatch[0]; + if (arrayMatch) { + return arrayMatch[0]; + } // No JSON delimiters found — return stripped response so JSON.parse produces a clear error return stripped; diff --git a/packages/notes/src/utils/retry.ts b/packages/notes/src/utils/retry.ts index 69377052..af0c9a6f 100644 --- a/packages/notes/src/utils/retry.ts +++ b/packages/notes/src/utils/retry.ts @@ -28,7 +28,9 @@ export async function withRetry(fn: () => Promise, options: RetryOptions = return await fn(); } catch (error) { lastError = error; - if (options.shouldRetry && !options.shouldRetry(error)) break; + if (options.shouldRetry && !options.shouldRetry(error)) { + break; + } if (attempt < maxAttempts - 1) { const base = Math.min(initialDelay * backoffFactor ** attempt, maxDelay); const jitter = base * 0.2 * (Math.random() * 2 - 1); diff --git a/packages/notes/test/eval/assertions.ts b/packages/notes/test/eval/assertions.ts index d942220d..dd9a17d0 100644 --- a/packages/notes/test/eval/assertions.ts +++ b/packages/notes/test/eval/assertions.ts @@ -18,8 +18,12 @@ export function findDuplicateDependencyChurn(text: string): string[] { /** Guard against a truncated (empty/near-empty) or runaway generation. */ export function checkLengthBounds(text: string, min: number, max: number): string[] { const n = text.trim().length; - if (n < min) return [`output too short: ${n} < ${min} chars`]; - if (n > max) return [`output too long: ${n} > ${max} chars`]; + if (n < min) { + return [`output too short: ${n} < ${min} chars`]; + } + if (n > max) { + return [`output too long: ${n} > ${max} chars`]; + } return []; } @@ -44,7 +48,9 @@ export function listItemLines(text: string): string[] { */ export function checkPastTenseLeaning(text: string, minRatio = 0.6): string[] { const items = listItemLines(text); - if (items.length === 0) return []; + if (items.length === 0) { + return []; + } const firstWords = items.map((line) => line.replace(/^\s*(?:[-*]|\d+\.)\s+/, '').split(/\s+/)[0] ?? ''); const pastCount = firstWords.filter(looksPastTense).length; const ratio = pastCount / items.length; diff --git a/packages/notes/test/unit/llm.spec.ts b/packages/notes/test/unit/llm.spec.ts index 0ab730c3..fe0beda8 100644 --- a/packages/notes/test/unit/llm.spec.ts +++ b/packages/notes/test/unit/llm.spec.ts @@ -43,7 +43,9 @@ function makeFailingProvider(after = 0): LLMProvider { capabilities: { systemRole: true, structuredOutputs: false, toolUse: false }, async complete(): Promise { calls++; - if (calls > after) throw new Error('provider error'); + if (calls > after) { + throw new Error('provider error'); + } return { content: 'ok' }; }, }; @@ -65,7 +67,9 @@ describe('enhanceEntry()', () => { it('should return the provider response as the new description', async () => { const provider = makeMockProvider('Add real-time streaming to the API'); const entry = sampleEntries[0]; - if (!entry) throw new Error('No sample entry'); + if (!entry) { + throw new Error('No sample entry'); + } const result = await enhanceEntry(provider, entry, llmContext); expect(result).toBe('Add real-time streaming to the API'); }); @@ -73,7 +77,9 @@ describe('enhanceEntry()', () => { it('should trim whitespace from the response', async () => { const provider = makeMockProvider(' trimmed '); const entry = sampleEntries[0]; - if (!entry) throw new Error('No sample entry'); + if (!entry) { + throw new Error('No sample entry'); + } const result = await enhanceEntry(provider, entry, llmContext); expect(result).toBe('trimmed'); }); @@ -81,7 +87,9 @@ describe('enhanceEntry()', () => { it('should call the provider exactly once', async () => { const provider = makeMockProvider('response'); const entry = sampleEntries[0]; - if (!entry) throw new Error('No sample entry'); + if (!entry) { + throw new Error('No sample entry'); + } await enhanceEntry(provider, entry, llmContext); expect(provider.callCount).toBe(1); }); @@ -136,7 +144,9 @@ describe('enhanceEntries()', () => { capabilities: { systemRole: true, structuredOutputs: false, toolUse: false }, async complete(): Promise { calls++; - if (calls === 2) throw new Error('second entry fails'); + if (calls === 2) { + throw new Error('second entry fails'); + } return { content: 'ok' }; }, }; diff --git a/packages/notes/test/unit/llm/cache.spec.ts b/packages/notes/test/unit/llm/cache.spec.ts index 90d8a9f7..0642e395 100644 --- a/packages/notes/test/unit/llm/cache.spec.ts +++ b/packages/notes/test/unit/llm/cache.spec.ts @@ -29,7 +29,9 @@ describe('withContentHashCache', () => { }; afterEach(() => { - for (const d of dirs) fs.rmSync(d, { recursive: true, force: true }); + for (const d of dirs) { + fs.rmSync(d, { recursive: true, force: true }); + } dirs.length = 0; }); diff --git a/packages/notes/test/unit/llm/tasks.spec.ts b/packages/notes/test/unit/llm/tasks.spec.ts index 76075288..9de793e2 100644 --- a/packages/notes/test/unit/llm/tasks.spec.ts +++ b/packages/notes/test/unit/llm/tasks.spec.ts @@ -45,7 +45,9 @@ describe('createCategorizeValidator()', () => { ); expect(result.valid).toBe(true); - if (!result.valid) return; + if (!result.valid) { + return; + } expect(result.value).toHaveLength(2); expect(result.value.find((c) => c.category === 'New Features')?.entries).toHaveLength(1); expect(result.value.find((c) => c.category === 'Bug Fixes')?.entries).toHaveLength(2); @@ -56,7 +58,9 @@ describe('createCategorizeValidator()', () => { const result = validate({ content: 'not json', structured: undefined }); expect(result.valid).toBe(false); - if (result.valid) return; + if (result.valid) { + return; + } expect(result.error).toContain('Invalid JSON'); }); @@ -65,7 +69,9 @@ describe('createCategorizeValidator()', () => { const result = validate(structured({ entries: [{ category: 'New', scope: null }] })); expect(result.valid).toBe(false); - if (result.valid) return; + if (result.valid) { + return; + } expect(result.error).toBe('Expected 3 entries, got 1'); }); @@ -88,7 +94,9 @@ describe('createCategorizeValidator()', () => { ); expect(result.valid).toBe(false); - if (result.valid) return; + if (result.valid) { + return; + } expect(result.error).toBe('Unknown categories: Mystery. Valid categories: New Features, Bug Fixes'); }); @@ -113,7 +121,9 @@ describe('createCategorizeValidator()', () => { ); expect(result.valid).toBe(true); - if (!result.valid) return; + if (!result.valid) { + return; + } const dev = result.value.find((c) => c.category === 'Developer'); expect(dev?.entries[0]?.scope).toBe('Dependencies'); expect(dev?.entries[1]?.scope).toBeUndefined(); @@ -148,7 +158,9 @@ describe('createEnhanceAndCategorizeValidator()', () => { ); expect(result.valid).toBe(true); - if (!result.valid) return; + if (!result.valid) { + return; + } expect(result.value.enhancedEntries).toHaveLength(3); expect(result.value.enhancedEntries[0]?.description).toBe('Added real-time streaming to the API'); expect(result.value.enhancedEntries[2]?.scope).toBe('Code Quality'); @@ -160,7 +172,9 @@ describe('createEnhanceAndCategorizeValidator()', () => { const result = validate({ content: 'not json', structured: undefined }); expect(result.valid).toBe(false); - if (result.valid) return; + if (result.valid) { + return; + } expect(result.error).toContain('Invalid JSON'); }); @@ -169,7 +183,9 @@ describe('createEnhanceAndCategorizeValidator()', () => { const result = validate(structured({ entries: [fullEntry('Only first', 'New')] })); expect(result.valid).toBe(false); - if (result.valid) return; + if (result.valid) { + return; + } expect(result.error).toBe('Expected 3 entries, got 1 (entries missing — cannot proceed)'); }); @@ -188,7 +204,9 @@ describe('createEnhanceAndCategorizeValidator()', () => { ); expect(result.valid).toBe(true); - if (!result.valid) return; + if (!result.valid) { + return; + } expect(result.value.enhancedEntries).toHaveLength(3); expect(result.value.enhancedEntries[2]?.description).toBe('Enhanced C'); }); @@ -209,7 +227,9 @@ describe('createEnhanceAndCategorizeValidator()', () => { ); expect(result.valid).toBe(false); - if (result.valid) return; + if (result.valid) { + return; + } expect(result.error).toBe('Unknown categories: Mystery. Valid categories: New, Fixed, Changed'); }); }); diff --git a/packages/notes/test/unit/retry.spec.ts b/packages/notes/test/unit/retry.spec.ts index de1df3f3..c31e008c 100644 --- a/packages/notes/test/unit/retry.spec.ts +++ b/packages/notes/test/unit/retry.spec.ts @@ -24,7 +24,9 @@ describe('withRetry()', () => { const result = await withRetry( async () => { calls++; - if (calls < 2) throw new Error('temporary error'); + if (calls < 2) { + throw new Error('temporary error'); + } return 'ok'; }, { maxAttempts: 3, initialDelay: 0 }, @@ -106,7 +108,9 @@ describe('withRetry()', () => { const result = await withRetry( async () => { calls++; - if (calls < 3) throw new Error('not yet'); + if (calls < 3) { + throw new Error('not yet'); + } return 'done'; }, { maxAttempts: 3, initialDelay: 0 }, diff --git a/packages/notes/test/unit/templates.spec.ts b/packages/notes/test/unit/templates.spec.ts index 034e2609..f23903ef 100644 --- a/packages/notes/test/unit/templates.spec.ts +++ b/packages/notes/test/unit/templates.spec.ts @@ -69,7 +69,9 @@ describe('Handlebars Engine', () => { registerHandlebarsHelpers(); const template = '{{capitalize type}}'; const entry = sampleContext.entries[0]; - if (!entry) throw new Error('No entry found'); + if (!entry) { + throw new Error('No entry found'); + } const result = renderHandlebars(template, { ...sampleContext, ...entry } as unknown as TemplateContext); expect(result).toBe('Added'); }); diff --git a/packages/notes/test/unit/utils/concurrency.spec.ts b/packages/notes/test/unit/utils/concurrency.spec.ts index 623c2e14..8fef7694 100644 --- a/packages/notes/test/unit/utils/concurrency.spec.ts +++ b/packages/notes/test/unit/utils/concurrency.spec.ts @@ -51,7 +51,9 @@ describe('mapWithConcurrency()', () => { await expect( mapWithConcurrency([0, 1, 2], 1, async (n) => { seen.push(n); - if (n === 1) throw new Error('boom'); + if (n === 1) { + throw new Error('boom'); + } return n; }), ).rejects.toThrow('boom'); diff --git a/packages/publish/src/command.ts b/packages/publish/src/command.ts index 3b3e1f2c..1d2dbca8 100644 --- a/packages/publish/src/command.ts +++ b/packages/publish/src/command.ts @@ -21,8 +21,12 @@ export function createPublishCommand(): Command { .option('--json', 'Output results as JSON', false) .option('--verbose', 'Verbose logging', false) .action(async (options) => { - if (options.verbose) setLogLevel('debug'); - if (options.json) setJsonMode(true); + if (options.verbose) { + setLogLevel('debug'); + } + if (options.json) { + setJsonMode(true); + } try { const config = loadConfig({ configPath: options.config }); diff --git a/packages/publish/src/registry/cargo.ts b/packages/publish/src/registry/cargo.ts index ecd39a1b..6cefe8a2 100644 --- a/packages/publish/src/registry/cargo.ts +++ b/packages/publish/src/registry/cargo.ts @@ -28,8 +28,12 @@ async function isCratePublished(name: string, version: string): Promise signal: AbortSignal.timeout(CRATES_IO_API_TIMEOUT_MS), headers: { 'User-Agent': CRATES_IO_USER_AGENT }, }); - if (response.status === 200) return true; - if (response.status === 404) return false; + if (response.status === 200) { + return true; + } + if (response.status === 404) { + return false; + } debug(`crates.io published-check returned ${response.status} for ${name}@${version}, will attempt publish`); return false; } catch (error) { @@ -94,8 +98,12 @@ export const cargoRegistry: Registry = { async publish(target: CargoTarget, ctx: PipelineContext, session: CargoSession): Promise { const publishArgs = ['publish', '--manifest-path', target.manifestPath]; - if (ctx.config.cargo.noVerify) publishArgs.push('--no-verify'); - if (session.isDirty) publishArgs.push('--allow-dirty'); + if (ctx.config.cargo.noVerify) { + publishArgs.push('--no-verify'); + } + if (session.isDirty) { + publishArgs.push('--allow-dirty'); + } await execCommand('cargo', publishArgs, { cwd: ctx.cwd, diff --git a/packages/publish/src/registry/dispatcher.ts b/packages/publish/src/registry/dispatcher.ts index 700e4bff..5f63cdbb 100644 --- a/packages/publish/src/registry/dispatcher.ts +++ b/packages/publish/src/registry/dispatcher.ts @@ -57,7 +57,9 @@ export async function runPublishStage( result.skipped = true; result.success = true; result.reason = skip.reason; - if (skip.alreadyPublished) result.alreadyPublished = true; + if (skip.alreadyPublished) { + result.alreadyPublished = true; + } results.push(result); continue; } @@ -87,7 +89,9 @@ export async function runPublishStage( }); result.success = true; - if (!dryRun) success(`Published ${target.packageName}@${target.version} to ${registry.displayName}`); + if (!dryRun) { + success(`Published ${target.packageName}@${target.version} to ${registry.displayName}`); + } results.push(result); } catch (error) { // A surfaced already-published conflict (a publish that landed but whose response was lost, diff --git a/packages/publish/src/registry/npm.ts b/packages/publish/src/registry/npm.ts index b91e5d7f..f623a83a 100644 --- a/packages/publish/src/registry/npm.ts +++ b/packages/publish/src/registry/npm.ts @@ -46,7 +46,9 @@ export function orderNpmUpdates u.filePath.endsWith('package.json')); - if (npm.length <= 1) return updates; + if (npm.length <= 1) { + return updates; + } const other = updates.filter((u) => !u.filePath.endsWith('package.json')); const byName = new Map(npm.map((u) => [u.packageName, u])); @@ -54,8 +56,14 @@ export function orderNpmUpdates 0) { const remaining = new Set(npm.map((u) => u.packageName)); orderedNames = explicitOrder.filter((n) => remaining.has(n)); - for (const n of orderedNames) remaining.delete(n); - for (const u of npm) if (remaining.has(u.packageName)) orderedNames.push(u.packageName); + for (const n of orderedNames) { + remaining.delete(n); + } + for (const u of npm) { + if (remaining.has(u.packageName)) { + orderedNames.push(u.packageName); + } + } } else { const graphPackages: GraphPackage[] = npm.map((u) => { const pkgJsonPath = path.resolve(cwd, u.filePath); @@ -190,8 +198,12 @@ export const npmRegistry: Registry = { }); debug('Publish command completed successfully'); - if (publishResult.stdout) debug(`Publish stdout: ${publishResult.stdout}`); - if (publishResult.stderr) debug(`Publish stderr: ${publishResult.stderr}`); + if (publishResult.stdout) { + debug(`Publish stdout: ${publishResult.stdout}`); + } + if (publishResult.stderr) { + debug(`Publish stderr: ${publishResult.stderr}`); + } }, dispose(session: NpmSession): void { diff --git a/packages/publish/src/registry/pub.ts b/packages/publish/src/registry/pub.ts index 2511cde9..0f711a21 100644 --- a/packages/publish/src/registry/pub.ts +++ b/packages/publish/src/registry/pub.ts @@ -71,7 +71,9 @@ export const pubRegistry: Registry = { async isPublished(target: PubTarget): Promise { // The pub.dev REST API only makes sense for pub.dev targets; skip it for custom registries. - if (!targetsPubDev(target.publishTo)) return false; + if (!targetsPubDev(target.publishTo)) { + return false; + } return isPubPackagePublished(target.packageName, target.version); }, diff --git a/packages/publish/src/stages/git-push.ts b/packages/publish/src/stages/git-push.ts index 4f56c094..c782b3e5 100644 --- a/packages/publish/src/stages/git-push.ts +++ b/packages/publish/src/stages/git-push.ts @@ -10,8 +10,12 @@ function toGithubAuthedUrl(remoteUrl: string, token: string): string | undefined // Avoid logging token by never returning it via logs. try { const url = new URL(remoteUrl); - if (url.protocol !== 'https:') return undefined; - if (url.host !== 'github.com') return undefined; + if (url.protocol !== 'https:') { + return undefined; + } + if (url.host !== 'github.com') { + return undefined; + } // Use the recommended username for GitHub token auth in HTTPS URLs. url.username = 'x-access-token'; @@ -35,7 +39,9 @@ export interface PushSetup { export async function preparePushSetup(ctx: PipelineContext): Promise { const { config, cwd, output } = ctx; - if (!config.git.push) return null; + if (!config.git.push) { + return null; + } const { remote } = config.git; const git = createGitCli(); @@ -55,7 +61,9 @@ export async function preparePushSetup(ctx: PipelineContext): Promise 0) { const resolved = resolveTagPackage(tag, packageNames); - if (resolved) return applyTemplate(resolved.packageName, resolved.version); + if (resolved) { + return applyTemplate(resolved.packageName, resolved.version); + } } // Fallback: try splitting on last @ for unsanitized "pkg@version" tags const atIndex = tag.lastIndexOf('@'); - if (atIndex === -1) return tag; + if (atIndex === -1) { + return tag; + } return applyTemplate(tag.slice(0, atIndex), tag.slice(atIndex + 1)); } @@ -142,11 +150,15 @@ function findNotesForTag(tag: string, notes: Record): string | u const resolved = resolveTagPackage(tag, Object.keys(notes)); if (resolved) { const body = notes[resolved.packageName]; - if (body?.trim()) return body; + if (body?.trim()) { + return body; + } } // Single-package fallback for version-only tags (e.g., "v1.0.0") const entries = Object.values(notes).filter((b) => b.trim()); - if (entries.length === 1 && isVersionOnlyTag(tag)) return entries[0]; + if (entries.length === 1 && isVersionOnlyTag(tag)) { + return entries[0]; + } return undefined; } @@ -154,7 +166,9 @@ function findNotesForTag(tag: string, notes: Record): string | u * Find the changelog for a given tag and format its entries as markdown. */ function formatChangelogForTag(tag: string, changelogs: VersionPackageChangelog[]): string | undefined { - if (changelogs.length === 0) return undefined; + if (changelogs.length === 0) { + return undefined; + } const resolved = resolveTagPackage( tag, @@ -168,7 +182,9 @@ function formatChangelogForTag(tag: string, changelogs: VersionPackageChangelog[ target = changelogs[0]; } - if (!target || target.entries.length === 0) return undefined; + if (!target || target.entries.length === 0) { + return undefined; + } const lines: string[] = []; for (const entry of target.entries) { @@ -202,7 +218,9 @@ export async function runGithubReleaseStage(ctx: PipelineContext): Promise } const firstTag = tags[0]; - if (!firstTag) return; + if (!firstTag) { + return; + } const tagsToRelease = config.githubRelease.perPackage ? tags : [firstTag]; const skipPackages = config.githubRelease.skipPackages ?? []; diff --git a/packages/publish/src/stages/prepare.ts b/packages/publish/src/stages/prepare.ts index bef27d3a..f784ef1d 100644 --- a/packages/publish/src/stages/prepare.ts +++ b/packages/publish/src/stages/prepare.ts @@ -80,12 +80,16 @@ export async function runPrepareStage(ctx: PipelineContext): Promise { debug(`Updated ${cargoPath} to version ${update.newVersion}`); const lockPath = await syncCargoLockfile(pkgDir); - if (lockPath) lockfiles.add(lockPath); + if (lockPath) { + lockfiles.add(lockPath); + } } if (lockfiles.size > 0) { const existing = new Set(ctx.additionalFiles ?? []); - for (const lock of lockfiles) existing.add(lock); + for (const lock of lockfiles) { + existing.add(lock); + } ctx.additionalFiles = [...existing]; } } diff --git a/packages/publish/src/types.ts b/packages/publish/src/types.ts index 6e7f0ceb..add8ef5c 100644 --- a/packages/publish/src/types.ts +++ b/packages/publish/src/types.ts @@ -218,7 +218,9 @@ export function getDefaultConfig(): PublishConfig { export function toPublishConfig(config: BasePublishConfig | undefined): PublishConfig { const defaults = getDefaultConfig(); - if (!config) return defaults; + if (!config) { + return defaults; + } return { npm: { diff --git a/packages/publish/src/utils/cargo.ts b/packages/publish/src/utils/cargo.ts index f10b5996..303c26e0 100644 --- a/packages/publish/src/utils/cargo.ts +++ b/packages/publish/src/utils/cargo.ts @@ -23,7 +23,9 @@ export { parseCargoToml }; */ export async function syncCargoLockfile(crateDir: string): Promise { const lockPath = findCargoLockfile(crateDir); - if (!lockPath) return undefined; + if (!lockPath) { + return undefined; + } try { await execCommand('cargo', ['update', '--workspace', '--offline'], { diff --git a/packages/publish/src/utils/exec.ts b/packages/publish/src/utils/exec.ts index e6ce28d7..18a40aca 100644 --- a/packages/publish/src/utils/exec.ts +++ b/packages/publish/src/utils/exec.ts @@ -93,7 +93,9 @@ export async function execCommand(file: string, args: string[], options: ExecOpt * Combine an exec error's message, stdout, and stderr into a single string for pattern matching. */ export function getExecErrorOutput(error: unknown): string { - if (!error || typeof error !== 'object') return String(error); + if (!error || typeof error !== 'object') { + return String(error); + } const e = error as { message?: string; stdout?: string; stderr?: string }; return [e.message, e.stdout, e.stderr].filter(Boolean).join('\n'); } diff --git a/packages/publish/src/utils/npm-env.ts b/packages/publish/src/utils/npm-env.ts index d63189a1..52cc032a 100644 --- a/packages/publish/src/utils/npm-env.ts +++ b/packages/publish/src/utils/npm-env.ts @@ -44,7 +44,9 @@ export function createNpmSubprocessIsolation(options: { // Default: no isolation. Still allow targeted env unsets. const baseEnv: Record = {}; - if (!authMethod) return { env: baseEnv, cleanup: () => {} }; + if (!authMethod) { + return { env: baseEnv, cleanup: () => {} }; + } const token = process.env.NPM_TOKEN ?? process.env.NODE_AUTH_TOKEN; const registryHost = (() => { diff --git a/packages/publish/src/utils/package-manager.ts b/packages/publish/src/utils/package-manager.ts index eac533bb..120741dc 100644 --- a/packages/publish/src/utils/package-manager.ts +++ b/packages/publish/src/utils/package-manager.ts @@ -4,8 +4,12 @@ import * as path from 'node:path'; export type PackageManager = 'pnpm' | 'npm' | 'yarn'; export function detectPackageManager(cwd: string): PackageManager { - if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'; - if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'; + if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) { + return 'pnpm'; + } + if (fs.existsSync(path.join(cwd, 'yarn.lock'))) { + return 'yarn'; + } return 'npm'; } @@ -21,7 +25,9 @@ export function buildPublishCommand( if (pm === 'pnpm') { file = 'pnpm'; args = ['publish', '--access', options.access, '--tag', options.tag]; - if (options.noGitChecks) args.push('--no-git-checks'); + if (options.noGitChecks) { + args.push('--no-git-checks'); + } } else { file = 'npm'; args = ['publish', '--access', options.access, '--tag', options.tag]; diff --git a/packages/publish/src/utils/pub.ts b/packages/publish/src/utils/pub.ts index 14199734..a0e2f7a1 100644 --- a/packages/publish/src/utils/pub.ts +++ b/packages/publish/src/utils/pub.ts @@ -18,8 +18,12 @@ export async function isPubPackagePublished(name: string, version: string): Prom signal: AbortSignal.timeout(PUB_DEV_API_TIMEOUT_MS), headers: { 'User-Agent': PUB_DEV_USER_AGENT }, }); - if (response.status === 200) return true; - if (response.status === 404) return false; + if (response.status === 200) { + return true; + } + if (response.status === 404) { + return false; + } debug(`pub.dev published-check returned ${response.status} for ${name}@${version}, will attempt publish`); return false; } catch (error) { diff --git a/packages/publish/src/utils/publish-retry.ts b/packages/publish/src/utils/publish-retry.ts index 69d49446..f6d01728 100644 --- a/packages/publish/src/utils/publish-retry.ts +++ b/packages/publish/src/utils/publish-retry.ts @@ -44,11 +44,15 @@ export function classifyPublishError(error: unknown): PublishErrorClass { const output = getExecErrorOutput(error); for (const pattern of PERMANENT_PATTERNS) { - if (pattern.test(output)) return 'permanent'; + if (pattern.test(output)) { + return 'permanent'; + } } for (const pattern of TRANSIENT_PATTERNS) { - if (pattern.test(output)) return 'transient'; + if (pattern.test(output)) { + return 'transient'; + } } return 'permanent'; diff --git a/packages/publish/test/integration/publish-e2e.spec.ts b/packages/publish/test/integration/publish-e2e.spec.ts index c829f690..a2e8847d 100644 --- a/packages/publish/test/integration/publish-e2e.spec.ts +++ b/packages/publish/test/integration/publish-e2e.spec.ts @@ -240,8 +240,12 @@ beforeAll(() => { }); afterAll(() => { - if (originalPath !== undefined) process.env.PATH = originalPath; - for (const dir of tempDirs) fs.rmSync(dir, { recursive: true, force: true }); + if (originalPath !== undefined) { + process.env.PATH = originalPath; + } + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } }); beforeEach(() => { @@ -255,7 +259,9 @@ afterEach(async () => { } process.chdir(ORIGINAL_CWD); for (const key of Object.keys(process.env)) { - if (!(key in savedEnv)) delete process.env[key]; + if (!(key in savedEnv)) { + delete process.env[key]; + } } Object.assign(process.env, savedEnv); }); @@ -363,7 +369,9 @@ describe.skipIf(!E2E_ENABLED)('Publish pipeline e2e (real npm registry)', () => // registry + token) wins, and pin globalconfig / cache / HOME to throwaway paths so no ambient // `~/.npmrc` leaks in either. The full-env restore in afterEach undoes all of this. for (const key of Object.keys(process.env)) { - if (/^npm_config_/i.test(key)) delete process.env[key]; + if (/^npm_config_/i.test(key)) { + delete process.env[key]; + } } const home = mkTemp('npm-home'); const emptyGlobalNpmrc = path.join(home, 'global-npmrc'); diff --git a/packages/publish/test/unit/registry/dispatcher.spec.ts b/packages/publish/test/unit/registry/dispatcher.spec.ts index c7104e23..b69c094e 100644 --- a/packages/publish/test/unit/registry/dispatcher.spec.ts +++ b/packages/publish/test/unit/registry/dispatcher.spec.ts @@ -111,8 +111,11 @@ describe('runPublishStage dispatcher', () => { await expect(runPublishStage(cargoRegistry, ctx)).resolves.toBeUndefined(); expect(ctx.output.cargo).toHaveLength(0); } finally { - if (savedToken === undefined) delete process.env.CARGO_REGISTRY_TOKEN; - else process.env.CARGO_REGISTRY_TOKEN = savedToken; + if (savedToken === undefined) { + delete process.env.CARGO_REGISTRY_TOKEN; + } else { + process.env.CARGO_REGISTRY_TOKEN = savedToken; + } } }); @@ -183,7 +186,9 @@ describe('runPublishStage dispatcher', () => { let calls = 0; const publish = vi.fn(async () => { calls += 1; - if (calls === 1) throw execError('503 Service Unavailable'); + if (calls === 1) { + throw execError('503 Service Unavailable'); + } }); const ctx = createContext(); diff --git a/packages/publish/test/unit/stages/npm-publish-order.spec.ts b/packages/publish/test/unit/stages/npm-publish-order.spec.ts index be3e66eb..a09df118 100644 --- a/packages/publish/test/unit/stages/npm-publish-order.spec.ts +++ b/packages/publish/test/unit/stages/npm-publish-order.spec.ts @@ -26,7 +26,9 @@ describe('orderNpmUpdates', () => { beforeEach(() => { vi.mocked(fs.readFileSync).mockImplementation((p: fs.PathOrFileDescriptor) => { const manifest = manifests[String(p)]; - if (!manifest) throw new Error(`no manifest for ${String(p)}`); + if (!manifest) { + throw new Error(`no manifest for ${String(p)}`); + } return JSON.stringify(manifest); }); }); diff --git a/packages/release/src/backfill/github-release.ts b/packages/release/src/backfill/github-release.ts index a7612086..1e8894e0 100644 --- a/packages/release/src/backfill/github-release.ts +++ b/packages/release/src/backfill/github-release.ts @@ -26,8 +26,12 @@ export function decideReleaseUpdate( onlyMissing: boolean, force: boolean = false, ): UpdateDecision { - if (existingBody === null) return { action: 'skip', reason: 'no-release' }; - if (onlyMissing && existingBody.includes(NOTES_MARKER)) return { action: 'skip', reason: 'already-backfilled' }; + if (existingBody === null) { + return { action: 'skip', reason: 'no-release' }; + } + if (onlyMissing && existingBody.includes(NOTES_MARKER)) { + return { action: 'skip', reason: 'already-backfilled' }; + } if (!force && !onlyMissing && existingBody.trim() && !existingBody.includes(NOTES_MARKER)) { return { action: 'skip', reason: 'hand-edited' }; } @@ -57,7 +61,9 @@ export function getReleaseBody(tag: string): string | null { throw new Error('GitHub CLI (`gh`) not found — install it and run `gh auth login` to update release bodies.'); } const stderr = (typeof e.stderr === 'string' ? e.stderr : e.stderr?.toString()) || e.message || ''; - if (/release not found/i.test(stderr)) return null; + if (/release not found/i.test(stderr)) { + return null; + } throw new Error(`\`gh release view ${tag}\` failed: ${stderr.trim() || 'unknown error'}`); } } @@ -84,7 +90,9 @@ export function getReleaseInfo(tag: string): { isDraft: boolean; body: string | throw new Error('GitHub CLI (`gh`) not found — install it and run `gh auth login` to update release bodies.'); } const stderr = (typeof e.stderr === 'string' ? e.stderr : e.stderr?.toString()) || e.message || ''; - if (/release not found/i.test(stderr)) return null; + if (/release not found/i.test(stderr)) { + return null; + } throw new Error(`\`gh release view ${tag}\` failed: ${stderr.trim() || 'unknown error'}`); } } diff --git a/packages/release/src/backfill/reconstruct.ts b/packages/release/src/backfill/reconstruct.ts index abee1236..31b5c7fb 100644 --- a/packages/release/src/backfill/reconstruct.ts +++ b/packages/release/src/backfill/reconstruct.ts @@ -75,10 +75,16 @@ export async function reconstructChangelogs(opts: ReconstructOptions): Promise tasks[k] !== false).length; } diff --git a/packages/release/src/commands/changed.ts b/packages/release/src/commands/changed.ts index 2f5518fe..3e27ab66 100644 --- a/packages/release/src/commands/changed.ts +++ b/packages/release/src/commands/changed.ts @@ -15,6 +15,8 @@ import type { ReleaseOutput } from '../types.js'; */ export function publishDidChange(result: ReleaseOutput | null | undefined): boolean { const output = result?.publishOutput; - if (!output || output.dryRun) return false; + if (!output || output.dryRun) { + return false; + } return [...output.npm, ...output.cargo, ...output.pub].some((r) => r.success && !r.skipped && !r.alreadyPublished); } diff --git a/packages/release/src/commands/emitResult.ts b/packages/release/src/commands/emitResult.ts index 8a213bc0..5df7b680 100644 --- a/packages/release/src/commands/emitResult.ts +++ b/packages/release/src/commands/emitResult.ts @@ -32,13 +32,17 @@ export function emitResult( data: unknown, opts: { json?: boolean; output?: string; changed?: boolean; warnings?: EnvelopeWarning[] }, ): void { - if (!opts.json && !opts.output) return; + if (!opts.json && !opts.output) { + return; + } writeEnvelope(successEnvelope(data ?? null, { changed: opts.changed, warnings: opts.warnings }), opts); } /** Emit a structured error envelope for a thrown value on the JSON channel. */ export function emitError(error: unknown, opts: { json?: boolean; output?: string }): void { - if (!opts.json && !opts.output) return; + if (!opts.json && !opts.output) { + return; + } writeEnvelope(errorEnvelope([toEnvelopeError(error)]), opts); } diff --git a/packages/release/src/commands/gate-command.ts b/packages/release/src/commands/gate-command.ts index 28035a05..11799241 100644 --- a/packages/release/src/commands/gate-command.ts +++ b/packages/release/src/commands/gate-command.ts @@ -14,9 +14,15 @@ export function createGateCommand(): Command { .option('-q, --quiet', 'Suppress non-error output', false) .option('--project-dir ', 'Project directory', process.cwd()) .action(async (opts) => { - if (opts.json) setJsonMode(true); - if (opts.verbose) setLogLevel('debug'); - if (opts.quiet) setQuietMode(true); + if (opts.json) { + setJsonMode(true); + } + if (opts.verbose) { + setLogLevel('debug'); + } + if (opts.quiet) { + setQuietMode(true); + } try { const result = await runGate({ diff --git a/packages/release/src/commands/labels-command.ts b/packages/release/src/commands/labels-command.ts index 5c512125..0648f9f7 100644 --- a/packages/release/src/commands/labels-command.ts +++ b/packages/release/src/commands/labels-command.ts @@ -45,7 +45,9 @@ async function detectRepoFromGit(projectDir?: string): Promise 0) parts.push(`${days}d`); - if (hours > 0) parts.push(`${hours}h`); - if (minutes > 0) parts.push(`${minutes}m`); - if (parts.length === 0) parts.push(`${Math.ceil(ms / 1000)}s`); + if (days > 0) { + parts.push(`${days}d`); + } + if (hours > 0) { + parts.push(`${hours}h`); + } + if (minutes > 0) { + parts.push(`${minutes}m`); + } + if (parts.length === 0) { + parts.push(`${Math.ceil(ms / 1000)}s`); + } return parts.join(' '); } diff --git a/packages/release/src/failure-report/failure-report.ts b/packages/release/src/failure-report/failure-report.ts index e4bcdbd7..da0a3bba 100644 --- a/packages/release/src/failure-report/failure-report.ts +++ b/packages/release/src/failure-report/failure-report.ts @@ -137,7 +137,9 @@ export function buildLedger(versionOutput: VersionOutput, publishOutput: Publish return updates.map((u) => { const entry = byPackage.get(u.packageName); - if (entry) return entry; + if (entry) { + return entry; + } return { packageName: u.packageName, version: u.newVersion, @@ -301,7 +303,9 @@ export function renderResolvedReport(versionOutput: VersionOutput): string { * treated as `unresolved` (the conservative default — surface the warning rather than hide it). */ export function parseFailureReportStatus(body: string): FailureReportStatus | null { - if (!body.startsWith(FAILURE_MARKER)) return null; + if (!body.startsWith(FAILURE_MARKER)) { + return null; + } return STATUS_FIELD.decode(body) ?? 'unresolved'; } @@ -310,7 +314,9 @@ export function parseFailureReportStatus(body: string): FailureReportStatus | nu * is not a failure report or the data comment is missing/malformed. */ export function parseFailureReportData(body: string): FailureReportData | null { - if (!body.startsWith(FAILURE_MARKER)) return null; + if (!body.startsWith(FAILURE_MARKER)) { + return null; + } return DATA_FIELD.decode(body); } diff --git a/packages/release/src/failure-report/post.ts b/packages/release/src/failure-report/post.ts index 7ca4a966..757d05bc 100644 --- a/packages/release/src/failure-report/post.ts +++ b/packages/release/src/failure-report/post.ts @@ -70,7 +70,9 @@ export async function resolveFailureReportIfPresent( ): Promise { try { const existing = await findPreviewComment(forge, prNumber, FAILURE_MARKER); - if (existing === null) return; + if (existing === null) { + return; + } await postOrUpdateComment(forge, prNumber, renderResolvedReport(versionOutput), FAILURE_MARKER); info(`Marked publish-failure report on PR #${prNumber} as resolved`); } catch (err) { @@ -116,8 +118,12 @@ export async function detectUnresolvedFailure(forge: Forge, prNumber: number): P return null; } - if (!body) return null; - if (parseFailureReportStatus(body) !== 'unresolved') return null; + if (!body) { + return null; + } + if (parseFailureReportStatus(body) !== 'unresolved') { + return null; + } return parseReportLedgerSummary(body, prNumber); } @@ -129,7 +135,9 @@ export async function detectUnresolvedFailure(forge: Forge, prNumber: number): P */ function parseReportLedgerSummary(body: string, prNumber: number): UnresolvedFailure | null { const data = parseFailureReportData(body); - if (!data) return null; + if (!data) { + return null; + } return { previousLabel: data.label, published: data.published, diff --git a/packages/release/src/gate/gate.ts b/packages/release/src/gate/gate.ts index 7614396b..a810feaf 100644 --- a/packages/release/src/gate/gate.ts +++ b/packages/release/src/gate/gate.ts @@ -225,9 +225,15 @@ async function computeGateResult(input: ComputeGateInput): Promise { * - In `commit` mode, don't notify on `release:skip` — that's deliberate, not user error. */ function shouldNotifyPR(e: PREvaluation, trigger: 'commit' | 'label'): boolean { - if (e.shouldRelease) return false; - if (e.blocked) return true; - if (!e.hasReleaseIntent) return false; + if (e.shouldRelease) { + return false; + } + if (e.blocked) { + return true; + } + if (!e.hasReleaseIntent) { + return false; + } return trigger === 'label'; } @@ -239,7 +245,9 @@ async function notifyInsufficientLabels( ): Promise { await Promise.all( evaluations.map(async (e) => { - if (!shouldNotifyPR(e, trigger)) return; + if (!shouldNotifyPR(e, trigger)) { + return; + } try { const body = buildNotifyBody(e, labelConfig); await postOrUpdateComment(forge, e.prNumber, body, NOTIFY_MARKER); diff --git a/packages/release/src/git.ts b/packages/release/src/git.ts index 7f2bd2ee..84a4830c 100644 --- a/packages/release/src/git.ts +++ b/packages/release/src/git.ts @@ -17,10 +17,14 @@ export interface GitHubContext { export function getGitHubContext(): GitHubContext | null { const repo = process.env.GITHUB_REPOSITORY; - if (!repo) return null; + if (!repo) { + return null; + } const [owner, repoName] = repo.split('/'); - if (!owner || !repoName) return null; + if (!owner || !repoName) { + return null; + } return { owner, diff --git a/packages/release/src/github.ts b/packages/release/src/github.ts index 2781cdcf..f94ab259 100644 --- a/packages/release/src/github.ts +++ b/packages/release/src/github.ts @@ -9,7 +9,9 @@ export const MARKER = ''; * token before reaching the forge-backed paths; the assertion is a safety net for that invariant. */ export function forgeFor(context: { token: string | null | undefined; owner: string; repo: string }): Forge { - if (!context.token) throw new Error('A GitHub token is required to access the forge.'); + if (!context.token) { + throw new Error('A GitHub token is required to access the forge.'); + } return createGitHubForge({ token: context.token, owner: context.owner, repo: context.repo }); } @@ -53,7 +55,9 @@ export async function findMergedPRsSinceLastRelease(forge: Forge, projectDir: st const prGroups = await Promise.all(mergeShas.map((sha) => findMergedPRsForCommit(forge, sha))); const seen = new Set(); for (const prs of prGroups) { - for (const n of prs) seen.add(n); + for (const n of prs) { + seen.add(n); + } } return [...seen]; } diff --git a/packages/release/src/label-definitions.ts b/packages/release/src/label-definitions.ts index 17e7a94d..bf71a10a 100644 --- a/packages/release/src/label-definitions.ts +++ b/packages/release/src/label-definitions.ts @@ -107,7 +107,9 @@ export function deriveLabelDefinitions( // first (more specific) definition so each label is created exactly once. const seen = new Set(); return definitions.filter((def) => { - if (!def.name || seen.has(def.name)) return false; + if (!def.name || seen.has(def.name)) { + return false; + } seen.add(def.name); return true; }); diff --git a/packages/release/src/label-utils.ts b/packages/release/src/label-utils.ts index bd98f44e..fdb13ed8 100644 --- a/packages/release/src/label-utils.ts +++ b/packages/release/src/label-utils.ts @@ -107,17 +107,31 @@ export function detectLabelConflicts(prLabels: string[], labels: LabelConfig = D * `release:graduate` wins over everything and returns undefined (graduation is bump-less). */ export function composeBumpFromLabels(prLabels: string[], labels: LabelConfig = DEFAULT_LABELS): string | undefined { - if (prLabels.includes(labels.graduate)) return undefined; + if (prLabels.includes(labels.graduate)) { + return undefined; + } if (prLabels.includes(labels.prerelease)) { - if (prLabels.includes(labels.major)) return 'premajor'; - if (prLabels.includes(labels.minor)) return 'preminor'; - if (prLabels.includes(labels.patch)) return 'prepatch'; + if (prLabels.includes(labels.major)) { + return 'premajor'; + } + if (prLabels.includes(labels.minor)) { + return 'preminor'; + } + if (prLabels.includes(labels.patch)) { + return 'prepatch'; + } return 'prerelease'; } - if (prLabels.includes(labels.major)) return 'major'; - if (prLabels.includes(labels.minor)) return 'minor'; - if (prLabels.includes(labels.patch)) return 'patch'; + if (prLabels.includes(labels.major)) { + return 'major'; + } + if (prLabels.includes(labels.minor)) { + return 'minor'; + } + if (prLabels.includes(labels.patch)) { + return 'patch'; + } return undefined; } diff --git a/packages/release/src/preview/detect.ts b/packages/release/src/preview/detect.ts index 291e1ed9..042a699c 100644 --- a/packages/release/src/preview/detect.ts +++ b/packages/release/src/preview/detect.ts @@ -17,12 +17,16 @@ export function detectPrerelease(packagePaths: string[], projectDir: string): Pr : [path.join(projectDir, 'package.json')]; for (const pkgPath of paths) { - if (!fs.existsSync(pkgPath)) continue; + if (!fs.existsSync(pkgPath)) { + continue; + } try { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); const result = parsePrerelease(pkg.version); - if (result.isPrerelease) return result; + if (result.isPrerelease) { + return result; + } } catch { // Skip unreadable package.json files } @@ -36,7 +40,9 @@ export function detectPrerelease(packagePaths: string[], projectDir: string): Pr * E.g. "0.3.0-next.4" → { isPrerelease: true, identifier: "next" } */ export function parsePrerelease(version: string | undefined): PrereleaseDetection { - if (!version) return { isPrerelease: false }; + if (!version) { + return { isPrerelease: false }; + } const match = version.match(/-([a-zA-Z0-9][a-zA-Z0-9-]*)(?:\.\d+)*(?:\+[^\s]+)?$/); if (match) { diff --git a/packages/release/src/preview/format.ts b/packages/release/src/preview/format.ts index 6fff23a5..6f3660ab 100644 --- a/packages/release/src/preview/format.ts +++ b/packages/release/src/preview/format.ts @@ -107,7 +107,9 @@ function getIntroMessage(strategy: ReleaseStrategy, standingPrNumber?: number): } function getLabelBanner(labelContext?: LabelContext): string[] { - if (!labelContext) return []; + if (!labelContext) { + return []; + } const lines: string[] = []; @@ -129,7 +131,9 @@ function getLabelBanner(labelContext?: LabelContext): string[] { ) { descriptor.push(labelContext.bumpLabel); const bumpLabelName = labelContext.labels?.[labelContext.bumpLabel]; - if (bumpLabelName) sources.push(`\`${bumpLabelName}\``); + if (bumpLabelName) { + sources.push(`\`${bumpLabelName}\``); + } } if (labelContext.prerelease) { descriptor.push('prerelease'); @@ -159,10 +163,18 @@ function getLabelBanner(labelContext?: LabelContext): string[] { // Show what was seen, point at the override surface (the standing PR) and the bypass label. if (labelContext.advisoryInStandingPr) { const seen: string[] = []; - if (labelContext.bumpLabel) seen.push(`\`bump:${labelContext.bumpLabel}\``); - if (labelContext.scopeLabels?.length) seen.push(...labelContext.scopeLabels.map((s) => `\`${s}\``)); - if (labelContext.stable) seen.push(`\`${labelContext.labels?.graduate ?? 'release:graduate'}\``); - if (labelContext.prerelease) seen.push(`\`${labelContext.labels?.prerelease ?? 'channel:prerelease'}\``); + if (labelContext.bumpLabel) { + seen.push(`\`bump:${labelContext.bumpLabel}\``); + } + if (labelContext.scopeLabels?.length) { + seen.push(...labelContext.scopeLabels.map((s) => `\`${s}\``)); + } + if (labelContext.stable) { + seen.push(`\`${labelContext.labels?.graduate ?? 'release:graduate'}\``); + } + if (labelContext.prerelease) { + seen.push(`\`${labelContext.labels?.prerelease ?? 'channel:prerelease'}\``); + } const seenStr = seen.length ? ` (saw: ${seen.join(', ')})` : ''; const immediateLabel = labelContext.labels?.immediate ?? 'release:immediate'; lines.push( @@ -373,14 +385,23 @@ export function formatPreviewComment(result: ReleaseOutput | null, options?: For const coversAll = syncListed.length === publishableUpdates(versionOutput).length; const heading = coversAll ? 'All packages' : 'Also bumped'; bumped.push(`**${heading} → ${syncListed[0]?.newVersion}** (sync versioning)`, ''); - for (const u of syncListed) bumped.push(`- \`${u.packageName}\``); + for (const u of syncListed) { + bumped.push(`- \`${u.packageName}\``); + } } } else { - if (hasPackageChangelogs || sharedEntries) bumped.push('**Also bumped** (sync versioning)', ''); - else bumped.push('**Bumped** (sync versioning — no individual changes)', ''); - for (const u of syncBumpedOnly) bumped.push(`- \`${u.packageName}\` → ${u.newVersion}`); + if (hasPackageChangelogs || sharedEntries) { + bumped.push('**Also bumped** (sync versioning)', ''); + } else { + bumped.push('**Bumped** (sync versioning — no individual changes)', ''); + } + for (const u of syncBumpedOnly) { + bumped.push(`- \`${u.packageName}\` → ${u.newVersion}`); + } + } + if (bumped.length > 0) { + lines.push(...blockquote(bumped), ''); } - if (bumped.length > 0) lines.push(...blockquote(bumped), ''); } } @@ -423,7 +444,9 @@ function renderQueuedTable(snapshot: StandingPRSnapshot): string[] { const changelogs = snapshot.manifest.versionOutput.changelogs.filter( (cl) => cl.entries.length > 0 || cl.version !== cl.previousVersion, ); - if (changelogs.length === 0) return []; + if (changelogs.length === 0) { + return []; + } const lines: string[] = [ '', '### Currently queued for release', @@ -482,8 +505,12 @@ function renderMergeTable(rows: MergedRow[]): string[] { const standing = row.standing ?? '—'; const current = row.current ?? '—'; let afterCell = row.afterMerge; - if (row.status === 'escalated' && row.standing) afterCell += ` ⚠ escalated from ${row.standing}`; - if (row.status === 'new-from-pr') afterCell += ' (new)'; + if (row.status === 'escalated' && row.standing) { + afterCell += ` ⚠ escalated from ${row.standing}`; + } + if (row.status === 'new-from-pr') { + afterCell += ' (new)'; + } lines.push(`| \`${row.packageName}\` | ${standing} | ${current} | ${afterCell} |`); } lines.push(''); @@ -494,7 +521,9 @@ function renderEntries(entries: VersionChangelogEntry[], refs: ChangelogRefsMode const lines: string[] = []; const grouped = new Map(); for (const entry of entries) { - if (!grouped.has(entry.type)) grouped.set(entry.type, []); + if (!grouped.has(entry.type)) { + grouped.set(entry.type, []); + } grouped.get(entry.type)?.push(entry); } const renderedTypes = new Set(); @@ -563,6 +592,8 @@ function capitalize(str: string): string { * Markdown blockquote. Trailing blanks are dropped so the quote ends cleanly before the next block. */ function blockquote(lines: string[]): string[] { const trimmed = [...lines]; - while (trimmed.length > 0 && trimmed[trimmed.length - 1] === '') trimmed.pop(); + while (trimmed.length > 0 && trimmed[trimmed.length - 1] === '') { + trimmed.pop(); + } return trimmed.map((l) => (l.length > 0 ? `> ${l}` : '>')); } diff --git a/packages/release/src/preview/merge.ts b/packages/release/src/preview/merge.ts index 393f1043..ba0b1aba 100644 --- a/packages/release/src/preview/merge.ts +++ b/packages/release/src/preview/merge.ts @@ -29,7 +29,9 @@ export function mergeForPreview( const byName = new Map(); for (const cl of standingChangelogs) { - if (!cl.entries.length && cl.version === cl.previousVersion) continue; + if (!cl.entries.length && cl.version === cl.previousVersion) { + continue; + } byName.set(cl.packageName, { packageName: cl.packageName, baseline: cl.previousVersion, @@ -40,7 +42,9 @@ export function mergeForPreview( } for (const cl of currentChangelogs) { - if (!cl.entries.length && cl.version === cl.previousVersion) continue; + if (!cl.entries.length && cl.version === cl.previousVersion) { + continue; + } const existing = byName.get(cl.packageName); if (!existing) { byName.set(cl.packageName, { @@ -54,7 +58,9 @@ export function mergeForPreview( } // Only record a current version when it is parseable — an invalid string would surface // as a garbled value in the "This PR" column of the rendered table. - if (semver.valid(cl.version)) existing.current = cl.version; + if (semver.valid(cl.version)) { + existing.current = cl.version; + } // Guard semver comparison against malformed version strings (older manifests, unexpected // calculator output) — semver.gt throws on invalid input, which would otherwise abort the // entire preview. Treat unparseable versions as 'unchanged' to keep the preview non-fatal. diff --git a/packages/release/src/preview/preview.ts b/packages/release/src/preview/preview.ts index 131be842..739f9db4 100644 --- a/packages/release/src/preview/preview.ts +++ b/packages/release/src/preview/preview.ts @@ -52,7 +52,9 @@ interface PullRequestEvent { /** Read and parse the GitHub Actions event payload once, or undefined on any error. */ function readEventPayload(): PullRequestEvent | undefined { const eventPath = process.env.GITHUB_EVENT_PATH; - if (!eventPath || !fs.existsSync(eventPath)) return undefined; + if (!eventPath || !fs.existsSync(eventPath)) { + return undefined; + } try { return JSON.parse(fs.readFileSync(eventPath, 'utf-8')) as PullRequestEvent; } catch { @@ -375,11 +377,19 @@ async function applyLabelOverrides( // do not propagate to `result`. Version analysis still runs (commit-driven) so the // preview can show this PR's contribution to the standing PR via the merge table. if (advisoryInStandingPr) { - if (prLabels.includes(labels.major)) labelContext.bumpLabel = 'major'; - else if (prLabels.includes(labels.minor)) labelContext.bumpLabel = 'minor'; - else if (prLabels.includes(labels.patch)) labelContext.bumpLabel = 'patch'; - if (prLabels.includes(labels.graduate)) labelContext.stable = true; - if (prLabels.includes(labels.prerelease)) labelContext.prerelease = true; + if (prLabels.includes(labels.major)) { + labelContext.bumpLabel = 'major'; + } else if (prLabels.includes(labels.minor)) { + labelContext.bumpLabel = 'minor'; + } else if (prLabels.includes(labels.patch)) { + labelContext.bumpLabel = 'patch'; + } + if (prLabels.includes(labels.graduate)) { + labelContext.stable = true; + } + if (prLabels.includes(labels.prerelease)) { + labelContext.prerelease = true; + } return { options: result, labelContext }; } @@ -508,10 +518,20 @@ async function applyLabelOverrides( * `preminor`, `prepatch`, `premajor`, or `prerelease` — only the first three carry a magnitude. */ function magnitudeFromBump(bump: string | undefined): string | undefined { - if (!bump) return undefined; - if (bump === 'major' || bump === 'minor' || bump === 'patch') return bump; - if (bump === 'premajor') return 'major'; - if (bump === 'preminor') return 'minor'; - if (bump === 'prepatch') return 'patch'; + if (!bump) { + return undefined; + } + if (bump === 'major' || bump === 'minor' || bump === 'patch') { + return bump; + } + if (bump === 'premajor') { + return 'major'; + } + if (bump === 'preminor') { + return 'minor'; + } + if (bump === 'prepatch') { + return 'patch'; + } return undefined; } diff --git a/packages/release/src/preview/refresh.ts b/packages/release/src/preview/refresh.ts index b187dc77..68707408 100644 --- a/packages/release/src/preview/refresh.ts +++ b/packages/release/src/preview/refresh.ts @@ -80,15 +80,23 @@ export async function refreshFeederPreviews(options: RefreshAfterReleaseOptions) const eligible: OpenPullRequest[] = []; for (const pr of open) { - if (pr.draft) continue; // drafts don't carry previews - if (pr.headRef === branch) continue; // the standing PR's manifest is authoritative, never previewed + if (pr.draft) { + continue; // drafts don't carry previews + } + if (pr.headRef === branch) { + continue; // the standing PR's manifest is authoritative, never previewed + } // Only refresh PRs that already have a preview — never manufacture one on a PR that opted out. - if (!(await forge.findComment(pr.number, MARKER))) continue; + if (!(await forge.findComment(pr.number, MARKER))) { + continue; + } eligible.push(pr); // Open PRs come back most-recently-updated first, so the first MAX_FEEDER_PR_REFRESH are exactly // the set we refresh. Stop probing once one past the cap is in hand — bounds the per-PR // findComment calls instead of fetching comments for every open PR on a busy repo. - if (eligible.length > MAX_FEEDER_PR_REFRESH) break; + if (eligible.length > MAX_FEEDER_PR_REFRESH) { + break; + } } let toRefresh = eligible; diff --git a/packages/release/src/release.ts b/packages/release/src/release.ts index 491a003f..da15899c 100644 --- a/packages/release/src/release.ts +++ b/packages/release/src/release.ts @@ -25,7 +25,9 @@ async function resolveReleaseReportTarget(): Promise<{ mode: 'direct' | 'manual'; } | null> { const githubContext = getGitHubContext(); - if (!githubContext?.token) return null; + if (!githubContext?.token) { + return null; + } const forge = forgeFor({ token: githubContext.token, owner: githubContext.owner, repo: githubContext.repo }); const { sha } = githubContext; @@ -68,8 +70,12 @@ async function reportReleaseFailure(versionOutput: VersionOutput, err: PipelineE export function resolveScopeToTarget(scopeName: string, scopeLabels: Record): string { const prefixed = `scope:${scopeName}`; - if (scopeLabels[prefixed]) return scopeLabels[prefixed]; - if (scopeLabels[scopeName]) return scopeLabels[scopeName]; + if (scopeLabels[prefixed]) { + return scopeLabels[prefixed]; + } + if (scopeLabels[scopeName]) { + return scopeLabels[scopeName]; + } const available = Object.keys(scopeLabels).join(', '); throw new Error(`Scope "${scopeName}" not found in ci.scopeLabels. Available: ${available}`); } @@ -182,22 +188,36 @@ async function applyScopeLabelsFromPR( function applyStepOverrides(options: ReleaseOptions, releaseConfig: ReleaseConfig | undefined): void { // Steps array: a step absent from the list is skipped unless CLI already set the flag. if (releaseConfig?.steps) { - if (!releaseConfig.steps.includes('notes') && !options.skipNotes) options.skipNotes = true; - if (!releaseConfig.steps.includes('publish') && !options.skipPublish) options.skipPublish = true; + if (!releaseConfig.steps.includes('notes') && !options.skipNotes) { + options.skipNotes = true; + } + if (!releaseConfig.steps.includes('publish') && !options.skipPublish) { + options.skipPublish = true; + } } // ci overrides: can suppress a step even when it appears in 'steps'. - if (releaseConfig?.ci?.notes === false && !options.skipNotes) options.skipNotes = true; - if (releaseConfig?.ci?.githubRelease === false && !options.skipGithubRelease) options.skipGithubRelease = true; + if (releaseConfig?.ci?.notes === false && !options.skipNotes) { + options.skipNotes = true; + } + if (releaseConfig?.ci?.githubRelease === false && !options.skipGithubRelease) { + options.skipGithubRelease = true; + } } export async function runRelease(inputOptions: ReleaseOptions): Promise { // Work on a copy so config-driven overrides never mutate the caller's object const options = { ...inputOptions }; - if (options.verbose) setLogLevel('debug'); - if (options.quiet) setQuietMode(true); - if (options.json) setJsonMode(true); + if (options.verbose) { + setLogLevel('debug'); + } + if (options.quiet) { + setQuietMode(true); + } + if (options.json) { + setJsonMode(true); + } // Load release config for automation behavior let releaseKitConfig: ReturnType; diff --git a/packages/release/src/standing-pr/authorization.ts b/packages/release/src/standing-pr/authorization.ts index e7ae987b..3473da91 100644 --- a/packages/release/src/standing-pr/authorization.ts +++ b/packages/release/src/standing-pr/authorization.ts @@ -37,7 +37,9 @@ export interface EventActor { /** Read the triggering actor (and the merger, for publish) from the GitHub Actions event payload. */ export function getEventActor(): EventActor { const eventPath = process.env.GITHUB_EVENT_PATH; - if (!eventPath) return {}; + if (!eventPath) { + return {}; + } try { const event = JSON.parse(fs.readFileSync(eventPath, 'utf-8')) as { action?: string; @@ -63,7 +65,9 @@ function isBot(login: string | undefined, type: string | undefined): boolean { /** An `allowedActors` entry naming a GitHub team, e.g. `@acme/releasers` → `{ org, team }`. */ function parseTeamRef(entry: string): { org: string; team: string } | undefined { - if (!entry.startsWith('@') || !entry.includes('/')) return undefined; + if (!entry.startsWith('@') || !entry.includes('/')) { + return undefined; + } const [org, team] = entry.slice(1).split('/'); return org && team ? { org, team } : undefined; } @@ -83,25 +87,35 @@ export async function isAuthorizedActor( type: string | undefined, authz: StandingPrAuthorization, ): Promise { - if (isBot(login, type)) return true; - if (!login) return false; + if (isBot(login, type)) { + return true; + } + if (!login) { + return false; + } const entries = authz.allowedActors ?? []; // 1. Username allow-list. GitHub usernames are case-insensitive, so compare case-folded — an // `allowedActors: ['Alice']` entry must still match an event delivering `login: 'alice'`. const lc = login.toLowerCase(); - if (entries.some((e) => !parseTeamRef(e) && e.toLowerCase() === lc)) return true; + if (entries.some((e) => !parseTeamRef(e) && e.toLowerCase() === lc)) { + return true; + } // 2. Repository permission threshold. const permission = await forge.getActorPermission(login); - if (PERMISSION_RANK[permission] >= PERMISSION_RANK[authz.requiredPermission]) return true; + if (PERMISSION_RANK[permission] >= PERMISSION_RANK[authz.requiredPermission]) { + return true; + } // 3. Team membership — last, since it costs an API call per team and a 403 (no org-read scope) // propagates to the caller's gate wrapper. An `@org/team` member is authorized regardless of // repo permission. for (const entry of entries) { const ref = parseTeamRef(entry); - if (ref && (await forge.isTeamMember(ref.org, ref.team, login))) return true; + if (ref && (await forge.isTeamMember(ref.org, ref.team, login))) { + return true; + } } return false; } diff --git a/packages/release/src/standing-pr/changelog-region.ts b/packages/release/src/standing-pr/changelog-region.ts index c6bae0ea..b29045f8 100644 --- a/packages/release/src/standing-pr/changelog-region.ts +++ b/packages/release/src/standing-pr/changelog-region.ts @@ -86,7 +86,9 @@ function dedupe(attributed: AttributedEntry[]): DedupedEntry[] { const byKey = new Map(); const order: DedupedEntry[] = []; for (const { entry, pkg } of attributed) { - if (entry.synthetic) continue; + if (entry.synthetic) { + continue; + } const key = dedupeKey(entry); let agg = byKey.get(key); if (!agg) { @@ -94,7 +96,9 @@ function dedupe(attributed: AttributedEntry[]): DedupedEntry[] { byKey.set(key, agg); order.push(agg); } - if (pkg) agg.pkgs.add(pkg); + if (pkg) { + agg.pkgs.add(pkg); + } } return order; } @@ -138,7 +142,11 @@ const DEMOTED_HEADING = 'Dependencies & version bumps'; * removed, so the caller's de-duplicated change count is unchanged. */ function renderGrouped(deduped: DedupedEntry[], refOpts: RefRenderOptions, demoteScopes: readonly string[]): string[] { const distinct = new Set(); - for (const d of deduped) for (const p of d.pkgs) distinct.add(p); + for (const d of deduped) { + for (const p of d.pkgs) { + distinct.add(p); + } + } // Attribution only earns its place when the list spans more than one package. const attribution = distinct.size > 1; @@ -146,8 +154,11 @@ function renderGrouped(deduped: DedupedEntry[], refOpts: RefRenderOptions, demot const promoted: DedupedEntry[] = []; const demoted: DedupedEntry[] = []; for (const d of deduped) { - if (demote && d.entry.scope !== undefined && demoteScopes.includes(d.entry.scope)) demoted.push(d); - else promoted.push(d); + if (demote && d.entry.scope !== undefined && demoteScopes.includes(d.entry.scope)) { + demoted.push(d); + } else { + promoted.push(d); + } } const byLabel = new Map(); @@ -164,12 +175,16 @@ function renderGrouped(deduped: DedupedEntry[], refOpts: RefRenderOptions, demot const lines: string[] = []; const emit = (label: string): void => { const list = byLabel.get(label); - if (!list?.length) return; + if (!list?.length) { + return; + } // `#### `, not bold `**…**`: inside the blockquote GitHub gives a heading paragraph-level // top margin, so consecutive sections stay visually separated — bold text collapses to near-zero // spacing there. Matches the preview surface, which already uses `#### ` headings. lines.push(`#### ${label}`, ''); - for (const d of list) lines.push(entryLine(d, attribution, refOpts)); + for (const d of list) { + lines.push(entryLine(d, attribution, refOpts)); + } lines.push(''); }; const rendered = new Set(); @@ -177,15 +192,23 @@ function renderGrouped(deduped: DedupedEntry[], refOpts: RefRenderOptions, demot emit(label); rendered.add(label); } - for (const label of byLabel.keys()) if (!rendered.has(label)) emit(label); + for (const label of byLabel.keys()) { + if (!rendered.has(label)) { + emit(label); + } + } // The demoted subsection trails every type bucket, so low-signal dependency churn never crowds the // user-facing changes at the top. First-seen order is preserved (deduped iteration order). if (demoted.length > 0) { lines.push(`#### ${DEMOTED_HEADING}`, ''); - for (const d of demoted) lines.push(entryLine(d, attribution, refOpts)); + for (const d of demoted) { + lines.push(entryLine(d, attribution, refOpts)); + } lines.push(''); } - if (lines[lines.length - 1] === '') lines.pop(); + if (lines[lines.length - 1] === '') { + lines.pop(); + } return lines; } @@ -238,11 +261,17 @@ export function makeRowChangelogRenderer( const attributed: AttributedEntry[] = []; for (const name of packageNames) { const cl = byPkg.get(name); - if (!cl) continue; - for (const entry of cl.entries) attributed.push({ entry, pkg: name }); + if (!cl) { + continue; + } + for (const entry of cl.entries) { + attributed.push({ entry, pkg: name }); + } } const deduped = dedupe(attributed); - if (deduped.length === 0) return ''; + if (deduped.length === 0) { + return ''; + } const summary = heldBack ? `Changelog (${pluralEntries(deduped.length)}) — held back, won’t publish` : `Changelog (${pluralEntries(deduped.length)})`; @@ -257,10 +286,14 @@ function collectAttributed(versionOutput: VersionOutput, sharedOnly: boolean): A const attributed: AttributedEntry[] = []; if (!sharedOnly) { for (const cl of versionOutput.changelogs) { - for (const entry of cl.entries) attributed.push({ entry, pkg: cl.packageName }); + for (const entry of cl.entries) { + attributed.push({ entry, pkg: cl.packageName }); + } } } - for (const entry of versionOutput.sharedEntries ?? []) attributed.push({ entry }); + for (const entry of versionOutput.sharedEntries ?? []) { + attributed.push({ entry }); + } return attributed; } @@ -286,7 +319,9 @@ export function renderCombinedFooter( opts: { sharedOnly?: boolean; refs?: ChangelogRefsMode; demoteScopes?: readonly string[] } = {}, ): string { const deduped = dedupe(collectAttributed(versionOutput, !!opts.sharedOnly)); - if (deduped.length === 0) return ''; + if (deduped.length === 0) { + return ''; + } const n = deduped.length; const summary = opts.sharedOnly ? `Show project-wide changes (${n} ${n === 1 ? 'change' : 'changes'})` diff --git a/packages/release/src/standing-pr/notes-region.ts b/packages/release/src/standing-pr/notes-region.ts index 9883a69c..faa0af32 100644 --- a/packages/release/src/standing-pr/notes-region.ts +++ b/packages/release/src/standing-pr/notes-region.ts @@ -24,7 +24,9 @@ const NOTES_TRUNCATION_MARKER = '\n\n…(truncated)'; /** Trim one package's notes to {@link MAX_NOTES_CHARS_PER_PACKAGE}, on a line boundary where possible. */ export function truncatePackageNotes(notes: string): string { - if (notes.length <= MAX_NOTES_CHARS_PER_PACKAGE) return notes; + if (notes.length <= MAX_NOTES_CHARS_PER_PACKAGE) { + return notes; + } const budget = MAX_NOTES_CHARS_PER_PACKAGE - NOTES_TRUNCATION_MARKER.length; const slice = notes.slice(0, budget); const lastNewline = slice.lastIndexOf('\n'); @@ -35,7 +37,9 @@ export function truncatePackageNotes(notes: string): string { /** Render the editable region for the given per-package notes, with keyed markers per package. */ export function renderNotesRegion(notesByPackage: Record): string { const packages = Object.keys(notesByPackage).sort(); - if (packages.length === 0) return ''; + if (packages.length === 0) { + return ''; + } const lines: string[] = [REGION_HEADING, '', REGION_HINT, '']; for (const pkg of packages) { @@ -49,7 +53,9 @@ export function extractNotesRegions(body: string, packageNames: string[]): Recor const result: Record = {}; for (const pkg of packageNames) { const region = extractNotesRegion(body, pkg); - if (region !== undefined && region.length > 0) result[pkg] = region; + if (region !== undefined && region.length > 0) { + result[pkg] = region; + } } return result; } diff --git a/packages/release/src/standing-pr/selection-region.ts b/packages/release/src/standing-pr/selection-region.ts index 2a789f07..a11feee5 100644 --- a/packages/release/src/standing-pr/selection-region.ts +++ b/packages/release/src/standing-pr/selection-region.ts @@ -53,7 +53,9 @@ function channelOf(update: Update): ReleaseChannel { /** A release unit's channel is its primary's; an unchanged primary (no `primaryUpdate`) inherits it * from its members, which are single-channel by construction for `linked`/`fixed` groups. */ function unitChannel(unit: ReleaseUnit): ReleaseChannel { - if (unit.primaryUpdate) return channelOf(unit.primaryUpdate); + if (unit.primaryUpdate) { + return channelOf(unit.primaryUpdate); + } const child = unit.children[0]; return child ? channelOf(child) : 'stable'; } @@ -64,15 +66,20 @@ function byChannel(items: readonly T[], key: (item: T) => ReleaseChannel): Ma for (const item of items) { const k = key(item); const list = map.get(k); - if (list) list.push(item); - else map.set(k, [item]); + if (list) { + list.push(item); + } else { + map.set(k, [item]); + } } return map; } /** Push a section heading, ensuring a blank line separates it from the rows above. */ function pushSectionHeading(lines: string[], channel: ReleaseChannel): void { - if (lines[lines.length - 1] !== '') lines.push(''); + if (lines[lines.length - 1] !== '') { + lines.push(''); + } lines.push(SECTION_HEADING[channel], ''); } @@ -80,7 +87,9 @@ function pushSectionHeading(lines: string[], channel: ReleaseChannel): void { * on (npm publishes a prerelease under its preid, e.g. `1.0.0-next.1` → `next`), so a mixed PR shows * per-package maturity at a glance. */ function versionDisplay(update: Update): string { - if (channelOf(update) !== 'prerelease') return update.newVersion; + if (channelOf(update) !== 'prerelease') { + return update.newVersion; + } // SEAM: the per-package "graduate to stable" affordance — gated on a `graduate:` // label — attaches to prerelease rows here once that mechanism merges. This renders the channel // grouping + dist-tag only; it deliberately renders no affordance yet. @@ -149,9 +158,13 @@ export function computeHierarchy(updates: Update[], cfg: PrimaryConfig): Selecti // A changed primary carries its group on its update; an unchanged one is resolved from config. const groupOfPrimary = (p: string): string | undefined => { const own = updateByName.get(p)?.group; - if (own) return own; + if (own) { + return own; + } for (const [name, gc] of Object.entries(groups)) { - if (shouldMatchPackageTargets(p, gc.packages ?? [])) return name; + if (shouldMatchPackageTargets(p, gc.packages ?? [])) { + return name; + } } return undefined; }; @@ -161,12 +174,24 @@ export function computeHierarchy(updates: Update[], cfg: PrimaryConfig): Selecti const seen = new Set(); const result: Update[] = []; const add = (u: Update) => { - if (u.packageName === p || declaredPrimaryNames.has(u.packageName) || seen.has(u.packageName)) return; + if (u.packageName === p || declaredPrimaryNames.has(u.packageName) || seen.has(u.packageName)) { + return; + } seen.add(u.packageName); result.push(u); }; - if (group) for (const u of updates) if (u.group === group) add(u); - for (const u of updates) if (u.role === 'prerequisite' && u.prerequisiteOf?.includes(p)) add(u); + if (group) { + for (const u of updates) { + if (u.group === group) { + add(u); + } + } + } + for (const u of updates) { + if (u.role === 'prerequisite' && u.prerequisiteOf?.includes(p)) { + add(u); + } + } return result.sort((a, b) => a.packageName.localeCompare(b.packageName)); }; @@ -176,7 +201,9 @@ export function computeHierarchy(updates: Update[], cfg: PrimaryConfig): Selecti for (const primaryName of [...declaredPrimaryNames].sort((a, b) => a.localeCompare(b))) { const children = childrenOf(primaryName); const primaryUpdate = updateByName.get(primaryName); - if (!primaryUpdate && children.length === 0) continue; // declared but nothing in its unit changed + if (!primaryUpdate && children.length === 0) { + continue; // declared but nothing in its unit changed + } const group = groupOfPrimary(primaryName); const groupSync = group ? groups[group]?.sync : undefined; units.push({ primaryName, primaryUpdate, children, groupSync }); @@ -208,12 +235,24 @@ export function cascadeDeselection(hierarchy: SelectionHierarchy, deselected: Re // one). Escalate it to holding its whole unit: a package a maintainer held back must never silently // ship, and holding the unit keeps the render coherent (the primary shows unticked). for (const [child, owners] of hierarchy.childOwners) { - if (deselected.has(child)) for (const owner of owners) deselectedPrimaries.add(owner); + if (deselected.has(child)) { + for (const owner of owners) { + deselectedPrimaries.add(owner); + } + } + } + for (const p of deselectedPrimaries) { + effective.add(p); + } + for (const orphan of hierarchy.orphans) { + if (deselected.has(orphan.packageName)) { + effective.add(orphan.packageName); + } } - for (const p of deselectedPrimaries) effective.add(p); - for (const orphan of hierarchy.orphans) if (deselected.has(orphan.packageName)) effective.add(orphan.packageName); for (const [child, owners] of hierarchy.childOwners) { - if (owners.every((o) => deselectedPrimaries.has(o))) effective.add(child); + if (owners.every((o) => deselectedPrimaries.has(o))) { + effective.add(child); + } } return effective; } @@ -295,9 +334,13 @@ function attachChangelog( heldBack: boolean, indent: string, ): void { - if (!rowChangelog) return; + if (!rowChangelog) { + return; + } const block = rowChangelog(packageNames, heldBack, indent); - if (block) lines.push(block); + if (block) { + lines.push(block); + } } /** @@ -333,7 +376,9 @@ function attachChannelToggle( indent: string, channel: ChannelToggleConfig | undefined, ): void { - if (!channel || !selected) return; + if (!channel || !selected) { + return; + } const name = update.packageName; if (channelOf(update) === 'stable') { const preid = channel.prereleaseIdentifier ?? 'next'; @@ -401,7 +446,9 @@ function renderFlat( const channels = CHANNEL_ORDER.filter((c) => (updatesByChannel.get(c)?.length ?? 0) > 0); const showHeadings = channels.length > 1; for (const channel of channels) { - if (showHeadings) pushSectionHeading(lines, channel); + if (showHeadings) { + pushSectionHeading(lines, channel); + } renderFlatSection(lines, updatesByChannel.get(channel) ?? [], selected, rowChangelog, channelToggle); } } @@ -419,13 +466,17 @@ function renderUnit( const heldBack = !selected(unit.primaryName); // The primary's channel toggle shifts the whole unit (the wiring expands the scope to its group); // an unchanged primary anchoring the unit has no version to shift, so it gets none. - if (unit.primaryUpdate) attachChannelToggle(lines, unit.primaryUpdate, !heldBack, ' ', channel); + if (unit.primaryUpdate) { + attachChannelToggle(lines, unit.primaryUpdate, !heldBack, ' ', channel); + } if (streamlined) { if (unit.children.length > 0) { // Children inside a collapsed pane as plain bullets — a blank line after lets GitHub // render the nested markdown list. lines.push(`
ships ${unit.children.length} ${unitWord(unit)}`, ''); - for (const child of unit.children) lines.push(childBullet(child, unit.groupSync)); + for (const child of unit.children) { + lines.push(childBullet(child, unit.groupSync)); + } lines.push('
'); } // The streamlined unit ships primary + its members together (coupled or bundled), so its changelog @@ -479,12 +530,16 @@ function renderHierarchical( ); const showHeadings = channels.length > 1; for (const channel of channels) { - if (showHeadings) pushSectionHeading(lines, channel); - for (const unit of unitsByChannel.get(channel) ?? []) + if (showHeadings) { + pushSectionHeading(lines, channel); + } + for (const unit of unitsByChannel.get(channel) ?? []) { renderUnit(lines, unit, selected, streamlined, rowChangelog, channelToggle); + } // Orphans stay flat top-level checkboxes (fail-safe), grouped under their own channel. - for (const orphan of orphansByChannel.get(channel) ?? []) + for (const orphan of orphansByChannel.get(channel) ?? []) { renderOrphan(lines, orphan, selected, rowChangelog, channelToggle); + } } } @@ -532,19 +587,27 @@ export function renderSelectionRegion( */ export function extractSelection(body: string): { deselected: string[] } | undefined { const region = extractSelectionRegion(body); - if (region === undefined) return undefined; + if (region === undefined) { + return undefined; + } const deselected: string[] = []; for (const line of region.split('\n')) { const markerStart = line.indexOf('', nameStart); - if (nameEnd === -1) continue; + if (nameEnd === -1) { + continue; + } const name = line.slice(nameStart, nameEnd); // A row is selected unless its checkbox glyph is explicitly empty. `includes('[ ]')` is a fixed // substring check on the row text before the marker — no prose parsing, no backtracking regex. - if (line.slice(0, markerStart).includes('[ ]')) deselected.push(name); + if (line.slice(0, markerStart).includes('[ ]')) { + deselected.push(name); + } } return { deselected }; } @@ -556,10 +619,14 @@ function markerHits(region: string, marker: string): Array<{ name: string; ticke const hits: Array<{ name: string; ticked: boolean; unticked: boolean }> = []; for (const line of region.split('\n')) { const markerStart = line.indexOf(open); - if (markerStart === -1) continue; + if (markerStart === -1) { + continue; + } const nameStart = markerStart + open.length; const nameEnd = line.indexOf(' -->', nameStart); - if (nameEnd === -1) continue; + if (nameEnd === -1) { + continue; + } const before = line.slice(0, markerStart); hits.push({ name: line.slice(nameStart, nameEnd), @@ -578,7 +645,9 @@ function markerHits(region: string, marker: string): Array<{ name: string; ticke */ export function extractChannelSelection(body: string): { prereleased: string[]; graduated: string[] } { const region = extractSelectionRegion(body); - if (region === undefined) return { prereleased: [], graduated: [] }; + if (region === undefined) { + return { prereleased: [], graduated: [] }; + } const heldBack = new Set( markerHits(region, 'rk-sel') .filter((h) => h.unticked) @@ -634,7 +703,9 @@ export function selectionWarnings( ): SelectionWarning[] { const warnings: SelectionWarning[] = []; for (const update of updates) { - if (!deselected.has(update.packageName)) continue; + if (!deselected.has(update.packageName)) { + continue; + } if (update.group && groups[update.group]?.sync === 'independent') { // Only a *partial* hold-back splits the group. When a primary toggle cascades the whole // independent group out together, every member is deselected — coherent, so don't warn. diff --git a/packages/release/src/standing-pr/standing-pr.ts b/packages/release/src/standing-pr/standing-pr.ts index ef851697..89bc166e 100644 --- a/packages/release/src/standing-pr/standing-pr.ts +++ b/packages/release/src/standing-pr/standing-pr.ts @@ -196,9 +196,13 @@ async function getHeadSha(cwd: string): Promise { * idempotent and the workflow guards out the bot's own edits, so an `edited` re-trigger is safe. */ function isStandingPrEventRun(): boolean { - if (process.env.GITHUB_EVENT_NAME !== 'pull_request') return false; + if (process.env.GITHUB_EVENT_NAME !== 'pull_request') { + return false; + } const eventPath = process.env.GITHUB_EVENT_PATH; - if (!eventPath) return false; + if (!eventPath) { + return false; + } try { const event = JSON.parse(fs.readFileSync(eventPath, 'utf-8')) as { action?: string }; return event.action === 'labeled' || event.action === 'unlabeled' || event.action === 'edited'; @@ -245,7 +249,9 @@ async function commitAndForcePush(branch: string, cwd: string): Promise { * state can't pollute the notes commit. */ async function commitNotesFiles(files: string[], versionOutput: VersionOutput, cwd: string): Promise { - if (files.length === 0) return; + if (files.length === 0) { + return; + } const git = createGitCli(); @@ -307,7 +313,9 @@ async function commitNotesFiles(files: string[], versionOutput: VersionOutput, c * publish pipeline's `--tags` push will publish whatever tags we managed to create. */ export async function createReleaseTags(tags: string[], cwd: string): Promise { - if (tags.length === 0) return; + if (tags.length === 0) { + return; + } const git = createGitCli(); @@ -371,7 +379,9 @@ async function deleteReleaseBranch(releaseBranch: string, cwd: string): Promise< export const STANDING_PR_BODY_CAP = 64000; function truncateAtLineBoundary(text: string, maxChars: number): string { - if (text.length <= maxChars) return text; + if (text.length <= maxChars) { + return text; + } const slice = text.slice(0, maxChars); const lastNewline = slice.lastIndexOf('\n'); return lastNewline > 0 ? slice.slice(0, lastNewline) : slice; @@ -423,7 +433,9 @@ function renderPrBody(versionOutput: VersionOutput, options: RenderPrBodyOptions // One-line release headline under the heading — a neutral "what will publish" scan before // the (conditional) supersede alert and the package list. Absent for sync (its own display leads). - if (summaryLine) lines.push(summaryLine, ''); + if (summaryLine) { + lines.push(summaryLine, ''); + } // While a prior release remains partially published, lead with the supersede warning so the // maintainer sees the retry-vs-supersede choice before the package list. @@ -434,7 +446,9 @@ function renderPrBody(versionOutput: VersionOutput, options: RenderPrBodyOptions // The scannable current → next table, opt-in via ci.standingPr.summaryTable, sits above the // selection region (and after any supersede warning) — it complements the checkbox list, which // GitHub renders interactively only as a list, never in a table cell. - if (summaryTable) lines.push(summaryTable, ''); + if (summaryTable) { + lines.push(summaryTable, ''); + } // The root lockstep bump (sync mode) is never published — keep it out of the package list. const updates = publishableUpdates(versionOutput); @@ -463,11 +477,17 @@ function renderPrBody(versionOutput: VersionOutput, options: RenderPrBodyOptions } } - if (footerSection) lines.push('', footerSection, ''); - if (extraNotice) lines.push('', extraNotice, ''); + if (footerSection) { + lines.push('', footerSection, ''); + } + if (extraNotice) { + lines.push('', extraNotice, ''); + } // The editable release-notes region (opt-in via the preview-notes label) sits below the changelog // and above the merge instructions, so a reviewer reads/edits it in the natural place. - if (notesSection) lines.push('', notesSection, ''); + if (notesSection) { + lines.push('', notesSection, ''); + } lines.push('---', '> Merge this PR to publish. The release will be triggered automatically.'); lines.push('', ATTRIBUTION_FOOTER); return lines.join('\n'); @@ -480,19 +500,25 @@ function renderPrBody(versionOutput: VersionOutput, options: RenderPrBodyOptions // Level 0 — the full body: per-row changelogs (non-sync) plus the combined deduped footer. const fullSelection = renderSelectionBlock?.(true); const full = build(fullSelection, footer, undefined, notesSection); - if (full.length <= STANDING_PR_BODY_CAP) return full; + if (full.length <= STANDING_PR_BODY_CAP) { + return full; + } if (renderSelectionBlock) { // Non-sync: always keep the rows (the PR must stay usable); shed changelog volume in stages. // Level 1 — keep the per-row changelogs, drop the redundant deduped footer. if (footer) { const body = build(fullSelection, '', undefined, notesSection); - if (body.length <= STANDING_PR_BODY_CAP) return body; + if (body.length <= STANDING_PR_BODY_CAP) { + return body; + } } // Level 2 — drop the per-row changelogs too (bare rows) and point reviewers at CHANGELOG.md. const bareSelection = renderSelectionBlock(false); const body = build(bareSelection, '', TRUNCATION_NOTICE, notesSection); - if (body.length <= STANDING_PR_BODY_CAP) return body; + if (body.length <= STANDING_PR_BODY_CAP) { + return body; + } // Level 3 — even the notice overflows (thousands of rows): bare rows only. return build(bareSelection, '', undefined, notesSection); } @@ -502,7 +528,9 @@ function renderPrBody(versionOutput: VersionOutput, options: RenderPrBodyOptions // 8 = 4 (\n\n around the footer) + 2 (\n\n before the notice) + 2 safety margin. const room = STANDING_PR_BODY_CAP - build(undefined, '', undefined, notesSection).length - TRUNCATION_NOTICE.length - 8; - if (room <= 0) return build(undefined, '', undefined, notesSection); + if (room <= 0) { + return build(undefined, '', undefined, notesSection); + } return build( undefined, `${truncateAtLineBoundary(footer, room)}\n\n${TRUNCATION_NOTICE}`, @@ -514,12 +542,16 @@ function renderPrBody(versionOutput: VersionOutput, options: RenderPrBodyOptions }; const primary = shed(notesRegion); - if (primary.length <= STANDING_PR_BODY_CAP || !notesRegion) return primary; + if (primary.length <= STANDING_PR_BODY_CAP || !notesRegion) { + return primary; + } // The trimmed changelog still overflows with the notes present — the notes are (part of) the excess. // Collapse them to a short notice, then drop them outright, so an inflated notes region can never // wedge the PR at GitHub's 422. const collapsed = shed(NOTES_OMITTED_NOTICE); - if (collapsed.length <= STANDING_PR_BODY_CAP) return collapsed; + if (collapsed.length <= STANDING_PR_BODY_CAP) { + return collapsed; + } return shed(undefined); } @@ -610,13 +642,19 @@ export async function fetchStandingPRSnapshot( ): Promise { const branch = ciConfig?.standingPr?.branch ?? 'release/next'; const pr = await forge.findStandingPR(branch); - if (!pr) return null; + if (!pr) { + return null; + } const comment = await findManifestComment(forge, pr.number); - if (!comment) return null; + if (!comment) { + return null; + } // findManifestComment falls back to a human-authored comment when no bot one exists; refuse to // surface a forged manifest in the preview, matching the publish path's author check. - if (!isBotComment(comment)) return null; + if (!isBotComment(comment)) { + return null; + } let manifest: StandingPRManifest; try { @@ -737,14 +775,18 @@ function graduatedPackageNames(versionOutput: VersionOutput): string[] { ); const names = new Set(); for (const u of updates) { - if (u.action === 'graduated' || (u.group && graduatedGroups.has(u.group))) names.add(u.packageName); + if (u.action === 'graduated' || (u.group && graduatedGroups.has(u.group))) { + names.add(u.packageName); + } } return [...names].sort(); } function resolveStandingPrLabelOverrides(prLabels: string[], ciConfig: CIConfig | undefined): StandingPrOverrides { // Return a fresh object rather than a shared constant — callers may mutate `conflicts`. - if (!prLabels || prLabels.length === 0) return { conflicts: [] }; + if (!prLabels || prLabels.length === 0) { + return { conflicts: [] }; + } const labels = ciConfig?.labels ?? DEFAULT_LABELS; const scopeLabels = ciConfig?.scopeLabels ?? {}; const conflicts: string[] = []; @@ -758,9 +800,13 @@ function resolveStandingPrLabelOverrides(prLabels: string[], ciConfig: CIConfig ].filter(Boolean) as string[]; if (bumpsPresent.length > 1) { conflicts.push(`Conflicting bump labels on standing PR (${bumpsPresent.join(', ')}) — remove all but one`); - } else if (prLabels.includes(labels.major)) bump = 'major'; - else if (prLabels.includes(labels.minor)) bump = 'minor'; - else if (prLabels.includes(labels.patch)) bump = 'patch'; + } else if (prLabels.includes(labels.major)) { + bump = 'major'; + } else if (prLabels.includes(labels.minor)) { + bump = 'minor'; + } else if (prLabels.includes(labels.patch)) { + bump = 'patch'; + } // Graduate / prerelease release-type const hasGraduate = prLabels.includes(labels.graduate); @@ -770,8 +816,12 @@ function resolveStandingPrLabelOverrides(prLabels: string[], ciConfig: CIConfig if (hasGraduate && hasPrerelease) { conflicts.push(`Conflicting release-type labels on standing PR (${labels.graduate} and ${labels.prerelease})`); } else { - if (hasGraduate) stable = true; - if (hasPrerelease) prerelease = true; + if (hasGraduate) { + stable = true; + } + if (hasPrerelease) { + prerelease = true; + } } // Per-package graduation: each `graduate:` label graduates that one prerelease to @@ -985,7 +1035,9 @@ export async function runStandingPRUpdate(options: StandingPROptions): Promise u.packageName === name); const groupSync = update?.group ? groups[update.group]?.sync : undefined; if (groupSync === 'fixed' || groupSync === 'linked') { @@ -1242,7 +1310,13 @@ export async function runStandingPRUpdate(options: StandingPROptions): Promise u.packageName === name)?.group; - if (grp) for (const u of dryUpdates) if (u.group === grp) out.add(u.packageName); + if (grp) { + for (const u of dryUpdates) { + if (u.group === grp) { + out.add(u.packageName); + } + } + } } return [...out]; }; @@ -1416,7 +1490,9 @@ export async function runStandingPRUpdate(options: StandingPROptions): Promise 0) notesRegion = renderNotesRegion(merged); + if (Object.keys(merged).length > 0) { + notesRegion = renderNotesRegion(merged); + } } // The interactive selection region — the package list for non-sync releases. Rendered from the @@ -1428,7 +1504,9 @@ export async function runStandingPRUpdate(options: StandingPROptions): Promise string) | undefined; if (versionOutputDry.strategy !== 'sync') { const warnings = selectionWarnings(dryUpdates, effectiveDeselected, groups); - for (const w of warnings) warn(`Selection: ${w.reason}`); + for (const w of warnings) { + warn(`Selection: ${w.reason}`); + } const warningSuffix = warnings.length > 0 ? `\n\n${warnings.map((w) => `> ⚠️ ${w.reason}`).join('\n')}` : ''; // Per-row changelogs are sourced from the DRY changelogs (the full changed set) so a held-back // row still shows its greyed changelog — the write output omits held-back packages entirely. @@ -1471,7 +1549,9 @@ export async function runStandingPRUpdate(options: StandingPROptions): Promise { const prev = baselineOf(update); - if (!prev || !semver.valid(update.newVersion)) return null; + if (!prev || !semver.valid(update.newVersion)) { + return null; + } return semver.diff(prev, update.newVersion); } @@ -54,9 +58,15 @@ function isMajorBump(update: Update): boolean { * the resolved action/channel, else a dash. */ function bumpLabel(update: Update): string { const diff = bumpDiff(update); - if (diff) return diff; - if (update.action === 'first-release') return 'first release'; - if (channelOf(update) === 'prerelease') return 'prerelease'; + if (diff) { + return diff; + } + if (update.action === 'first-release') { + return 'first release'; + } + if (channelOf(update) === 'prerelease') { + return 'prerelease'; + } return '—'; } @@ -83,11 +93,17 @@ export function renderReleaseSummaryLine(versionOutput: VersionOutput, input: Re const major = updates.filter(isMajorBump).length; const buckets: string[] = []; - if (stable > 0) buckets.push(plural(stable, 'stable', 'stable')); - if (prerelease > 0) buckets.push(plural(prerelease, 'prerelease', 'prerelease')); + if (stable > 0) { + buckets.push(plural(stable, 'stable', 'stable')); + } + if (prerelease > 0) { + buckets.push(plural(prerelease, 'prerelease', 'prerelease')); + } const segments: string[] = []; - if (buckets.length > 1) segments.push(...buckets); + if (buckets.length > 1) { + segments.push(...buckets); + } segments.push(plural(changes, 'change')); const majorClause = major > 0 ? `⚠️ ${plural(major, 'major bump')}.` : 'No major bumps.'; @@ -102,7 +118,9 @@ export function renderReleaseSummaryLine(versionOutput: VersionOutput, input: Re */ export function renderVersionSummaryTable(versionOutput: VersionOutput): string { const updates = publishableUpdates(versionOutput); - if (updates.length === 0) return ''; + if (updates.length === 0) { + return ''; + } const lines: string[] = [ `
Version summary (${plural(updates.length, 'package')})`, diff --git a/packages/release/test/unit/authorization.spec.ts b/packages/release/test/unit/authorization.spec.ts index 248e648e..a74a7fc5 100644 --- a/packages/release/test/unit/authorization.spec.ts +++ b/packages/release/test/unit/authorization.spec.ts @@ -68,8 +68,11 @@ describe('getEventActor', () => { tmpFile = `${process.env.TMPDIR ?? '/tmp'}/rk-event-${process.pid}.json`; }); afterEach(() => { - if (ORIG === undefined) delete process.env.GITHUB_EVENT_PATH; - else process.env.GITHUB_EVENT_PATH = ORIG; + if (ORIG === undefined) { + delete process.env.GITHUB_EVENT_PATH; + } else { + process.env.GITHUB_EVENT_PATH = ORIG; + } try { fs.unlinkSync(tmpFile); } catch {} diff --git a/packages/release/test/unit/commands/emitResult.spec.ts b/packages/release/test/unit/commands/emitResult.spec.ts index 67a6c641..c0a6d869 100644 --- a/packages/release/test/unit/commands/emitResult.spec.ts +++ b/packages/release/test/unit/commands/emitResult.spec.ts @@ -9,7 +9,9 @@ describe('emitResult', () => { const dirs: string[] = []; afterEach(() => { - for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); + for (const dir of dirs) { + rmSync(dir, { recursive: true, force: true }); + } dirs.length = 0; vi.restoreAllMocks(); }); diff --git a/packages/release/test/unit/failure-report-post.spec.ts b/packages/release/test/unit/failure-report-post.spec.ts index b75b7214..7cbcf71d 100644 --- a/packages/release/test/unit/failure-report-post.spec.ts +++ b/packages/release/test/unit/failure-report-post.spec.ts @@ -172,8 +172,11 @@ describe('detectUnresolvedFailure env hygiene', () => { savedSummary = process.env.GITHUB_STEP_SUMMARY; }); afterEach(() => { - if (savedSummary === undefined) delete process.env.GITHUB_STEP_SUMMARY; - else process.env.GITHUB_STEP_SUMMARY = savedSummary; + if (savedSummary === undefined) { + delete process.env.GITHUB_STEP_SUMMARY; + } else { + process.env.GITHUB_STEP_SUMMARY = savedSummary; + } }); it('should act as a placeholder to anchor the env reset hooks', () => { expect(true).toBe(true); diff --git a/packages/release/test/unit/gate.spec.ts b/packages/release/test/unit/gate.spec.ts index e9cfb9e3..90da54d5 100644 --- a/packages/release/test/unit/gate.spec.ts +++ b/packages/release/test/unit/gate.spec.ts @@ -34,8 +34,12 @@ vi.mock('../../src/github.js', () => ({ vi.mock('../../src/release.js', () => ({ resolveScopeToTarget: (scopeName: string, scopeLabels: Record) => { const prefixed = `scope:${scopeName}`; - if (scopeLabels[prefixed]) return scopeLabels[prefixed]; - if (scopeLabels[scopeName]) return scopeLabels[scopeName]; + if (scopeLabels[prefixed]) { + return scopeLabels[prefixed]; + } + if (scopeLabels[scopeName]) { + return scopeLabels[scopeName]; + } throw new Error(`Scope "${scopeName}" not found`); }, runRelease: vi.fn(), @@ -47,7 +51,9 @@ vi.mock('../../src/git.js', () => ({ const repo = process.env.GITHUB_REPOSITORY; const sha = process.env.GITHUB_SHA; const token = process.env.GITHUB_TOKEN; - if (!repo) return null; + if (!repo) { + return null; + } const [owner, repoName] = repo.split('/'); return { owner, repo: repoName ?? '', sha: sha ?? null, token: token ?? null }; }, diff --git a/packages/release/test/unit/refresh.spec.ts b/packages/release/test/unit/refresh.spec.ts index ec7711c5..9a0eacae 100644 --- a/packages/release/test/unit/refresh.spec.ts +++ b/packages/release/test/unit/refresh.spec.ts @@ -136,7 +136,9 @@ describe('runRefreshAfterRelease', () => { it('should continue refreshing remaining PRs when one runPreview throws', async () => { mockForgeFor.mockReturnValue(fakeForge([pr(10), pr(11), pr(12)])); mockRunPreview.mockImplementation(async (opts: { pr: string }) => { - if (opts.pr === '11') throw new Error('boom'); + if (opts.pr === '11') { + throw new Error('boom'); + } }); await runRefreshAfterRelease({ projectDir: '/p' }); diff --git a/packages/release/test/unit/standing-pr.spec.ts b/packages/release/test/unit/standing-pr.spec.ts index 7b827bbf..8e7f0285 100644 --- a/packages/release/test/unit/standing-pr.spec.ts +++ b/packages/release/test/unit/standing-pr.spec.ts @@ -42,7 +42,9 @@ class ControllableGit extends FakeGit { } if (opts.format === '%H') { // createReleaseTags resolves an existing tag's commit — default to HEAD ("tag at HEAD"). - if (this.tagResolveThrows) throw new Error('cannot resolve tag'); + if (this.tagResolveThrows) { + throw new Error('cannot resolve tag'); + } return `${this.tagCommitSha ?? (await this.headSha(opts.cwd))}\n`; } return super.log(opts); @@ -1096,10 +1098,16 @@ describe('runStandingPRUpdate', () => { // Did NOT noop on the first guard — the label-triggered run proceeded to update the PR. expect(result.action).not.toBe('noop'); } finally { - if (prevName === undefined) delete process.env.GITHUB_EVENT_NAME; - else process.env.GITHUB_EVENT_NAME = prevName; - if (prevPath === undefined) delete process.env.GITHUB_EVENT_PATH; - else process.env.GITHUB_EVENT_PATH = prevPath; + if (prevName === undefined) { + delete process.env.GITHUB_EVENT_NAME; + } else { + process.env.GITHUB_EVENT_NAME = prevName; + } + if (prevPath === undefined) { + delete process.env.GITHUB_EVENT_PATH; + } else { + process.env.GITHUB_EVENT_PATH = prevPath; + } } }); @@ -2360,7 +2368,9 @@ describe('runStandingPRUpdate', () => { const bodies = forge.updatedPullRequests .map((u) => u.changes.body) .filter((b): b is string => typeof b === 'string'); - for (const b of bodies) expect(b.length).toBeLessThanOrEqual(STANDING_PR_BODY_CAP); + for (const b of bodies) { + expect(b.length).toBeLessThanOrEqual(STANDING_PR_BODY_CAP); + } }); }); }); diff --git a/packages/version/src/core/baselineResolver.ts b/packages/version/src/core/baselineResolver.ts index 76ea2e96..04572912 100644 --- a/packages/version/src/core/baselineResolver.ts +++ b/packages/version/src/core/baselineResolver.ts @@ -211,9 +211,15 @@ export class BaselineResolver { * the nearest reachable tag rather than flooding with full history. */ async sharedFloor(perPackageRange: string): Promise { - if (this.opts.baseRef) return perPackageRange; - if (this.opts.sharedChangelogFloor === 'sinceLastRelease') return this.nearestReachableRange(); - if (perPackageRange !== 'HEAD') return perPackageRange; + if (this.opts.baseRef) { + return perPackageRange; + } + if (this.opts.sharedChangelogFloor === 'sinceLastRelease') { + return this.nearestReachableRange(); + } + if (perPackageRange !== 'HEAD') { + return perPackageRange; + } return this.nearestReachableRange(); } diff --git a/packages/version/src/core/groupResolution.ts b/packages/version/src/core/groupResolution.ts index 37dc7fa9..6cd4e342 100644 --- a/packages/version/src/core/groupResolution.ts +++ b/packages/version/src/core/groupResolution.ts @@ -107,7 +107,9 @@ export function resolveGroups(config: Config, packages: Package[]): GroupResolut const name = pkg.packageJson.name; // The implicit sync group is "every package" by definition — don't route it through glob // matching, where a bare `*` would fail to cross the `/` in scoped names. - if (!matchesAll && !shouldMatchPackageTargets(name, def.packages)) continue; + if (!matchesAll && !shouldMatchPackageTargets(name, def.packages)) { + continue; + } const existing = memberOf.get(name); if (existing) { @@ -182,13 +184,17 @@ export function expandTargetsForAtomicGroups( resolution: GroupResolution, targets: string[], ): { targets: string[]; expandedGroups: string[] } { - if (targets.length === 0) return { targets, expandedGroups: [] }; + if (targets.length === 0) { + return { targets, expandedGroups: [] }; + } const expanded = new Set(targets); const expandedGroups: string[] = []; for (const group of resolution.groups) { - if (group.sync === 'linked') continue; + if (group.sync === 'linked') { + continue; + } const someTargeted = group.members.some((m) => shouldMatchPackageTargets(m.packageJson.name, targets)); const allTargeted = group.members.every((m) => shouldMatchPackageTargets(m.packageJson.name, targets)); diff --git a/packages/version/src/core/groupStrategy.ts b/packages/version/src/core/groupStrategy.ts index 5200685a..8d69cd33 100644 --- a/packages/version/src/core/groupStrategy.ts +++ b/packages/version/src/core/groupStrategy.ts @@ -84,7 +84,9 @@ interface MemberPlan { } function cleanBaseline(version: string | undefined): string { - if (!version) return '0.0.0'; + if (!version) { + return '0.0.0'; + } // semver.coerce() strips the prerelease (1.0.0-next.0 -> 1.0.0), which would graduate a linked or // fixed group off its prerelease line on every increment. Parse prerelease-preserving first: // clean() for a bare semver, then coerce({ includePrerelease }) only to pull it from a tag prefix. @@ -115,7 +117,9 @@ async function planMember( }); usedPackageSpecificTag = !!latestTag; } - if (!latestTag) latestTag = globalTag; + if (!latestTag) { + latestTag = globalTag; + } const ownNext = await calculateVersion(config, { latestTag, @@ -138,7 +142,9 @@ async function planMember( /** Derive the bump magnitude a member earned by comparing its computed next version to baseline. */ function memberBumpRank(plan: MemberPlan): number { - if (!plan.changed) return -1; + if (!plan.changed) { + return -1; + } const diff = semver.diff(plan.baseline, plan.ownNext); if (!diff) { // Same version family (e.g. prerelease increment) — treat as a patch-level change so the @@ -165,9 +171,13 @@ interface GroupComputation { * `release:graduate` path → every group graduates) or some member falls inside that scope. */ function isGroupGraduating(group: ResolvedGroup, config: Config): boolean { - if (!config.stableOnly) return false; + if (!config.stableOnly) { + return false; + } const scope = config.graduateScope; - if (!scope || scope.length === 0) return true; + if (!scope || scope.length === 0) { + return true; + } return group.members.some((m) => shouldMatchPackageTargets(m.packageJson.name, scope)); } @@ -181,9 +191,13 @@ function isGroupGraduating(group: ResolvedGroup, config: Config): boolean { * happens to land on a prerelease line yet. */ function isGroupPrereleasing(group: ResolvedGroup, config: Config): boolean { - if (!config.isPrerelease) return false; + if (!config.isPrerelease) { + return false; + } const scope = config.prereleaseScope; - if (!scope || scope.length === 0) return true; + if (!scope || scope.length === 0) { + return true; + } return group.members.some((m) => shouldMatchPackageTargets(m.packageJson.name, scope)); } @@ -298,12 +312,20 @@ function shouldProcess(pkg: Package, config: Config): boolean { function readRepoUrl(pkgDir: string): string | null { try { const pkgJsonPath = path.join(pkgDir, 'package.json'); - if (!fs.existsSync(pkgJsonPath)) return null; + if (!fs.existsSync(pkgJsonPath)) { + return null; + } const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); let url: string | undefined = typeof pkgJson.repository === 'string' ? pkgJson.repository : pkgJson.repository?.url; - if (!url) return null; - if (url.startsWith('git+')) url = url.slice(4); - if (url.endsWith('.git')) url = url.slice(0, -4); + if (!url) { + return null; + } + if (url.startsWith('git+')) { + url = url.slice(4); + } + if (url.endsWith('.git')) { + url = url.slice(0, -4); + } return url; } catch { return null; @@ -344,7 +366,9 @@ async function extractEntries( entries = await extractChangelogEntriesFromCommits(input.pkgDir, revisionRange); } catch (error) { // A strictReachable violation must abort the run, not degrade to a minimal entry. - if (error instanceof StrictReachableError) throw error; + if (error instanceof StrictReachableError) { + throw error; + } log(`Error extracting changelog entries: ${error instanceof Error ? error.message : String(error)}`, 'warning'); } if (entries.length === 0) { @@ -356,7 +380,9 @@ async function extractEntries( // swallows git failures and returns [] — so leave it unflagged and let the bump render a // visible block instead of vanishing as a no-change carry. const fallback: ChangelogEntry = { type: 'changed', description: `Update version to ${input.nextVersion}` }; - if (input.isCarry) fallback.synthetic = true; + if (input.isCarry) { + fallback.synthetic = true; + } entries = [fallback]; } return { entries, revisionRange, previousVersion }; @@ -380,7 +406,9 @@ async function releaseGroup( for (const plan of releasing) { const pkg = plan.pkg; const name = pkg.packageJson.name; - if (!shouldProcess(pkg, config)) continue; + if (!shouldProcess(pkg, config)) { + continue; + } // Independent members release on their own commit-driven line; fixed/linked members adopt the // shared group version. @@ -512,7 +540,9 @@ export function createGroupStrategy(config: Config): (packages: PackagesWithRoot for (const group of resolution.groups) { const members = group.members.filter(targetFilter).filter((p) => shouldProcess(p, config)); - if (members.length === 0) continue; + if (members.length === 0) { + continue; + } const plans = await Promise.all(members.map((pkg) => planMember(config, pkg, formattedPrefix, globalTag))); const computation = computeGroup(group, plans, config); @@ -564,9 +594,13 @@ export function createGroupStrategy(config: Config): (packages: PackagesWithRoot // Ungrouped packages version independently — same per-member machinery, no shared version. for (const pkg of resolution.ungrouped) { - if (!targetFilter(pkg) || !shouldProcess(pkg, config)) continue; + if (!targetFilter(pkg) || !shouldProcess(pkg, config)) { + continue; + } const plan = await planMember(config, pkg, formattedPrefix, globalTag); - if (!plan.changed) continue; + if (!plan.changed) { + continue; + } const name = pkg.packageJson.name; const packageJsonPath = path.join(pkg.dir, 'package.json'); diff --git a/packages/version/src/core/prerequisiteScope.ts b/packages/version/src/core/prerequisiteScope.ts index 0bab7370..ec58d854 100644 --- a/packages/version/src/core/prerequisiteScope.ts +++ b/packages/version/src/core/prerequisiteScope.ts @@ -41,14 +41,20 @@ export function resolvePrerequisiteTargets( const overrideSet = new Set(explicitNames); for (const name of explicitNames) { const group = resolution.groupOf(name); - if (group) for (const member of group.members) overrideSet.add(member.packageJson.name); + if (group) { + for (const member of group.members) { + overrideSet.add(member.packageJson.name); + } + } } // Detect which transitive dependencies of the expanded targets actually changed — only those // become prerequisites (an unchanged dependency needs no release). const candidateDeps = new Set(); for (const name of overrideSet) { - for (const dep of graph.transitiveDependencies(name)) candidateDeps.add(dep); + for (const dep of graph.transitiveDependencies(name)) { + candidateDeps.add(dep); + } } const changed = [...candidateDeps].filter((name) => isChanged(name)); diff --git a/packages/version/src/core/versionCalculator.ts b/packages/version/src/core/versionCalculator.ts index 97c1b69e..3b636e55 100644 --- a/packages/version/src/core/versionCalculator.ts +++ b/packages/version/src/core/versionCalculator.ts @@ -117,7 +117,9 @@ export function applyOverrideScope( * no-tag axis, so they warn like the default.) */ function guardFirstReleaseBump(config: Config, name: string | undefined, currentVer: string, type: ReleaseType): void { - if (config.allowFirstBump) return; + if (config.allowFirstBump) { + return; + } const pkg = name || 'package'; const bumped = bumpVersion(currentVer, type); const message = @@ -127,7 +129,9 @@ function guardFirstReleaseBump(config: Config, name: string | undefined, current if ((config.mismatchStrategy ?? 'warn') === 'error') { throw new Error(`First-release version overshoot: ${message}`); } - if (config.mismatchStrategy === 'ignore') return; + if (config.mismatchStrategy === 'ignore') { + return; + } log(message, 'warning'); } @@ -190,7 +194,9 @@ async function calculateVersionInner(config: Config, options: VersionOptions): P } // Fallback to hardcoded patterns for backward compatibility - if (!packageName) return escapeRegExp(prefix); + if (!packageName) { + return escapeRegExp(prefix); + } const sanitized = sanitizePackageName(packageName); const escapedRaw = escapeRegExp(`${packageName}@${prefix}`); const escapedDash = escapeRegExp(`${sanitized}-${prefix}`); diff --git a/packages/version/src/core/versionEngine.ts b/packages/version/src/core/versionEngine.ts index 9b587828..ce9a4655 100644 --- a/packages/version/src/core/versionEngine.ts +++ b/packages/version/src/core/versionEngine.ts @@ -50,15 +50,23 @@ export class VersionEngine { const effective: Config = { ...config }; if (runOptions) { - if (runOptions.dryRun) effective.dryRun = true; - if (runOptions.sync) effective.sync = true; - if (runOptions.bump) effective.type = runOptions.bump; + if (runOptions.dryRun) { + effective.dryRun = true; + } + if (runOptions.sync) { + effective.sync = true; + } + if (runOptions.bump) { + effective.type = runOptions.bump; + } if (runOptions.prerelease) { effective.prereleaseIdentifier = typeof runOptions.prerelease === 'string' ? runOptions.prerelease : effective.prereleaseIdentifier || 'next'; effective.isPrerelease = true; } - if (runOptions.stable) effective.stableOnly = true; + if (runOptions.stable) { + effective.stableOnly = true; + } // Per-package graduation: graduate only the named packages, leaving other prereleases on // their line. Sets stableOnly + graduateScope; applyOverrideScope clears stableOnly for any // package outside the scope. The global `stable` flag wins — when it's set we graduate ALL @@ -78,12 +86,24 @@ export class VersionEngine { effective.prereleaseIdentifier = effective.prereleaseIdentifier || 'next'; effective.prereleaseScope = runOptions.prereleaseScope; } - if (runOptions.allowFirstBump) effective.allowFirstBump = true; - if (runOptions.targets?.length) this.runtimeTargets = runOptions.targets; - if (runOptions.baseRef) effective.baseRef = runOptions.baseRef; - if (runOptions.overrideScope) effective.overrideScope = runOptions.overrideScope; - if (runOptions.includePrerequisites) this.includePrerequisites = true; - if (runOptions.exclude?.length) this.excludePackages = runOptions.exclude; + if (runOptions.allowFirstBump) { + effective.allowFirstBump = true; + } + if (runOptions.targets?.length) { + this.runtimeTargets = runOptions.targets; + } + if (runOptions.baseRef) { + effective.baseRef = runOptions.baseRef; + } + if (runOptions.overrideScope) { + effective.overrideScope = runOptions.overrideScope; + } + if (runOptions.includePrerequisites) { + this.includePrerequisites = true; + } + if (runOptions.exclude?.length) { + this.excludePackages = runOptions.exclude; + } } // Default values for required properties @@ -388,7 +408,9 @@ export class VersionEngine { tagTemplate: this.config.tagTemplate, packageSpecificTags: this.config.packageSpecificTags, }); - if (!tag) tag = await getLatestTag(formattedPrefix); + if (!tag) { + tag = await getLatestTag(formattedPrefix); + } } catch { // No tag resolvable — getCommitsLength falls back to git describe / full history. } @@ -411,7 +433,9 @@ export class VersionEngine { try { const pkg = parseCargoToml(cargoPath).package; const publish = pkg?.publish; - if (pkg?.name && publish !== false && !(Array.isArray(publish) && publish.length === 0)) return true; + if (pkg?.name && publish !== false && !(Array.isArray(publish) && publish.length === 0)) { + return true; + } } catch { // Unparseable Cargo.toml — fall through; don't claim publishable on a parse failure. } @@ -419,7 +443,9 @@ export class VersionEngine { const pubspecPath = path.join(packageDir, 'pubspec.yaml'); if (fs.existsSync(pubspecPath)) { try { - if (parsePubspec(pubspecPath).publish_to !== 'none') return true; + if (parsePubspec(pubspecPath).publish_to !== 'none') { + return true; + } } catch { // Unparseable pubspec — fall through. } @@ -487,9 +513,15 @@ export class VersionEngine { const namedExactly = new Set(this.config.packages ?? []); const beforeCount = mergedPackages.packages.length; mergedPackages.packages = mergedPackages.packages.filter((pkg) => { - if (!isPrivatePackageJson(pkg.packageJson, path.join(pkg.dir, 'package.json'))) return true; - if (namedExactly.has(pkg.packageJson.name)) return true; - if (this.isNativelyPublishable(pkg.dir)) return true; + if (!isPrivatePackageJson(pkg.packageJson, path.join(pkg.dir, 'package.json'))) { + return true; + } + if (namedExactly.has(pkg.packageJson.name)) { + return true; + } + if (this.isNativelyPublishable(pkg.dir)) { + return true; + } log(`Skipping private npm package ${pkg.packageJson.name}: package.json "private": true`, 'debug'); return false; }); diff --git a/packages/version/src/core/versionStrategies.ts b/packages/version/src/core/versionStrategies.ts index 836c565e..59d90f4c 100644 --- a/packages/version/src/core/versionStrategies.ts +++ b/packages/version/src/core/versionStrategies.ts @@ -95,7 +95,9 @@ function updateCargoFiles( updatePackageVersion(cargoTomlPath, version, dryRun); updatedFiles.push(cargoTomlPath); const lockPath = syncCargoLockfile(cargoTomlPath, dryRun); - if (lockPath && !updatedFiles.includes(lockPath)) updatedFiles.push(lockPath); + if (lockPath && !updatedFiles.includes(lockPath)) { + updatedFiles.push(lockPath); + } }; if (cargoPaths && cargoPaths.length > 0) { @@ -348,7 +350,9 @@ export function createSyncStrategy(config: Config): StrategyFunction { } } catch (error) { // A strictReachable violation must abort the run, not degrade to a minimal entry. - if (error instanceof StrictReachableError) throw error; + if (error instanceof StrictReachableError) { + throw error; + } log(`Error extracting changelog entries: ${error instanceof Error ? error.message : String(error)}`, 'warning'); changelogEntries = [{ type: 'changed', description: `Update version to ${nextVersion}` }]; } @@ -466,7 +470,9 @@ export function createSyncStrategy(config: Config): StrategyFunction { const baselineTag = config.baselineTagTemplate ? formatTag(nextVersion, formattedPrefix, mainPkgName, config.baselineTagTemplate, false) : undefined; - if (baselineTag) addBaselineTag(baselineTag); + if (baselineTag) { + addBaselineTag(baselineTag); + } const allTags = baselineTag ? [...nextTags, baselineTag] : nextTags; // Link per-package tags back to their update records so the publish pipeline // can push each tag immediately after that package publishes. @@ -474,7 +480,9 @@ export function createSyncStrategy(config: Config): StrategyFunction { for (let i = 0; i < workspaceNames.length; i++) { const pkgName = workspaceNames[i]; const pkgTag = nextTags[i]; - if (pkgName && pkgTag) setPackageUpdateTag(pkgName, pkgTag); + if (pkgName && pkgTag) { + setPackageUpdateTag(pkgName, pkgTag); + } } } // Resolved version action. Every package moves in lockstep to the same nextVersion @@ -618,7 +626,9 @@ export function createSingleStrategy(config: Config): StrategyFunction { } } catch (error) { // A strictReachable violation must abort the run, not degrade to a minimal entry. - if (error instanceof StrictReachableError) throw error; + if (error instanceof StrictReachableError) { + throw error; + } log(`Error extracting changelog entries: ${error instanceof Error ? error.message : String(error)}`, 'warning'); // Fall back to minimal entry changelogEntries = [{ type: 'changed', description: `Update version to ${nextVersion}` }]; diff --git a/packages/version/src/core/workspaceGraph.ts b/packages/version/src/core/workspaceGraph.ts index debe2058..407f9291 100644 --- a/packages/version/src/core/workspaceGraph.ts +++ b/packages/version/src/core/workspaceGraph.ts @@ -26,7 +26,9 @@ function cargoDeps(cargoTomlPath: string, pkgDir: string, nameByDir: Map { - if (!ref || ref.trim() === '') return Promise.resolve(false); + if (!ref || ref.trim() === '') { + return Promise.resolve(false); + } // The seam's refExists is the same `rev-parse --verify --quiet ^{commit}` soft lookup. return git.refExists(ref, cwd); } @@ -304,7 +306,9 @@ export async function getLatestStableTag(versionPrefix?: string): Promise { diff --git a/packages/version/src/package/packageProcessor.ts b/packages/version/src/package/packageProcessor.ts index 8d99121e..f327ea5d 100644 --- a/packages/version/src/package/packageProcessor.ts +++ b/packages/version/src/package/packageProcessor.ts @@ -297,7 +297,9 @@ export class PackageProcessor { } } catch (error) { // A strictReachable violation must abort the run, not degrade to a minimal entry. - if (error instanceof StrictReachableError) throw error; + if (error instanceof StrictReachableError) { + throw error; + } log(`Error extracting changelog entries: ${error instanceof Error ? error.message : String(error)}`, 'warning'); // Fall back to minimal entry changelogEntries = [ diff --git a/packages/version/src/utils/formatting.ts b/packages/version/src/utils/formatting.ts index 1d6fc6b6..675e7431 100644 --- a/packages/version/src/utils/formatting.ts +++ b/packages/version/src/utils/formatting.ts @@ -170,7 +170,9 @@ export function deriveBaselineTagPrefix( formattedPrefix: string, packageName?: string, ): string | undefined { - if (!template) return undefined; + if (!template) { + return undefined; + } return template .split('${' + 'version}')[0] .replace(/\$\{prefix\}/g, formattedPrefix) @@ -187,6 +189,8 @@ export function deriveBaselineTagPrefix( * alone — only the display surfaces are cleaned up. */ export function displayTag(tag: string, baselineTagPrefix: string | undefined, formattedPrefix: string): string { - if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) return tag; + if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) { + return tag; + } return `${formattedPrefix}${tag.slice(baselineTagPrefix.length)}`; } diff --git a/packages/version/src/utils/jsonOutput.ts b/packages/version/src/utils/jsonOutput.ts index e04fab23..99c528b5 100644 --- a/packages/version/src/utils/jsonOutput.ts +++ b/packages/version/src/utils/jsonOutput.ts @@ -55,7 +55,9 @@ export function enableJsonOutput(dryRun = false): void { * Called during dryRun passes in place of writing directly to disk. */ export function recordPendingWrite(path: string, content: string): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } _pendingWrites.push({ path, content }); } @@ -91,7 +93,9 @@ export function isJsonOutputMode(): boolean { * (preview, standing PR) can render sync releases as a single versioned unit. */ export function setVersioningStrategy(strategy: 'sync' | 'single' | 'async' | 'group'): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } _jsonData.strategy = strategy; } @@ -109,7 +113,9 @@ export function setVersioningStrategy(strategy: 'sync' | 'single' | 'async' | 'g * for the same directory exists. */ export function addPackageUpdate(packageName: string, newVersion: string, filePath: string, isRoot?: boolean): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } // Channel is derived per-package from the resolved version so every consumer (preview, // standing PR) reads a single authoritative value rather than re-deriving it. @@ -143,9 +149,13 @@ export function addPackageUpdate(packageName: string, newVersion: string, filePa * Called after the tag name is computed, to link it back to the update record. */ export function setPackageUpdateTag(packageName: string, tag: string): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } const update = _jsonData.updates.find((u) => u.packageName === packageName); - if (update) update.tag = tag; + if (update) { + update.tag = tag; + } } /** @@ -154,9 +164,13 @@ export function setPackageUpdateTag(packageName: string, tag: string): void { * package.json update has been recorded. */ export function setPackageUpdateGroup(packageName: string, group: string): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } const update = _jsonData.updates.find((u) => u.packageName === packageName); - if (update) update.group = group; + if (update) { + update.group = group; + } } /** @@ -166,7 +180,9 @@ export function setPackageUpdateGroup(packageName: string, group: string): void * isn't found (mirrors the other setPackageUpdate* helpers). */ export function setPackageUpdateAction(packageName: string, action: VersionAction, reason: string): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } const update = _jsonData.updates.find((u) => u.packageName === packageName); if (update) { update.action = action; @@ -182,9 +198,13 @@ export function setPackageUpdateAction(packageName: string, action: VersionActio * setPackageUpdate* helpers). */ export function setPackageUpdatePreviousVersion(packageName: string, previousVersion: string | null): void { - if (!_jsonOutputMode || previousVersion === null) return; + if (!_jsonOutputMode || previousVersion === null) { + return; + } const update = _jsonData.updates.find((u) => u.packageName === packageName); - if (update) update.previousVersion = previousVersion; + if (update) { + update.previousVersion = previousVersion; + } } /** @@ -194,7 +214,9 @@ export function setPackageUpdatePreviousVersion(packageName: string, previousVer * (otherwise-private) update list. */ export function setAllPackageUpdateActions(action: VersionAction, reason: string): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } for (const update of _jsonData.updates) { update.action = action; update.actionReason = reason; @@ -207,7 +229,9 @@ export function setAllPackageUpdateActions(action: VersionAction, reason: string * the field absent everywhere. Owns the iteration internally, mirroring {@link setAllPackageUpdateActions}. */ export function setAllPackageUpdatePreviousVersions(previousVersion: string | null): void { - if (!_jsonOutputMode || previousVersion === null) return; + if (!_jsonOutputMode || previousVersion === null) { + return; + } for (const update of _jsonData.updates) { update.previousVersion = previousVersion; } @@ -220,7 +244,9 @@ export function setAllPackageUpdatePreviousVersions(previousVersion: string | nu * the engine after the strategy has produced the updates. */ export function tagPrerequisiteRoles(overrideScope: string[], prerequisiteOf: Record): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } const targets = new Set(overrideScope); for (const update of _jsonData.updates) { if (targets.has(update.packageName)) { @@ -236,7 +262,9 @@ export function tagPrerequisiteRoles(overrideScope: string[], prerequisiteOf: Re * Add changelog data for a package to the JSON output */ export function addChangelogData(data: VersionPackageChangelog): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } _jsonData.changelogs.push(data); } @@ -246,7 +274,9 @@ export function addChangelogData(data: VersionPackageChangelog): void { * that don't belong to any specific package. */ export function setSharedEntries(entries: VersionChangelogEntry[]): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } _jsonData.sharedEntries = entries.length > 0 ? entries : undefined; } @@ -254,7 +284,9 @@ export function setSharedEntries(entries: VersionChangelogEntry[]): void { * Add a tag to the JSON output */ export function addTag(tag: string): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } _jsonData.tags.push(tag); } @@ -264,9 +296,13 @@ export function addTag(tag: string): void { * consumer tags but excluded from GitHub Release creation. */ export function addBaselineTag(tag: string): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } - if (!_jsonData.baselineTags) _jsonData.baselineTags = []; + if (!_jsonData.baselineTags) { + _jsonData.baselineTags = []; + } _jsonData.baselineTags.push(tag); } @@ -274,7 +310,9 @@ export function addBaselineTag(tag: string): void { * Set the commit message in the JSON output */ export function setCommitMessage(message: string): void { - if (!_jsonOutputMode) return; + if (!_jsonOutputMode) { + return; + } _jsonData.commitMessage = message; } diff --git a/packages/version/src/utils/logging.ts b/packages/version/src/utils/logging.ts index 82031f6c..110791ab 100644 --- a/packages/version/src/utils/logging.ts +++ b/packages/version/src/utils/logging.ts @@ -10,7 +10,9 @@ import { isJsonOutputMode } from './jsonOutput.js'; * Print a figlet banner */ export function printFiglet(text: string): void { - if (isJsonOutputMode()) return; + if (isJsonOutputMode()) { + return; + } console.log( chalk.yellow( diff --git a/packages/version/src/utils/versionAction.ts b/packages/version/src/utils/versionAction.ts index 9fa6385a..e4a4eb56 100644 --- a/packages/version/src/utils/versionAction.ts +++ b/packages/version/src/utils/versionAction.ts @@ -40,7 +40,9 @@ function versionFromTag(tag: string): string | null { // Bounded quantifiers (not `\d+`) guard against polynomial backtracking on uncontrolled tag input // (CodeQL js/polynomial-redos) — real semver components never approach these lengths. const match = tag.match(/\d{1,16}\.\d{1,16}\.\d{1,16}(?:-[0-9A-Za-z.-]{1,256})?/); - if (!match) return null; + if (!match) { + return null; + } return semver.valid(match[0]); } diff --git a/packages/version/src/workspace.ts b/packages/version/src/workspace.ts index 4567255d..47a278ca 100644 --- a/packages/version/src/workspace.ts +++ b/packages/version/src/workspace.ts @@ -32,7 +32,9 @@ export async function getWorkspacePackageVersions( for (const p of packages) { const name = p.packageJson.name; const version = p.packageJson.version; - if (name && version) versions[name] = version; + if (name && version) { + versions[name] = version; + } } return versions; } diff --git a/packages/version/test/unit/core/groupStrategy.spec.ts b/packages/version/test/unit/core/groupStrategy.spec.ts index 5d0b0b52..5ebd5555 100644 --- a/packages/version/test/unit/core/groupStrategy.spec.ts +++ b/packages/version/test/unit/core/groupStrategy.spec.ts @@ -87,7 +87,9 @@ describe('createGroupStrategy', () => { // Only native-utils has a releasable change (patch). vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-utils') return '2.3.1'; + if (opts.name === '@wdio/native-utils') { + return '2.3.1'; + } return ''; }); @@ -161,8 +163,12 @@ describe('createGroupStrategy', () => { // core gets a minor, utils gets a patch — group should bump minor. vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-core') return '2.4.0'; - if (opts.name === '@wdio/native-utils') return '2.3.1'; + if (opts.name === '@wdio/native-core') { + return '2.4.0'; + } + if (opts.name === '@wdio/native-utils') { + return '2.3.1'; + } return ''; }); @@ -206,7 +212,9 @@ describe('createGroupStrategy', () => { // Only utils changed. vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-utils') return '2.3.1'; + if (opts.name === '@wdio/native-utils') { + return '2.3.1'; + } return ''; }); @@ -237,8 +245,12 @@ describe('createGroupStrategy', () => { // core minor, utils patch, spy unchanged → both changed members align to 2.4.0. vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-core') return '2.4.0'; - if (opts.name === '@wdio/native-utils') return '2.3.1'; + if (opts.name === '@wdio/native-core') { + return '2.4.0'; + } + if (opts.name === '@wdio/native-utils') { + return '2.3.1'; + } return ''; }); @@ -269,7 +281,9 @@ describe('createGroupStrategy', () => { // Only the service earned a change — a premajor prerelease. vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/flutter-service') return '1.0.0-next.0'; + if (opts.name === '@wdio/flutter-service') { + return '1.0.0-next.0'; + } return ''; }); @@ -297,7 +311,9 @@ describe('createGroupStrategy', () => { const contract = mkPackage('wdio_flutter', '0.0.1'); vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/flutter-service') return '1.0.0-next.0'; + if (opts.name === '@wdio/flutter-service') { + return '1.0.0-next.0'; + } return ''; }); @@ -528,7 +544,9 @@ describe('createGroupStrategy', () => { vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { // core earns a patch from its own 2.3.0 baseline. - if (opts.name === '@wdio/native-core') return '2.3.1'; + if (opts.name === '@wdio/native-core') { + return '2.3.1'; + } return ''; }); @@ -558,9 +576,13 @@ describe('createGroupStrategy', () => { const newcomer = mkPackage('@wdio/native-fresh', '1.0.0'); vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-core') return '2.3.1'; + if (opts.name === '@wdio/native-core') { + return '2.3.1'; + } // newcomer also has a change, but its own bump is irrelevant under fixed. - if (opts.name === '@wdio/native-fresh') return '1.0.1'; + if (opts.name === '@wdio/native-fresh') { + return '1.0.1'; + } return ''; }); @@ -586,8 +608,12 @@ describe('createGroupStrategy', () => { const utils = mkPackage('@wdio/native-utils', '2.3.0'); vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-core') return '2.3.1'; - if (opts.name === '@wdio/native-utils') return '2.3.1'; + if (opts.name === '@wdio/native-core') { + return '2.3.1'; + } + if (opts.name === '@wdio/native-utils') { + return '2.3.1'; + } return ''; }); @@ -616,7 +642,9 @@ describe('createGroupStrategy', () => { const b = mkPackage('@app/b', '1.0.0'); vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@app/a') return '1.1.0'; + if (opts.name === '@app/a') { + return '1.1.0'; + } return ''; }); @@ -645,7 +673,9 @@ describe('createGroupStrategy', () => { // Only native-core is in graduateScope → its calculator returns the stable base; native-utils // is out of scope so (per applyOverrideScope, simulated here) it advances its prerelease line. vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-core') return '1.0.0'; + if (opts.name === '@wdio/native-core') { + return '1.0.0'; + } return '1.0.0-next.6'; }); @@ -678,7 +708,9 @@ describe('createGroupStrategy', () => { // Both members changed; native-core is the graduate target. Linked releases only changed members, // all at the shared graduated version. vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-core') return '2.0.0'; + if (opts.name === '@wdio/native-core') { + return '2.0.0'; + } return '2.0.0-next.3'; }); @@ -869,8 +901,12 @@ describe('createGroupStrategy', () => { const standalone = mkPackage('@app/standalone', '5.0.0'); vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-core') return '2.3.1'; - if (opts.name === '@app/standalone') return '5.1.0'; + if (opts.name === '@wdio/native-core') { + return '2.3.1'; + } + if (opts.name === '@app/standalone') { + return '5.1.0'; + } return ''; }); @@ -913,8 +949,12 @@ describe('createGroupStrategy', () => { // core earns a minor, utils a patch — independent means each keeps its own commit-driven bump // (a fixed/linked group would write the shared 2.4.0 to both). vi.mocked(calculator.calculateVersion).mockImplementation(async (_cfg, opts) => { - if (opts.name === '@wdio/native-core') return '2.4.0'; - if (opts.name === '@wdio/native-utils') return '2.3.1'; + if (opts.name === '@wdio/native-core') { + return '2.4.0'; + } + if (opts.name === '@wdio/native-utils') { + return '2.3.1'; + } return ''; }); diff --git a/packages/version/test/unit/core/versionCalculator.spec.ts b/packages/version/test/unit/core/versionCalculator.spec.ts index 94f2c67c..9ab03dc1 100644 --- a/packages/version/test/unit/core/versionCalculator.spec.ts +++ b/packages/version/test/unit/core/versionCalculator.spec.ts @@ -100,43 +100,85 @@ describe('Version Calculator', () => { // Mock normalizePrereleaseIdentifier vi.spyOn(versionUtils, 'normalizePrereleaseIdentifier').mockImplementation((val) => { - if (val === true) return 'next'; + if (val === true) { + return 'next'; + } return val as string; }); // Mock bumpVersion vi.spyOn(versionUtils, 'bumpVersion').mockImplementation((version, releaseType, identifier) => { // Special cases for test expectations - if (version === '1.0.0' && releaseType === 'minor' && !identifier) return '1.1.0'; - if (version === '1.0.0-next.0' && releaseType === 'major' && !identifier) return '2.0.0'; - if (version === '1.0.0-beta.1' && releaseType === 'minor' && !identifier) return '1.1.0'; - if (version === '1.0.0-alpha.2' && releaseType === 'patch' && !identifier) return '1.0.1'; - if (version === '1.0.0' && releaseType === 'prerelease' && identifier === 'alpha') return '1.0.0-alpha.1'; - if (version === '1.3.0' && releaseType === 'major' && identifier === 'next') return '2.0.0-next.0'; - if (version === '1.3.0' && releaseType === 'minor' && identifier === 'next') return '1.4.0-next.0'; - if (version === '1.3.1' && releaseType === 'patch' && identifier === 'next') return '1.3.2-next.0'; - if (version === '1.0.0-beta.1' && releaseType === 'major' && !identifier) return '2.0.0'; - if (version === '1.0.0-test' && releaseType === 'minor' && !identifier) return '1.1.0'; - if (version === '1.0.0' && releaseType === 'patch' && !identifier) return '1.0.1'; + if (version === '1.0.0' && releaseType === 'minor' && !identifier) { + return '1.1.0'; + } + if (version === '1.0.0-next.0' && releaseType === 'major' && !identifier) { + return '2.0.0'; + } + if (version === '1.0.0-beta.1' && releaseType === 'minor' && !identifier) { + return '1.1.0'; + } + if (version === '1.0.0-alpha.2' && releaseType === 'patch' && !identifier) { + return '1.0.1'; + } + if (version === '1.0.0' && releaseType === 'prerelease' && identifier === 'alpha') { + return '1.0.0-alpha.1'; + } + if (version === '1.3.0' && releaseType === 'major' && identifier === 'next') { + return '2.0.0-next.0'; + } + if (version === '1.3.0' && releaseType === 'minor' && identifier === 'next') { + return '1.4.0-next.0'; + } + if (version === '1.3.1' && releaseType === 'patch' && identifier === 'next') { + return '1.3.2-next.0'; + } + if (version === '1.0.0-beta.1' && releaseType === 'major' && !identifier) { + return '2.0.0'; + } + if (version === '1.0.0-test' && releaseType === 'minor' && !identifier) { + return '1.1.0'; + } + if (version === '1.0.0' && releaseType === 'patch' && !identifier) { + return '1.0.1'; + } // For branch pattern tests - if (version === '1.0.0' && releaseType === 'minor' && identifier === undefined) return '1.1.0'; - if (version === '1.0.0' && releaseType === 'patch' && identifier === undefined) return '1.0.1'; + if (version === '1.0.0' && releaseType === 'minor' && identifier === undefined) { + return '1.1.0'; + } + if (version === '1.0.0' && releaseType === 'patch' && identifier === undefined) { + return '1.0.1'; + } // For Package.json fallback tests - if (version === '0.0.0' && releaseType === 'minor') return '0.1.0'; - if (version === '2.0.0' && releaseType === 'minor' && !identifier) return '2.1.0'; + if (version === '0.0.0' && releaseType === 'minor') { + return '0.1.0'; + } + if (version === '2.0.0' && releaseType === 'minor' && !identifier) { + return '2.1.0'; + } // For conventional commits - if (version === '1.0.0' && releaseType === 'patch' && identifier === undefined) return '1.0.1'; - if (version === '0.0.0' && releaseType === 'patch') return '0.0.1'; + if (version === '1.0.0' && releaseType === 'patch' && identifier === undefined) { + return '1.0.1'; + } + if (version === '0.0.0' && releaseType === 'patch') { + return '0.0.1'; + } // Handle specific prerelease cases - if (version === '1.3.0' && releaseType === 'premajor' && identifier === 'next') return '2.0.0-next.0'; + if (version === '1.3.0' && releaseType === 'premajor' && identifier === 'next') { + return '2.0.0-next.0'; + } // Handle smart fallback cases where git tag version is used directly - if (version === '1.1.0' && releaseType === 'patch' && !identifier) return '1.1.1'; - if (version === '1.2.0' && releaseType === 'patch' && !identifier) return '1.2.1'; + if (version === '1.1.0' && releaseType === 'patch' && !identifier) { + return '1.1.1'; + } + if (version === '1.2.0' && releaseType === 'patch' && !identifier) { + return '1.2.1'; + } // Fallback - for unexpected cases, increment patch const semverResult = semver.inc(version, 'patch'); @@ -145,7 +187,9 @@ describe('Version Calculator', () => { // Create a mock for semver.inc vi.spyOn(semver, 'inc').mockImplementation((version, releaseType, identifier) => { - if (!version) return null; + if (!version) { + return null; + } // Convert version to string safely const versionStr = typeof version === 'string' ? version : (version as { version?: string })?.version || '0.0.0'; @@ -153,8 +197,12 @@ describe('Version Calculator', () => { const parts = versionStr.split('.'); // Handle different release types - if (releaseType === 'major') return `${Number(parts[0]) + 1}.0.0`; - if (releaseType === 'minor') return `${parts[0]}.${Number(parts[1]) + 1}.0`; + if (releaseType === 'major') { + return `${Number(parts[0]) + 1}.0.0`; + } + if (releaseType === 'minor') { + return `${parts[0]}.${Number(parts[1]) + 1}.0`; + } if (releaseType === 'patch') { // Handle prerelease versions if (versionStr.includes('-')) { @@ -641,7 +689,9 @@ describe('Version Calculator', () => { // Mock semver.clean to behave like the real library: only pure semver strings // (optionally v-prefixed) are accepted; compound tags like "scope-pkg-v1.2.3" return null. vi.spyOn(semver, 'clean').mockImplementation((version) => { - if (typeof version !== 'string') return null; + if (typeof version !== 'string') { + return null; + } const match = version.match(/^v?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)$/); return match ? match[1] : null; }); @@ -682,7 +732,9 @@ describe('Version Calculator', () => { // Mock semver.clean to behave like the real library vi.spyOn(semver, 'clean').mockImplementation((version) => { - if (typeof version !== 'string') return null; + if (typeof version !== 'string') { + return null; + } const match = version.match(/^v?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)$/); return match ? match[1] : null; }); @@ -729,7 +781,9 @@ describe('Version Calculator', () => { // Mock semver.clean to behave like the real library vi.spyOn(semver, 'clean').mockImplementation((version) => { - if (typeof version !== 'string') return null; + if (typeof version !== 'string') { + return null; + } const match = version.match(/^v?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)$/); return match ? match[1] : null; }); @@ -777,7 +831,9 @@ describe('Version Calculator', () => { // instead of 0.22.0. vi.resetAllMocks(); vi.spyOn(semver, 'clean').mockImplementation((version) => { - if (typeof version !== 'string') return null; + if (typeof version !== 'string') { + return null; + } const match = version.match(/^v?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)$/); return match ? match[1] : null; }); @@ -1144,7 +1200,9 @@ describe('Version Calculator', () => { // Implement semver.inc with a more complex behaviour for different scenarios vi.spyOn(semver, 'inc').mockImplementation((version, releaseType, identifier) => { - if (!version) return '1.0.0'; + if (!version) { + return '1.0.0'; + } // For test: should use package.json version when no latestTag exists with explicit bump if (version === '1.0.0-beta.1' && releaseType === 'patch') { @@ -1643,7 +1701,9 @@ describe('Version Calculator', () => { // Mock existing prerelease version vi.spyOn(semver, 'prerelease').mockReturnValue(['next', 0]); vi.spyOn(semver, 'clean').mockImplementation((version) => { - if (version === 'v1.0.0-next.0') return '1.0.0-next.0'; + if (version === 'v1.0.0-next.0') { + return '1.0.0-next.0'; + } return version?.replace(/^[^\d]*/, '') || null; }); vi.spyOn(versionUtils, 'bumpVersion').mockReturnValue('1.0.0'); // Clean to stable diff --git a/packages/version/test/unit/core/versionStrategies.spec.ts b/packages/version/test/unit/core/versionStrategies.spec.ts index 6b88dc8c..108c1c31 100644 --- a/packages/version/test/unit/core/versionStrategies.spec.ts +++ b/packages/version/test/unit/core/versionStrategies.spec.ts @@ -37,14 +37,18 @@ vi.mock('../../../src/utils/formatting.js', () => ({ return template.replace(/\$\{version\}/g, version).replace(/\$\{packageName\}/g, packageName || ''); }), deriveBaselineTagPrefix: vi.fn().mockImplementation((template, formattedPrefix, packageName) => { - if (!template) return undefined; + if (!template) { + return undefined; + } return template .split('${' + 'version}')[0] .replace(/\$\{prefix\}/g, formattedPrefix) .replace(/\$\{packageName\}/g, packageName ?? ''); }), displayTag: vi.fn().mockImplementation((tag, baselineTagPrefix, formattedPrefix) => { - if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) return tag; + if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) { + return tag; + } return `${formattedPrefix}${tag.slice(baselineTagPrefix.length)}`; }), })); @@ -105,7 +109,9 @@ describe('Version Strategies', () => { vi.mocked(formatting.formatCommitMessage, { partial: true }).mockReturnValue('chore: release package-a v1.1.0'); vi.mocked(formatting.deriveBaselineTagPrefix, { partial: true }).mockImplementation( (template, formattedPrefix, packageName) => { - if (!template) return undefined; + if (!template) { + return undefined; + } return template .split('${' + 'version}')[0] .replace(/\$\{prefix\}/g, formattedPrefix) @@ -114,7 +120,9 @@ describe('Version Strategies', () => { ); vi.mocked(formatting.displayTag, { partial: true }).mockImplementation( (tag, baselineTagPrefix, formattedPrefix) => { - if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) return tag; + if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) { + return tag; + } return `${formattedPrefix}${tag.slice(baselineTagPrefix.length)}`; }, ); diff --git a/packages/version/test/unit/package/packageProcessor.spec.ts b/packages/version/test/unit/package/packageProcessor.spec.ts index 7951ea3c..b6ac8d6b 100644 --- a/packages/version/test/unit/package/packageProcessor.spec.ts +++ b/packages/version/test/unit/package/packageProcessor.spec.ts @@ -36,7 +36,9 @@ vi.mock('../../../src/utils/formatting.js', () => ({ escapeRegExp: vi.fn().mockImplementation((str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), deriveBaselineTagPrefix: vi.fn().mockReturnValue(undefined), displayTag: vi.fn().mockImplementation((tag, baselineTagPrefix, formattedPrefix) => { - if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) return tag; + if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) { + return tag; + } return `${formattedPrefix}${tag.slice(baselineTagPrefix.length)}`; }), })); @@ -380,7 +382,9 @@ describe('Package Processor', () => { vi.spyOn(gitTags, 'getLatestTagForPackage').mockResolvedValue('release/v1.0.0'); vi.spyOn(formatting, 'deriveBaselineTagPrefix').mockReturnValue('release/v'); vi.spyOn(formatting, 'displayTag').mockImplementation((tag, baselineTagPrefix, formattedPrefix) => { - if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) return tag; + if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) { + return tag; + } return `${formattedPrefix}${tag.slice(baselineTagPrefix.length)}`; }); @@ -404,7 +408,9 @@ describe('Package Processor', () => { vi.spyOn(gitTags, 'getLatestTagForPackage').mockResolvedValue('release/v1.0.0'); vi.spyOn(formatting, 'deriveBaselineTagPrefix').mockReturnValue('release/v'); vi.spyOn(formatting, 'displayTag').mockImplementation((tag, baselineTagPrefix, formattedPrefix) => { - if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) return tag; + if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) { + return tag; + } return `${formattedPrefix}${tag.slice(baselineTagPrefix.length)}`; }); @@ -427,7 +433,9 @@ describe('Package Processor', () => { vi.spyOn(gitTags, 'getLatestTagForPackage').mockResolvedValue('release/v1.0.0'); vi.spyOn(formatting, 'deriveBaselineTagPrefix').mockReturnValue('release/v'); vi.spyOn(formatting, 'displayTag').mockImplementation((tag, baselineTagPrefix, formattedPrefix) => { - if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) return tag; + if (!baselineTagPrefix || !tag.startsWith(baselineTagPrefix)) { + return tag; + } return `${formattedPrefix}${tag.slice(baselineTagPrefix.length)}`; }); // Baseline resolves as a ref but can't be verified from HEAD (shallow clone / unpushed tag). @@ -700,7 +708,9 @@ describe('Package Processor', () => { const pkgAEntry = { type: 'added', description: 'New feature in pkg-a' }; vi.spyOn(commitParser, 'extractChangelogEntriesFromCommits').mockImplementation((_dir) => { - if (_dir.includes('package-a')) return [pkgAEntry]; + if (_dir.includes('package-a')) { + return [pkgAEntry]; + } return []; }); vi.spyOn(commitParser, 'extractRepoLevelChangelogEntries').mockResolvedValue([repoLevelEntry]); diff --git a/packages/version/test/unit/utils/versionUtils.spec.ts b/packages/version/test/unit/utils/versionUtils.spec.ts index b0ea4d8a..960c1d0e 100644 --- a/packages/version/test/unit/utils/versionUtils.spec.ts +++ b/packages/version/test/unit/utils/versionUtils.spec.ts @@ -171,16 +171,36 @@ describe('Version Utils', () => { beforeEach(() => { // Setup semver mocks vi.mocked(semver.prerelease, { partial: true }).mockImplementation((version) => { - if (version === '1.0.0-beta.1') return ['beta', 1]; - if (version === '1.0.0-beta.2') return ['beta', 2]; - if (version === '1.0.0-alpha.3') return ['alpha', 3]; - if (version === '1.0.0-next.0') return ['next', 0]; - if (version === '2.0.0-alpha.3') return ['alpha', 3]; - if (version === '2.0.0-next.0') return ['next', 0]; - if (version === '3.0.0-rc.1') return ['rc', 1]; - if (version === '2.1.0-next.4') return ['next', 4]; - if (version === '3.5.0-beta.12') return ['beta', 12]; - if (version === '4.0.1-rc.2') return ['rc', 2]; + if (version === '1.0.0-beta.1') { + return ['beta', 1]; + } + if (version === '1.0.0-beta.2') { + return ['beta', 2]; + } + if (version === '1.0.0-alpha.3') { + return ['alpha', 3]; + } + if (version === '1.0.0-next.0') { + return ['next', 0]; + } + if (version === '2.0.0-alpha.3') { + return ['alpha', 3]; + } + if (version === '2.0.0-next.0') { + return ['next', 0]; + } + if (version === '3.0.0-rc.1') { + return ['rc', 1]; + } + if (version === '2.1.0-next.4') { + return ['next', 4]; + } + if (version === '3.5.0-beta.12') { + return ['beta', 12]; + } + if (version === '4.0.1-rc.2') { + return ['rc', 2]; + } return null; }); @@ -270,15 +290,33 @@ describe('Version Utils', () => { vi.mocked(semver.inc, { partial: true }).mockImplementation((version, releaseType, identifier) => { // Stable base versions (after stripping prerelease) - if (version === '1.0.0' && releaseType === 'major') return '2.0.0'; - if (version === '1.0.0' && releaseType === 'minor') return '1.1.0'; - if (version === '1.0.0' && releaseType === 'patch') return '1.0.1'; - if (version === '2.0.0' && releaseType === 'major') return '3.0.0'; - if (version === '3.0.0' && releaseType === 'major') return '4.0.0'; - if (version === '2.1.0' && releaseType === 'minor') return '2.2.0'; - if (version === '3.5.0' && releaseType === 'minor') return '3.6.0'; - if (version === '4.0.1' && releaseType === 'patch') return '4.0.2'; - if (version === '4.0.1' && releaseType === 'minor') return '4.1.0'; + if (version === '1.0.0' && releaseType === 'major') { + return '2.0.0'; + } + if (version === '1.0.0' && releaseType === 'minor') { + return '1.1.0'; + } + if (version === '1.0.0' && releaseType === 'patch') { + return '1.0.1'; + } + if (version === '2.0.0' && releaseType === 'major') { + return '3.0.0'; + } + if (version === '3.0.0' && releaseType === 'major') { + return '4.0.0'; + } + if (version === '2.1.0' && releaseType === 'minor') { + return '2.2.0'; + } + if (version === '3.5.0' && releaseType === 'minor') { + return '3.6.0'; + } + if (version === '4.0.1' && releaseType === 'patch') { + return '4.0.2'; + } + if (version === '4.0.1' && releaseType === 'minor') { + return '4.1.0'; + } if (version === '1.0.0' && releaseType === 'premajor' && identifier === 'alpha') { return '2.0.0-alpha.0'; @@ -298,24 +336,44 @@ describe('Version Utils', () => { // Cases for prerelease increment on existing prerelease versions // Using 'prerelease' release type increments the prerelease counter only if (releaseType === 'prerelease' && identifier === 'next') { - if (version === '1.0.0-next.0') return '1.0.0-next.1'; - if (version === '1.0.0-next.4') return '1.0.0-next.5'; - if (version === '2.0.0-next.0') return '2.0.0-next.1'; + if (version === '1.0.0-next.0') { + return '1.0.0-next.1'; + } + if (version === '1.0.0-next.4') { + return '1.0.0-next.5'; + } + if (version === '2.0.0-next.0') { + return '2.0.0-next.1'; + } } if (releaseType === 'prerelease' && identifier === 'beta') { - if (version === '1.0.0-beta.1') return '1.0.0-beta.2'; - if (version === '1.0.0-beta.2') return '1.0.0-beta.3'; + if (version === '1.0.0-beta.1') { + return '1.0.0-beta.2'; + } + if (version === '1.0.0-beta.2') { + return '1.0.0-beta.3'; + } } if (releaseType === 'prerelease' && identifier === 'alpha') { - if (version === '1.0.0-alpha.3') return '1.0.0-alpha.4'; - if (version === '2.0.0-alpha.3') return '2.0.0-alpha.4'; + if (version === '1.0.0-alpha.3') { + return '1.0.0-alpha.4'; + } + if (version === '2.0.0-alpha.3') { + return '2.0.0-alpha.4'; + } } // Handle prepatch/preminor/premajor calls that fall through (for stable versions with identifiers) if (identifier === 'next' || identifier === 'alpha' || identifier === 'beta') { - if (version === '2.0.0' && releaseType === 'premajor') return '3.0.0-next.0'; - if (version === '1.0.0' && releaseType === 'preminor') return '1.1.0-next.0'; - if (version === '1.0.0' && releaseType === 'prepatch') return '1.0.1-next.0'; + if (version === '2.0.0' && releaseType === 'premajor') { + return '3.0.0-next.0'; + } + if (version === '1.0.0' && releaseType === 'preminor') { + return '1.1.0-next.0'; + } + if (version === '1.0.0' && releaseType === 'prepatch') { + return '1.0.1-next.0'; + } } return `${version}.incremented`; diff --git a/packages/version/test/utils/cli.ts b/packages/version/test/utils/cli.ts index 2e1a3d8d..36a3b175 100644 --- a/packages/version/test/utils/cli.ts +++ b/packages/version/test/utils/cli.ts @@ -10,6 +10,8 @@ import { join } from 'node:path'; export function executeCliCommand(command: string, cwd: string, dryRun = true) { const cliPath = join(process.cwd(), 'dist/cli.js'); const args = command.split(' '); - if (dryRun) args.push('--dry-run'); + if (dryRun) { + args.push('--dry-run'); + } return spawnSync('node', [cliPath, ...args], { cwd, encoding: 'utf-8' }); } diff --git a/scripts/examples-smoke/build-fixture.ts b/scripts/examples-smoke/build-fixture.ts index 8dabae2d..5c6df4d4 100644 --- a/scripts/examples-smoke/build-fixture.ts +++ b/scripts/examples-smoke/build-fixture.ts @@ -38,10 +38,15 @@ function parseArgs(argv: string[]): Args { const args: Partial = { seedLockfile: false }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; - if (a === '--scenario') args.scenario = argv[++i]; - else if (a === '--out') args.out = argv[++i]; - else if (a === '--tarball-dir') args.tarballDir = argv[++i]; - else if (a === '--seed-lockfile') args.seedLockfile = true; + if (a === '--scenario') { + args.scenario = argv[++i]; + } else if (a === '--out') { + args.out = argv[++i]; + } else if (a === '--tarball-dir') { + args.tarballDir = argv[++i]; + } else if (a === '--seed-lockfile') { + args.seedLockfile = true; + } } if (!args.scenario || !args.out || !args.tarballDir) { throw new Error('Usage: build-fixture --scenario --out --tarball-dir [--seed-lockfile]'); @@ -51,7 +56,9 @@ function parseArgs(argv: string[]): Args { function findTarball(dir: string, prefix: string): string { const file = readdirSync(dir).find((f) => f.startsWith(prefix) && f.endsWith('.tgz')); - if (!file) throw new Error(`Could not find ${prefix}*.tgz in ${dir}`); + if (!file) { + throw new Error(`Could not find ${prefix}*.tgz in ${dir}`); + } return file; } @@ -158,7 +165,9 @@ function buildNpmCargoMonorepo(out: string, tarballNames: Record function main(): void { const { scenario: scenarioId, out, tarballDir, seedLockfile } = parseArgs(process.argv.slice(2)); const scenario = SCENARIOS.find((s) => s.id === scenarioId); - if (!scenario) throw new Error(`Unknown scenario: ${scenarioId}`); + if (!scenario) { + throw new Error(`Unknown scenario: ${scenarioId}`); + } const outDir = resolve(out); rmSync(outDir, { recursive: true, force: true }); diff --git a/scripts/examples-smoke/generate-smoke-workflow.ts b/scripts/examples-smoke/generate-smoke-workflow.ts index c2a43943..3bf9b1fa 100644 --- a/scripts/examples-smoke/generate-smoke-workflow.ts +++ b/scripts/examples-smoke/generate-smoke-workflow.ts @@ -76,8 +76,12 @@ function extractSetupSteps(scenario: Scenario): Step[] { } const setup: Step[] = []; for (const step of job.steps) { - if (isReleasekitRun(step)) break; // setup is everything before the release call - if (isCheckout(step) || isOutOfScopeApiStep(step)) continue; + if (isReleasekitRun(step)) { + break; // setup is everything before the release call + } + if (isCheckout(step) || isOutOfScopeApiStep(step)) { + continue; + } // Strip only envs that would fail or do harm in a dry run: references to // `secrets.*`. Keep diagnostic toggles (LOG_LEVEL, RUST_BACKTRACE, ...) and // plain literals — the smoke job's value is the missing-tool signal, not @@ -86,7 +90,9 @@ function extractSetupSteps(scenario: Scenario): Step[] { if (cleaned.env && typeof cleaned.env === 'object') { const filtered: Record = {}; for (const [k, v] of Object.entries(cleaned.env as Record)) { - if (!isSecretReferencingEnvValue(v)) filtered[k] = v; + if (!isSecretReferencingEnvValue(v)) { + filtered[k] = v; + } } if (Object.keys(filtered).length > 0) { cleaned.env = filtered; @@ -114,7 +120,9 @@ function runtimeProbe(scenario: Scenario): Step | undefined { publish?: { cargo?: { enabled?: boolean } }; }; const cargoEnabled = cfg.version?.cargo?.enabled === true || cfg.publish?.cargo?.enabled === true; - if (!cargoEnabled) return undefined; + if (!cargoEnabled) { + return undefined; + } return { name: 'Probe: cargo is on PATH (config enables cargo)', // The example's rust-toolchain step must have put cargo here; if it was @@ -149,7 +157,9 @@ function smokeJob(scenario: Scenario): Record { } const probe = runtimeProbe(scenario); - if (probe) steps.push(probe); + if (probe) { + steps.push(probe); + } steps.push({ name: `Dry-run release (${scenario.proves})`, diff --git a/scripts/generate-config-docs.ts b/scripts/generate-config-docs.ts index ad462d7e..a2266b6f 100644 --- a/scripts/generate-config-docs.ts +++ b/scripts/generate-config-docs.ts @@ -39,20 +39,32 @@ const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')) as Schema; // --------------------------------------------------------------------------- function ensurePeriod(str: string | undefined): string { - if (!str) return ''; + if (!str) { + return ''; + } const s = str.trim(); return s.endsWith('.') ? s : `${s}.`; } function fmtDefault(val: unknown): string { - if (val === undefined || val === null) return '—'; - if (typeof val === 'boolean') return `\`${val}\``; - if (typeof val === 'string') return `\`"${val}"\``; + if (val === undefined || val === null) { + return '—'; + } + if (typeof val === 'boolean') { + return `\`${val}\``; + } + if (typeof val === 'string') { + return `\`"${val}"\``; + } if (Array.isArray(val)) { - if (val.length === 0) return '`[]`'; + if (val.length === 0) { + return '`[]`'; + } return `\`${JSON.stringify(val)}\``; } - if (typeof val === 'number') return `\`${val}\``; + if (typeof val === 'number') { + return `\`${val}\``; + } return `\`${JSON.stringify(val)}\``; } @@ -63,9 +75,15 @@ function fmtType(prop: SchemaProperty): string { if (prop.oneOf) { return prop.oneOf .map((s) => { - if (s.type === 'boolean' && s.enum) return (s.enum as unknown[]).map((v) => `\`${v}\``).join(' | '); - if (s.type === 'boolean') return 'boolean'; - if (s.type === 'string' && s.enum) return s.enum.map((v) => `\`"${v}"\``).join(' | '); + if (s.type === 'boolean' && s.enum) { + return (s.enum as unknown[]).map((v) => `\`${v}\``).join(' | '); + } + if (s.type === 'boolean') { + return 'boolean'; + } + if (s.type === 'string' && s.enum) { + return s.enum.map((v) => `\`"${v}"\``).join(' | '); + } return s.type ?? 'any'; }) .join(' | '); @@ -74,9 +92,15 @@ function fmtType(prop: SchemaProperty): string { const itemType = prop.items?.type ?? 'any'; return `\`${itemType}[]\``; } - if (prop.type === 'object') return 'object'; - if (prop.type === 'integer') return 'integer'; - if (prop.type === 'number') return 'number'; + if (prop.type === 'object') { + return 'object'; + } + if (prop.type === 'integer') { + return 'integer'; + } + if (prop.type === 'number') { + return 'number'; + } return prop.type ?? '—'; } @@ -85,7 +109,9 @@ function escapePipes(str: string): string { } function propsTable(properties: Record): string { - if (!properties || Object.keys(properties).length === 0) return ''; + if (!properties || Object.keys(properties).length === 0) { + return ''; + } const rows = Object.entries(properties).map(([key, prop]) => { const type = escapePipes(fmtType(prop)); const def = fmtDefault(prop.default); @@ -312,7 +338,9 @@ function renderPublish(prop: SchemaProperty): void { // --------------------------------------------------------------------------- function renderOneOfBooleanObject(key: string, schemaProp: SchemaProperty): void { const objSchema = schemaProp.oneOf?.find((s) => s.type === 'object'); - if (!objSchema) return; + if (!objSchema) { + return; + } emit('Set to `false` to disable.', ''); diff --git a/scripts/generate-schema.ts b/scripts/generate-schema.ts index e064f3e3..a24df33f 100644 --- a/scripts/generate-schema.ts +++ b/scripts/generate-schema.ts @@ -163,9 +163,15 @@ function orderedKeywordKeys(keys: string[]): string[] { return [...keys].sort((a, b) => { const ia = KEY_ORDER.indexOf(a); const ib = KEY_ORDER.indexOf(b); - if (ia === -1 && ib === -1) return a.localeCompare(b); - if (ia === -1) return 1; - if (ib === -1) return -1; + if (ia === -1 && ib === -1) { + return a.localeCompare(b); + } + if (ia === -1) { + return 1; + } + if (ib === -1) { + return -1; + } return ia - ib; }); } @@ -249,8 +255,12 @@ function printUnifiedDiff(before: string, after: string, label: string): void { ['-u', `--label=${label} (committed)`, `--label=${label} (generated)`, beforePath, afterPath], { encoding: 'utf8' }, ); - if (result.stdout) process.stderr.write(result.stdout); - if (result.stderr) process.stderr.write(result.stderr); + if (result.stdout) { + process.stderr.write(result.stdout); + } + if (result.stderr) { + process.stderr.write(result.stderr); + } if (result.status === 127 || result.error) { // `diff` not on PATH — fall back to a line-by-line diff. Good enough // for diagnostic output on a (typically small) schema file. @@ -267,8 +277,12 @@ function printLineDiffFallback(before: string, after: string, label: string): vo const afterLines = after.split('\n'); for (let i = 0; i < Math.max(beforeLines.length, afterLines.length); i++) { if (beforeLines[i] !== afterLines[i]) { - if (beforeLines[i] !== undefined) process.stderr.write(`- ${beforeLines[i]}\n`); - if (afterLines[i] !== undefined) process.stderr.write(`+ ${afterLines[i]}\n`); + if (beforeLines[i] !== undefined) { + process.stderr.write(`- ${beforeLines[i]}\n`); + } + if (afterLines[i] !== undefined) { + process.stderr.write(`+ ${afterLines[i]}\n`); + } } } } diff --git a/scripts/run-action.mjs b/scripts/run-action.mjs index 227a295a..5d343648 100644 --- a/scripts/run-action.mjs +++ b/scripts/run-action.mjs @@ -7,7 +7,9 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; function normalizeBoolean(value, defaultValue = false) { - if (value === undefined || value === '') return defaultValue; + if (value === undefined || value === '') { + return defaultValue; + } return value === 'true'; } @@ -161,7 +163,9 @@ export function writeGateOutputs(stdout, verbose = false) { export function parseReleaseOutput(stdout, verbose = false) { const trimmed = stdout.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } let parsed; try { parsed = JSON.parse(trimmed); @@ -210,7 +214,9 @@ export function resolveReleaseTags({ parsedTags, gitTags, allowRecovery }) { function setOutput(name, value) { const outputPath = process.env.GITHUB_OUTPUT; - if (!outputPath) return; + if (!outputPath) { + return; + } const text = value === undefined ? '' : String(value); const delimiter = `releasekit_${randomBytes(8).toString('hex')}`; @@ -520,7 +526,9 @@ export function writeStandingPROutputs(stdout, verbose = false) { export function writeSummary(markdown) { const summaryPath = process.env.GITHUB_STEP_SUMMARY; - if (!summaryPath) return; + if (!summaryPath) { + return; + } try { fs.appendFileSync(summaryPath, markdown); } catch (err) { @@ -551,11 +559,21 @@ export function buildReleaseSummary(input, parsed, success) { } const settings = []; - if (input.bump) settings.push(`| Bump | \`${input.bump}\` |`); - if (input.target) settings.push(`| Target | \`${input.target}\` |`); - if (input.scope) settings.push(`| Scope | \`${input.scope}\` |`); - if (input.prerelease) settings.push(`| Prerelease | \`${input.prerelease}\` |`); - if (normalizeBoolean(input.stable)) settings.push(`| Stable | Yes |`); + if (input.bump) { + settings.push(`| Bump | \`${input.bump}\` |`); + } + if (input.target) { + settings.push(`| Target | \`${input.target}\` |`); + } + if (input.scope) { + settings.push(`| Scope | \`${input.scope}\` |`); + } + if (input.prerelease) { + settings.push(`| Prerelease | \`${input.prerelease}\` |`); + } + if (normalizeBoolean(input.stable)) { + settings.push(`| Stable | Yes |`); + } if (settings.length > 0) { lines.push('| Setting | Value |'); @@ -616,9 +634,15 @@ export function buildGateSummary(_input, parsed, success) { lines.push(`| Check | Result |`); lines.push(`|-------|--------|`); lines.push(`| Should release | ${icon} ${shouldRelease ? 'Yes' : 'No'} |`); - if (parsed?.bump) lines.push(`| Bump | \`${parsed.bump}\` |`); - if (parsed?.scope) lines.push(`| Scope | \`${parsed.scope}\` |`); - if (parsed?.target) lines.push(`| Target | \`${parsed.target}\` |`); + if (parsed?.bump) { + lines.push(`| Bump | \`${parsed.bump}\` |`); + } + if (parsed?.scope) { + lines.push(`| Scope | \`${parsed.scope}\` |`); + } + if (parsed?.target) { + lines.push(`| Target | \`${parsed.target}\` |`); + } lines.push(''); if (parsed?.blocked) { diff --git a/scripts/test-harness/index.mjs b/scripts/test-harness/index.mjs index 1a560273..21c18fee 100644 --- a/scripts/test-harness/index.mjs +++ b/scripts/test-harness/index.mjs @@ -140,13 +140,19 @@ try { encoding: 'utf-8', env: process.env, }); - if (result.stdout) console.log(result.stdout); - if (result.stderr) console.log(result.stderr); + if (result.stdout) { + console.log(result.stdout); + } + if (result.stderr) { + console.log(result.stderr); + } return result; }; const assert = (cond, msg) => { - if (!cond) throw new Error(`Backfill assertion failed: ${msg}`); + if (!cond) { + throw new Error(`Backfill assertion failed: ${msg}`); + } }; // 1. --all --apply: reconstruct every workspace package from the global tag series, write files. @@ -166,8 +172,12 @@ try { assert(body.includes(exp.date), `${exp.file} should be dated ${exp.date}`); assert(!body.includes(today), `${exp.file} should not be stamped with today's date (${today})`); // Entries are scoped to the package's own directory. - for (const s of exp.has) assert(body.includes(s), `${exp.file} should mention "${s}"`); - for (const s of exp.hasNot) assert(!body.includes(s), `${exp.file} should NOT mention "${s}" (path scoping)`); + for (const s of exp.has) { + assert(body.includes(s), `${exp.file} should mention "${s}"`); + } + for (const s of exp.hasNot) { + assert(!body.includes(s), `${exp.file} should NOT mention "${s}" (path scoping)`); + } console.log(`✓ ${exp.file}: dated ${exp.date}, scoped correctly`); } diff --git a/scripts/test-harness/run-action-local.mjs b/scripts/test-harness/run-action-local.mjs index 9b4db312..4a01dfc5 100644 --- a/scripts/test-harness/run-action-local.mjs +++ b/scripts/test-harness/run-action-local.mjs @@ -4,7 +4,9 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; function normalizeBoolean(value, defaultValue = false) { - if (value === undefined || value === '') return defaultValue; + if (value === undefined || value === '') { + return defaultValue; + } return value === 'true'; } @@ -78,7 +80,9 @@ function buildPreviewArgs(input) { export function parseReleaseOutput(stdout) { const trimmed = stdout.trim(); - if (!trimmed) return undefined; + if (!trimmed) { + return undefined; + } try { return JSON.parse(trimmed); } catch { diff --git a/test/integration/examples-smoke.spec.ts b/test/integration/examples-smoke.spec.ts index da3e5c58..46006068 100644 --- a/test/integration/examples-smoke.spec.ts +++ b/test/integration/examples-smoke.spec.ts @@ -67,7 +67,9 @@ describe('examples smoke-test generator', () => { it('should not add a cargo probe to npm-only scenarios', () => { for (const scenario of SCENARIOS) { - if (scenario.fixture === 'npm-cargo-monorepo') continue; + if (scenario.fixture === 'npm-cargo-monorepo') { + continue; + } const hasCargoProbe = stepsOf(`smoke-${scenario.id}`).some((s) => s.run === 'cargo --version'); expect(hasCargoProbe).toBe(false); } From e77ae1a1c114a618d5b392d1fad86de7d3b33179 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 20:31:10 +0000 Subject: [PATCH 5/6] chore(biome): shorten the noConsole override comment and suppression reasons Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e --- biome.jsonc | 4 +--- packages/notes/src/llm/messages.ts | 2 +- packages/release/src/preview/preview.ts | 3 +-- packages/version/src/utils/jsonOutput.ts | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 7ad7fcca..a2f58f29 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -128,9 +128,7 @@ } }, { - // Package code logs through the logger (core's, or version's utils/logging.ts). Exempt are the - // loggers themselves and the CLI command layer, which owns the terminal: the JSON result on - // stdout, the final error message on stderr. + // Use the logger, not console. The loggers and CLI command files are exempt. "includes": [ "packages/*/src/**", "!packages/core/src/logger.ts", diff --git a/packages/notes/src/llm/messages.ts b/packages/notes/src/llm/messages.ts index 84d0a829..f25b3ee5 100644 --- a/packages/notes/src/llm/messages.ts +++ b/packages/notes/src/llm/messages.ts @@ -19,6 +19,6 @@ export function debugLogMessages(providerName: string, messages: LLMMessage[]): const preview = msg.content.length > 300 ? `${msg.content.slice(0, 300)}…` : msg.content; lines.push(` [${msg.role}]: ${preview}`); } - // biome-ignore lint/suspicious/noConsole: RELEASEKIT_DEBUG is its own opt-in, so the dump prints whatever the log level or --quiet. + // biome-ignore lint/suspicious/noConsole: the debug dump bypasses the logger so --quiet can't hide it console.error(lines.join('\n')); } diff --git a/packages/release/src/preview/preview.ts b/packages/release/src/preview/preview.ts index 739f9db4..0143f36c 100644 --- a/packages/release/src/preview/preview.ts +++ b/packages/release/src/preview/preview.ts @@ -211,8 +211,7 @@ export async function runPreview(options: PreviewOptions): Promise { }); if (!context || !forge) { - // Dry-run mode or GitHub context unavailable — print to stdout - // biome-ignore lint/suspicious/noConsole: the comment body is the command's output here, not a diagnostic. + // biome-ignore lint/suspicious/noConsole: no PR to post to (dry run or no GitHub context), so print it console.log(commentBody); return; } diff --git a/packages/version/src/utils/jsonOutput.ts b/packages/version/src/utils/jsonOutput.ts index 99c528b5..1410b590 100644 --- a/packages/version/src/utils/jsonOutput.ts +++ b/packages/version/src/utils/jsonOutput.ts @@ -329,7 +329,7 @@ export function getJsonData(): VersionOutput { */ export function printJsonOutput(): void { if (_jsonOutputMode) { - // biome-ignore lint/suspicious/noConsole: the JSON result is the command's stdout output, not a diagnostic. + // biome-ignore lint/suspicious/noConsole: prints the --json result console.log(JSON.stringify(_jsonData, null, 2)); } } From 1ca5ea6beff076b124b666233d3ed5d556fcfff8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 20:35:17 +0000 Subject: [PATCH 6/6] chore(biome): reword the noConsole override and debug-dump suppression Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HQN3v9Z4f1mu3xK4Z7K44e --- biome.jsonc | 2 +- packages/notes/src/llm/messages.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index a2f58f29..3e031c27 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -128,7 +128,7 @@ } }, { - // Use the logger, not console. The loggers and CLI command files are exempt. + // Disallow console except in the loggers and CLI command files. "includes": [ "packages/*/src/**", "!packages/core/src/logger.ts", diff --git a/packages/notes/src/llm/messages.ts b/packages/notes/src/llm/messages.ts index f25b3ee5..bf404cce 100644 --- a/packages/notes/src/llm/messages.ts +++ b/packages/notes/src/llm/messages.ts @@ -19,6 +19,6 @@ export function debugLogMessages(providerName: string, messages: LLMMessage[]): const preview = msg.content.length > 300 ? `${msg.content.slice(0, 300)}…` : msg.content; lines.push(` [${msg.role}]: ${preview}`); } - // biome-ignore lint/suspicious/noConsole: the debug dump bypasses the logger so --quiet can't hide it + // biome-ignore lint/suspicious/noConsole: RELEASEKIT_DEBUG=1 should always print this; the logger would hide it under --quiet console.error(lines.join('\n')); }