diff --git a/README.md b/README.md index dd1dfad..dbc1c3e 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,35 @@ GTS_DOCKER_TAGS=1.0.1 1.0 As you can see, the resolved version is not the highest in the repository, but as it is the highest within its minor version the Docker tags include `1.0` as well. +## Configuration + +TODO + +### Configuration overrides + +You can override any configuration value from the command line using the `-c/--config-value` option. This is useful for one-off changes without modifying the config file: + +```shell +# Override JSON output indentation +git-that-semver -o json -c output.json.indent=2 + +# Configure environment variable prefix +git-that-semver -c output.env.prefix=CUSTOM_ + +# Enable/disable strategies +git-that-semver -c strategies.npm.enabled=true + +# Configure default branches (using JSON array syntax) +git-that-semver -c 'defaults.snapshot.defaultBranches=["main","develop"]' + +# Disable change request identifier for snapshots +git-that-semver -c defaults.snapshot.useChangeRequestIdentifier=false + +# Configure snapshot version template +git-that-semver -c 'defaults.snapshot.versionTpl={{ prefix }}-{{ commitIdentifier }}' +``` + ## TODOs - dynamic loading of SCM adapters? -- logging to STDERR colorizes to the output to red by default - this is overridden and works as long as the output is not redirected, but results in red output otherwise - any way to fix this? +- logging to STDERR colorizes to the output to red by default - this is overridden and works as long as the output is not redir diff --git a/bun.lock b/bun.lock index 2c57630..ce0ac66 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "chalk": "^5.4.1", "commander": "^13.1.0", "liquidjs": "^10.20.2", + "lodash-es": "^4.17.21", "merge-anything": "^6.0.2", "semver": "^7.6.3", "slug": "^10.0.0", @@ -17,6 +18,7 @@ "@commander-js/extra-typings": "^13.0.0", "@trivago/prettier-plugin-sort-imports": "^5.2.1", "@types/bun": "^1.1.18", + "@types/lodash-es": "^4.17.12", "@types/semver": "^7.5.8", "@types/slug": "^5.0.9", "husky": "^9.1.7", @@ -59,7 +61,11 @@ "@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.1", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-NDZndt0fmVThIx/8cExuJHLZagUVzfGCoVrwH9x6aZvwfBdkrDFTYujecek6X2WpG4uUFsVaPg5+aNQPSyjcmw=="], - "@types/bun": ["@types/bun@1.1.18", "", { "dependencies": { "bun-types": "1.1.44" } }, "sha512-gtw6cIv/8Q530D0BmoYnjEzR65SjVq2SaUE0NeU6tbm7QBMsTZ61/NBNERtK/FUJaoi7PiteUohK7JcrXBCkvw=="], + "@types/bun": ["@types/bun@1.2.0", "", { "dependencies": { "bun-types": "1.2.0" } }, "sha512-5N1JqdahfpBlAv4wy6svEYcd/YfO2GNrbL95JOmFx8nkE6dbK4R0oSE5SpBA4vBRqgrOUAXF8Dpiz+gi7r80SA=="], + + "@types/lodash": ["@types/lodash@4.17.14", "", {}, "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A=="], + + "@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="], "@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], @@ -77,7 +83,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bun-types": ["bun-types@1.1.44", "", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" } }, "sha512-jtcekoZeSINgEcHSISzhR13w/cyE+Fankw2Cpl4c0fN3lRmKVAX0i9ay4FyK4lOxUK1HG4HkuIlrPvXKz4Y7sw=="], + "bun-types": ["bun-types@1.2.0", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-KEaJxyZfbV/c4eyG0vyehDpYmBGreNiQbZIqvVHJwZ4BmeuWlNZ7EAzMN2Zcd7ailmS/tGVW0BgYbGf+lGEpWw=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], @@ -139,6 +145,8 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], "merge-anything": ["merge-anything@6.0.2", "", { "dependencies": { "is-what": "^5.0.1" } }, "sha512-U8x6DL/YVudOcf82B6hd8GFg+6gF6hEHYwzqdo67GrH6vnDZ5YBq6BYX3hHWyCnG3CcqJDB1a9tj9fzMI3RL9Q=="], diff --git a/index.ts b/index.ts index 76eca97..5c221a7 100644 --- a/index.ts +++ b/index.ts @@ -15,12 +15,18 @@ const program = new Command("git-that-semver") .version("0.0.1") .addOption( new Option( - "-c, --config-file ", + "-f, --config-file ", "Config file (git-that-semver.yaml)", ) .env("GTS_CONFIG_FILE") .default("git-that-semver.yaml"), ) + .addOption( + new Option( + "-c, --config-value ", + "Override config values (e.g. output.json.indent=2)", + ).default([]), + ) .addOption( new Option("--log-level ", "Log level") .env("GTS_LOG_LEVEL") @@ -29,13 +35,13 @@ const program = new Command("git-that-semver") ) .addOption( new Option( - "-e, --enable-strategies ", + "-e, --enable-strategy ", "Enable strategies by name", ).default([]), ) .addOption( new Option( - "-d, --disable-strategies ", + "-d, --disable-strategy ", "Disable strategies by name", ).default([]), ) @@ -58,9 +64,10 @@ logger.setLevel(LogLevel[program.opts().logLevel]); try { const config = await resolveConfig( path.resolve(program.opts().configFile), - [...program.opts().enableStrategies], - [...program.opts().disableStrategies], + [...program.opts().enableStrategy], + [...program.opts().disableStrategy], program.opts().outputFormat, + [...program.opts().configValue], ); if (program.opts().dumpConfig) { diff --git a/package.json b/package.json index 6f1f43a..cd3a414 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@commander-js/extra-typings": "^13.0.0", "@trivago/prettier-plugin-sort-imports": "^5.2.1", "@types/bun": "^1.1.18", + "@types/lodash-es": "^4.17.12", "@types/semver": "^7.5.8", "@types/slug": "^5.0.9", "husky": "^9.1.7", @@ -19,6 +20,7 @@ "chalk": "^5.4.1", "commander": "^13.1.0", "liquidjs": "^10.20.2", + "lodash-es": "^4.17.21", "merge-anything": "^6.0.2", "semver": "^7.6.3", "slug": "^10.0.0", diff --git a/src/config/index.ts b/src/config/index.ts index 1d96107..6f6348c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -3,6 +3,7 @@ import YAML from "yaml"; import { logger } from "../logging"; import defaultConfigContents from "./git-that-semver.default.yaml" with { type: "text" }; +import { applyConfigOverrides } from "./overrides"; import { Config } from "./types"; const configLogger = logger.childLogger("config"); @@ -12,6 +13,7 @@ export const resolveConfig = async ( enabledStrategies: string[], disabledStrategies: string[], outputFormat: string | undefined, + configOverrides: string[] = [], ): Promise => { const defaultConfig = YAML.parse(defaultConfigContents); configLogger.trace("Default config", defaultConfig); @@ -49,6 +51,11 @@ export const resolveConfig = async ( mergedConfig = merge(mergedConfig, { output: { type: outputFormat } }); } + if (configOverrides.length > 0) { + configLogger.trace("Config overrides", configOverrides); + mergedConfig = applyConfigOverrides(mergedConfig, configOverrides); + } + configLogger.trace("Merged config before strategy merge", mergedConfig); const defaultsWithoutBranchPrefixes = merge({}, mergedConfig.defaults ?? {}); diff --git a/src/config/overrides.ts b/src/config/overrides.ts new file mode 100644 index 0000000..198a9db --- /dev/null +++ b/src/config/overrides.ts @@ -0,0 +1,61 @@ +import { set } from "lodash-es"; + +import { logger } from "../logging"; + +const configLogger = logger.childLogger("config"); + +function parseConfigOverride(override: string): { path: string; value: any } { + if (!/^[^=]+=[^=]*$/.test(override)) { + throw new Error( + `Invalid config value format: ${override}. Expected format: path.to.config=value`, + ); + } + + const [path, value] = override.split("=", 2); + return { path, value: parseValue(value) }; +} + +function parseValue(value: string | undefined): any { + if (value === undefined) { + return undefined; + } + + // Handle boolean values + if (value.toLowerCase() === "true") return true; + if (value.toLowerCase() === "false") return false; + + // Handle array values (JSON array syntax) + if (value.startsWith("[") && value.endsWith("]")) { + try { + return JSON.parse(value); + } catch (e) { + throw new Error( + `Invalid array format: ${value}. Expected JSON array format like [value1,value2]`, + ); + } + } + + // Handle numeric values + const numValue = Number(value); + if (!isNaN(numValue) && value.trim() !== "") { + return numValue; + } + + // Return as string if no other type matches + return value; +} + +export function applyConfigOverrides( + config: Record, + overrides: string[], +): Record { + const result = { ...config }; + + for (const override of overrides) { + const { path, value } = parseConfigOverride(override); + configLogger.trace("Applying config override", { path, value }); + set(result, path, value); + } + + return result; +} diff --git a/src/config/types.ts b/src/config/types.ts index 1225a91..09f2e3e 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -42,6 +42,11 @@ export const OutputConfig = z.object({ env: z.object({ prefix: z.string().default(""), }), + json: z + .object({ + indent: z.number().optional(), + }) + .optional(), }); export const Config = z.object({ diff --git a/src/output/json.test.ts b/src/output/json.test.ts index 997a673..43381a0 100644 --- a/src/output/json.test.ts +++ b/src/output/json.test.ts @@ -20,13 +20,25 @@ describe("JsonOutputPrinter", () => { consoleSpy.mockRestore(); }); - const testPrintResult = (description: string, versionResult: any) => { - it(`should print ${description} as formatted JSON`, () => { - printer.printResult({} as Config, versionResult); + const testPrintResult = ( + description: string, + versionResult: any, + indent: number = 2, + ) => { + it(`should print ${description} as formatted JSON with indent ${indent}`, () => { + const config = { + output: { + json: { + indent, + }, + }, + } as Config; + + printer.printResult(config, versionResult); expect(consoleSpy).toHaveBeenCalledTimes(1); expect(consoleSpy).toHaveBeenCalledWith( - JSON.stringify(versionResult, null, 2), + JSON.stringify(versionResult, null, indent), ); const printedJson = JSON.parse(consoleSpy.mock.calls[0][0]); @@ -34,6 +46,31 @@ describe("JsonOutputPrinter", () => { }); }; - testPrintResult("release version result", releaseVersionResult); - testPrintResult("snapshot version result", snapshotVersionResult); + describe("with default indent (2)", () => { + testPrintResult("release version result", releaseVersionResult); + testPrintResult("snapshot version result", snapshotVersionResult); + }); + + describe("with custom indent", () => { + testPrintResult( + "release version result with no indent", + releaseVersionResult, + 0, + ); + testPrintResult( + "snapshot version result with no indent", + snapshotVersionResult, + 0, + ); + testPrintResult( + "release version result with indent 4", + releaseVersionResult, + 4, + ); + testPrintResult( + "snapshot version result with indent 4", + snapshotVersionResult, + 4, + ); + }); }); diff --git a/src/output/json.ts b/src/output/json.ts index 4143c6a..11924d9 100644 --- a/src/output/json.ts +++ b/src/output/json.ts @@ -3,7 +3,9 @@ import type { Config } from "../config/types"; import type { VersionResult } from "../version/versionResolver"; export class JsonOutputPrinter implements OutputPrinter { - printResult(_config: Config, versionResult: VersionResult) { - console.log(JSON.stringify(versionResult, null, 2)); + printResult(config: Config, versionResult: VersionResult) { + console.log( + JSON.stringify(versionResult, null, config.output.json?.indent), + ); } }