UNPKG

@bfra.me/semantic-release

Version:

Semantic Release shareable configuration and plugins for bfra.me.

597 lines (591 loc) 20.6 kB
// src/validation/schemas/core.ts import { z } from "zod"; var branchSpecSchema = z.union([ z.string().min(1), z.object({ name: z.string().min(1), channel: z.string().optional(), range: z.string().optional(), prerelease: z.union([z.boolean(), z.string()]).optional() }) ]); var pluginSpecSchema = z.union([ z.string().min(1), z.tuple([z.string().min(1), z.record(z.string(), z.unknown())]) ]); var globalConfigSchema = z.object({ // Core configuration extends: z.union([z.string(), z.array(z.string())]).optional(), branches: z.array(branchSpecSchema).optional(), repositoryUrl: z.string().url().optional(), tagFormat: z.string().optional(), plugins: z.array(pluginSpecSchema).optional(), // CI environment ci: z.boolean().optional(), dryRun: z.boolean().optional(), debug: z.boolean().optional(), // Git configuration preset: z.string().optional(), presetConfig: z.record(z.string(), z.unknown()).optional(), // Advanced options noCi: z.boolean().optional() // Environment variables will be validated separately }); // src/validation/schemas/plugins.ts import { z as z2 } from "zod"; var commitAnalyzerConfigSchema = z2.object({ preset: z2.string().optional(), config: z2.record(z2.string(), z2.unknown()).optional(), parserOpts: z2.record(z2.string(), z2.unknown()).optional(), releaseRules: z2.array( z2.object({ type: z2.string().optional(), scope: z2.string().optional(), subject: z2.string().optional(), release: z2.union([z2.literal("major"), z2.literal("minor"), z2.literal("patch"), z2.literal(false)]).optional(), breaking: z2.boolean().optional(), revert: z2.boolean().optional() }) ).optional(), presetConfig: z2.record(z2.string(), z2.unknown()).optional() }); var releaseNotesGeneratorConfigSchema = z2.object({ preset: z2.string().optional(), config: z2.record(z2.string(), z2.unknown()).optional(), parserOpts: z2.record(z2.string(), z2.unknown()).optional(), writerOpts: z2.object({ groupBy: z2.string().optional(), commitGroupsSort: z2.union([z2.string(), z2.unknown()]).optional(), commitsSort: z2.union([z2.string(), z2.unknown()]).optional(), noteGroupsSort: z2.union([z2.string(), z2.unknown()]).optional(), notesSort: z2.union([z2.string(), z2.unknown()]).optional(), generateOn: z2.union([z2.string(), z2.unknown()]).optional(), finalizeContext: z2.unknown().optional(), debug: z2.unknown().optional(), reverse: z2.boolean().optional(), includeDetails: z2.boolean().optional(), ignoreReverted: z2.boolean().optional(), doFlush: z2.boolean().optional(), mainTemplate: z2.string().optional(), headerPartial: z2.string().optional(), commitPartial: z2.string().optional(), footerPartial: z2.string().optional(), partials: z2.record(z2.string(), z2.string()).optional(), transform: z2.record(z2.string(), z2.unknown()).optional() }).optional(), presetConfig: z2.record(z2.string(), z2.unknown()).optional(), linkCompare: z2.boolean().optional(), linkReferences: z2.boolean().optional(), host: z2.string().optional(), owner: z2.string().optional(), repository: z2.string().optional(), repoUrl: z2.string().optional(), commit: z2.string().optional(), issue: z2.string().optional(), userUrlFormat: z2.string().optional() }); var changelogConfigSchema = z2.object({ changelogFile: z2.string().optional(), changelogTitle: z2.string().optional() }); var npmConfigSchema = z2.object({ npmPublish: z2.boolean().optional(), pkgRoot: z2.string().optional(), tarballDir: z2.string().optional() }); var githubConfigSchema = z2.object({ githubUrl: z2.string().optional(), githubApiPathPrefix: z2.string().optional(), proxy: z2.union([z2.string(), z2.record(z2.string(), z2.unknown())]).optional(), assets: z2.array( z2.union([ z2.string(), z2.object({ path: z2.string(), name: z2.string().optional(), label: z2.string().optional() }) ]) ).optional(), successComment: z2.union([z2.string(), z2.literal(false)]).optional(), failTitle: z2.string().optional(), failComment: z2.union([z2.string(), z2.literal(false)]).optional(), labels: z2.union([z2.array(z2.string()), z2.literal(false)]).optional(), assignees: z2.array(z2.string()).optional(), releasedLabels: z2.union([z2.array(z2.string()), z2.literal(false)]).optional(), addReleases: z2.union([z2.literal("bottom"), z2.literal("top"), z2.literal(false)]).optional(), draftRelease: z2.boolean().optional(), releaseBodyTemplate: z2.string().optional(), releaseNameTemplate: z2.string().optional(), discussionCategoryName: z2.string().optional() }); var gitConfigSchema = z2.object({ assets: z2.array(z2.string()).optional(), message: z2.string().optional() }); // src/validation/validator.ts import { z as z3 } from "zod"; var ValidationError = class extends Error { /** * The original Zod validation error with detailed issue information. */ zodError; /** * Optional path identifier for the validation context (e.g., 'global', 'plugin:npm'). */ path; /** * Create a new ValidationError instance. * * @param message - Human-readable error message * @param zodError - The original Zod validation error * @param path - Optional path identifier for context */ constructor(message, zodError, path) { super(message); this.name = "ValidationError"; this.zodError = zodError; this.path = path; } /** * Get formatted error messages for display to users. * * Transforms Zod validation issues into human-readable error messages * with path information, making it easier to identify and fix configuration problems. * * @returns Array of formatted error message strings * * @example * ```typescript * const errors = validationError.getFormattedErrors() * // ['branches: Expected array, got string', 'plugins.0: Expected string or array, got object'] * ``` */ getFormattedErrors() { return this.zodError.issues.map((issue) => { const path = issue.path.length > 0 ? issue.path.join(".") : "root"; return `${path}: ${issue.message}`; }); } }; var pluginSchemas = { "@semantic-release/commit-analyzer": commitAnalyzerConfigSchema, "@semantic-release/release-notes-generator": releaseNotesGeneratorConfigSchema, "@semantic-release/changelog": changelogConfigSchema, "@semantic-release/npm": npmConfigSchema, "@semantic-release/github": githubConfigSchema, "@semantic-release/git": gitConfigSchema }; function validateConfig(config) { try { const result = globalConfigSchema.parse(config); return { success: true, data: result }; } catch (error) { if (error instanceof z3.ZodError) { return { success: false, error: new ValidationError("Invalid semantic-release configuration", error, "global") }; } throw error; } } function validatePluginConfig(pluginName, config) { const schema = pluginSchemas[pluginName]; if (schema === void 0) { return { success: true, data: config }; } try { const result = schema.parse(config); return { success: true, data: result }; } catch (error) { if (error instanceof z3.ZodError) { return { success: false, error: new ValidationError( `Invalid configuration for plugin ${pluginName}`, error, pluginName ) }; } throw error; } } function validateCompleteConfig(config) { const errors = []; const globalResult = validateConfig(config); if (!globalResult.success) { errors.push(globalResult.error); return { success: false, errors }; } if (globalResult.data.plugins) { for (const [index, plugin] of globalResult.data.plugins.entries()) { if (Array.isArray(plugin) && plugin.length === 2) { const [pluginName, pluginConfig] = plugin; const pluginResult = validatePluginConfig(pluginName, pluginConfig); if (!pluginResult.success) { const contextualError = new ValidationError( `Plugin at index ${index}: ${pluginResult.error.message}`, pluginResult.error.zodError, `plugins.${index}` ); errors.push(contextualError); } } } } if (errors.length > 0) { return { success: false, errors }; } return { success: true, data: globalResult.data }; } // src/config/environment.ts import process from "process"; function detectEnvironment(explicitEnv) { if (explicitEnv) { return { environment: explicitEnv, autoDetected: false, source: "explicit", isCI: detectCI(), metadata: { nodeEnv: process.env.NODE_ENV, ciVendor: detectCIVendor(), branchName: detectBranchName() } }; } const nodeEnv = process.env.NODE_ENV?.toLowerCase(); if (nodeEnv != null && ["development", "staging", "production"].includes(nodeEnv)) { return { environment: nodeEnv, autoDetected: true, source: "NODE_ENV", isCI: detectCI(), metadata: { nodeEnv: process.env.NODE_ENV, ciVendor: detectCIVendor(), branchName: detectBranchName() } }; } const ciVendor = detectCIVendor(); if (process.env.CI === "true" && ciVendor != null) { const branchName = detectBranchName(); if (branchName?.includes("staging") || branchName?.includes("stage")) { return { environment: "staging", autoDetected: true, source: "CI_BRANCH", isCI: true, metadata: { nodeEnv: process.env.NODE_ENV, ciVendor: detectCIVendor(), branchName } }; } } const hasExplicitTestIndicators = process.argv.includes("--test") || process.env.npm_lifecycle_event === "test" || process.env.JEST_WORKER_ID !== void 0 && process.env.JEST_WORKER_ID !== ""; const hasVitestIndicator = process.env.VITEST === "true"; if (nodeEnv === "test" || hasExplicitTestIndicators || hasVitestIndicator) { return { environment: "test", autoDetected: true, source: hasExplicitTestIndicators || hasVitestIndicator ? "TEST_INDICATORS" : "NODE_ENV", isCI: detectCI(), metadata: { nodeEnv: process.env.NODE_ENV, ciVendor: detectCIVendor(), branchName: detectBranchName() } }; } return { environment: "production", autoDetected: true, source: "DEFAULT", isCI: detectCI(), metadata: { nodeEnv: process.env.NODE_ENV, ciVendor: detectCIVendor(), branchName: detectBranchName() } }; } function getEnvironmentTransformations(environment, context) { switch (environment) { case "development": { return { // Enable dry-run mode for safety in development dryRun: true, // Enable debug mode for better development experience debug: true, // Use local branches for development branches: ["main", "master", "develop", "development"], // Disable CI mode if not explicitly in CI ci: context.isCI, // Use development tag format for clarity // eslint-disable-next-line no-template-curly-in-string tagFormat: "v${version}-dev" }; } case "test": { return { // Always enable dry-run in test environments dryRun: true, // Enable CI mode for consistent test behavior ci: true, // Enable debug for test troubleshooting debug: true, // Use test-specific branches branches: ["main", "master"], // Use test tag format // eslint-disable-next-line no-template-curly-in-string tagFormat: "v${version}-test" }; } case "staging": { return { // Allow actual releases in staging but with staging tag dryRun: false, // Enable CI mode for staging ci: true, // Enable debug for staging troubleshooting debug: true, // Use staging-specific branches branches: ["staging", "stage", "main"], // Use staging tag format // eslint-disable-next-line no-template-curly-in-string tagFormat: "v${version}-staging" }; } case "production": { return { // Production releases are real dryRun: false, // Disable debug in production for clean output debug: false, // Enable CI mode in production ci: context.isCI, // Use production branches branches: ["main", "master"], // Use standard production tag format // eslint-disable-next-line no-template-curly-in-string tagFormat: "v${version}" }; } default: { return { dryRun: false, debug: false, ci: context.isCI, branches: ["main", "master"], // eslint-disable-next-line no-template-curly-in-string tagFormat: "v${version}" }; } } } function applyEnvironmentTransformations(config, environment, context) { const transformations = getEnvironmentTransformations(environment, context); const processedConfig = { ...config }; if (transformations.branches != null && processedConfig.branches === void 0) { processedConfig.branches = transformations.branches; } if (transformations.ci !== void 0 && processedConfig.ci === void 0) { processedConfig.ci = transformations.ci; } if (transformations.dryRun !== void 0 && processedConfig.dryRun === void 0) { processedConfig.dryRun = transformations.dryRun; } if (transformations.debug !== void 0 && processedConfig.debug === void 0) { processedConfig.debug = transformations.debug; } if (transformations.tagFormat != null && processedConfig.tagFormat === void 0) { processedConfig.tagFormat = transformations.tagFormat; } if (transformations.repositoryUrl != null && processedConfig.repositoryUrl === void 0) { processedConfig.repositoryUrl = transformations.repositoryUrl; } return processedConfig; } function detectCI() { return process.env.CI === "true" || process.env.CONTINUOUS_INTEGRATION === "true" || process.env.GITHUB_ACTIONS != null && process.env.GITHUB_ACTIONS !== "" || process.env.GITLAB_CI != null && process.env.GITLAB_CI !== "" || process.env.CIRCLECI != null && process.env.CIRCLECI !== "" || process.env.TRAVIS != null && process.env.TRAVIS !== "" || process.env.BUILDKITE != null && process.env.BUILDKITE !== "" || process.env.JENKINS_URL != null && process.env.JENKINS_URL !== ""; } function detectCIVendor() { if (process.env.GITHUB_ACTIONS != null && process.env.GITHUB_ACTIONS !== "") return "github-actions"; if (process.env.GITLAB_CI != null && process.env.GITLAB_CI !== "") return "gitlab-ci"; if (process.env.CIRCLECI != null && process.env.CIRCLECI !== "") return "circleci"; if (process.env.TRAVIS != null && process.env.TRAVIS !== "") return "travis"; if (process.env.BUILDKITE != null && process.env.BUILDKITE !== "") return "buildkite"; if (process.env.JENKINS_URL != null && process.env.JENKINS_URL !== "") return "jenkins"; return void 0; } function detectBranchName() { const envVars = [ process.env.GITHUB_REF_NAME, process.env.CI_COMMIT_REF_NAME, process.env.CIRCLE_BRANCH, process.env.TRAVIS_BRANCH, process.env.BUILDKITE_BRANCH, process.env.GIT_BRANCH ]; for (const envVar of envVars) { if (envVar != null && envVar !== "") { return envVar; } } return void 0; } var environmentPresets = { /** * Development environment preset with safe defaults. */ development: { branches: ["main", "master", "develop", "development"], dryRun: true, debug: true, ci: false, // eslint-disable-next-line no-template-curly-in-string tagFormat: "v${version}-dev" }, /** * Test environment preset optimized for testing. */ test: { branches: ["main", "master"], dryRun: true, debug: true, ci: true, // eslint-disable-next-line no-template-curly-in-string tagFormat: "v${version}-test" }, /** * Staging environment preset for pre-production testing. */ staging: { branches: ["staging", "stage", "main"], dryRun: false, debug: true, ci: true, // eslint-disable-next-line no-template-curly-in-string tagFormat: "v${version}-staging" }, /** * Production environment preset for production releases. */ production: { branches: ["main", "master"], dryRun: false, debug: false, ci: true, // eslint-disable-next-line no-template-curly-in-string tagFormat: "v${version}" } }; // src/config/define-config.ts function generateValidationSuggestions(errors) { const suggestions = []; for (const error of errors) { for (const issue of error.zodError.issues) { const path = issue.path.join(".") || "root"; if (issue.code === "invalid_type") { if (path === "branches" && issue.received === "string") { suggestions.push('\u2022 `branches` should be an array. Try: branches: ["main"]'); } else if (path.includes("plugins")) { suggestions.push( "\u2022 Plugin configurations should be either strings or [name, config] tuples" ); } else { suggestions.push(`\u2022 ${path}: Expected ${issue.expected}, got ${issue.received}`); } } else if (issue.code === "invalid_string" && "validation" in issue && issue.validation === "url") { suggestions.push( '\u2022 `repositoryUrl` must be a valid URL. Example: "https://github.com/owner/repo.git"' ); } else if (issue.code === "too_small" && "minimum" in issue) { if (issue.type === "string") { suggestions.push(`\u2022 ${path}: Cannot be empty`); } else if (issue.type === "array") { suggestions.push(`\u2022 ${path}: Must contain at least ${issue.minimum} item(s)`); } else { suggestions.push(`\u2022 ${path}: ${issue.message}`); } } else if (issue.code === "invalid_union") { if (path.includes("branches")) { suggestions.push("\u2022 Branch specifications must be strings or objects with name property"); } else if (path.includes("plugins")) { suggestions.push("\u2022 Plugins must be strings (plugin names) or [name, config] tuples"); } else { suggestions.push(`\u2022 ${path}: ${issue.message}`); } } else if (issue.code === "unrecognized_keys") { suggestions.push( `\u2022 ${path}: Unrecognized configuration options. Check the semantic-release documentation.` ); } else { suggestions.push(`\u2022 ${path}: ${issue.message}`); } } } if (suggestions.length > 0) { suggestions.push( "", "\u{1F4D6} For more help: https://semantic-release.gitbook.io/semantic-release/usage/configuration" ); } return suggestions.join("\n"); } function defineConfig(config, options = {}) { const { validate: shouldValidate = true, environment = "production" } = options; if (shouldValidate) { const validationResult = validateCompleteConfig(config); if (!validationResult.success) { const errorMessages = validationResult.errors.map((error) => { const formattedErrors = error.getFormattedErrors(); return formattedErrors.join(", "); }).join("; "); const suggestions = generateValidationSuggestions(validationResult.errors); const fullMessage = `Configuration validation failed: ${errorMessages}${suggestions ? ` Suggestions: ${suggestions}` : ""}`; const firstError = validationResult.errors[0]; if (firstError) { throw new ValidationError(fullMessage, firstError.zodError, "defineConfig"); } else { throw new Error(fullMessage); } } } const envContext = detectEnvironment(environment); const processedConfig = applyEnvironmentTransformations( config, envContext.environment, envContext ); return processedConfig; } export { branchSpecSchema, pluginSpecSchema, globalConfigSchema, commitAnalyzerConfigSchema, releaseNotesGeneratorConfigSchema, changelogConfigSchema, npmConfigSchema, githubConfigSchema, gitConfigSchema, ValidationError, validateConfig, validatePluginConfig, validateCompleteConfig, detectEnvironment, getEnvironmentTransformations, applyEnvironmentTransformations, environmentPresets, defineConfig }; //# sourceMappingURL=chunk-2II77SNF.js.map