From 2b2a4eb53f53c1adb72f05d45814bae8ce23e364 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:11:24 +0000 Subject: [PATCH 01/16] feat: add centralized action metadata system with proper array parsing - Add ACTION_INPUTS mapping for all GitHub Action inputs - Add createConfigFromInputs() function for dynamic config creation - Implement proper array parsing that distinguishes required vs optional - Required array inputs throw error on empty string - Optional array inputs return empty array on empty string - Add comprehensive type definitions for action metadata --- src/types/action-metadata.types.ts | 18 ++++ src/types/index.ts | 3 + src/utils/action-metadata.ts | 144 +++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 src/types/action-metadata.types.ts create mode 100644 src/utils/action-metadata.ts diff --git a/src/types/action-metadata.types.ts b/src/types/action-metadata.types.ts new file mode 100644 index 0000000..0850844 --- /dev/null +++ b/src/types/action-metadata.types.ts @@ -0,0 +1,18 @@ +import type { Config } from '@/types/config.types'; + +/** + * Metadata about GitHub Action inputs, including their types, defaults, and mapping to config properties. + * This serves as the single source of truth for action configuration. + * + * @todo update doc - defaults at runtime come from action.yml, testing see helpers/inputs.ts + */ +export interface ActionInputMetadata { + /** The config property name this input maps to */ + configKey: keyof Config; + + /** Whether this input is required */ + required: boolean; + + /** The input type for proper parsing */ + type: 'string' | 'boolean' | 'number' | 'array'; +} diff --git a/src/types/index.ts b/src/types/index.ts index 1cd04a7..ca0629d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,6 @@ +// Action metadata types +export * from './action-metadata.types'; + // Common types export * from './common.types'; diff --git a/src/utils/action-metadata.ts b/src/utils/action-metadata.ts new file mode 100644 index 0000000..0856b9f --- /dev/null +++ b/src/utils/action-metadata.ts @@ -0,0 +1,144 @@ +import type { ActionInputMetadata, Config } from '@/types'; +import { getBooleanInput, getInput } from '@actions/core'; + +/** + * Complete mapping of all GitHub Action inputs to their metadata. + * This is the single source of truth for input configuration. + * Note: defaultValue is removed as defaults come from action.yml at runtime + */ +export const ACTION_INPUTS: Record = { + 'major-keywords': { + configKey: 'majorKeywords', + required: true, + type: 'array', + }, + 'minor-keywords': { + configKey: 'minorKeywords', + required: true, + type: 'array', + }, + 'patch-keywords': { + configKey: 'patchKeywords', + required: true, + type: 'array', + }, + 'default-first-tag': { + configKey: 'defaultFirstTag', + required: true, + type: 'string', + }, + 'terraform-docs-version': { + configKey: 'terraformDocsVersion', + required: true, + type: 'string', + }, + 'delete-legacy-tags': { + configKey: 'deleteLegacyTags', + required: true, + type: 'boolean', + }, + 'disable-wiki': { + configKey: 'disableWiki', + required: true, + type: 'boolean', + }, + 'wiki-sidebar-changelog-max': { + configKey: 'wikiSidebarChangelogMax', + required: true, + type: 'number', + }, + 'disable-branding': { + configKey: 'disableBranding', + required: true, + type: 'boolean', + }, + 'module-path-ignore': { + configKey: 'modulePathIgnore', + required: false, + type: 'array', + }, + 'module-change-exclude-patterns': { + configKey: 'moduleChangeExcludePatterns', + required: false, + type: 'array', + }, + 'module-asset-exclude-patterns': { + configKey: 'moduleAssetExcludePatterns', + required: false, + type: 'array', + }, + 'use-ssh-source-format': { + configKey: 'useSSHSourceFormat', + required: true, + type: 'boolean', + }, + github_token: { + configKey: 'githubToken', + required: true, + type: 'string', + }, + 'tag-directory-separator': { + configKey: 'tagDirectorySeparator', + required: true, + type: 'string', + }, + 'use-version-prefix': { + configKey: 'useVersionPrefix', + required: true, + type: 'boolean', + }, +} as const; + +/** + * Creates a config object by reading inputs using GitHub Actions API and converting them + * according to the metadata definitions. This provides a dynamic way to build the config + * without manually mapping each input. + */ +export function createConfigFromInputs(): Config { + const config = {} as Config; + + for (const [inputName, metadata] of Object.entries(ACTION_INPUTS)) { + const { configKey, required, type } = metadata; + + try { + let value: unknown; + + if (type === 'boolean') { + // Use getBooleanInput for boolean types for proper parsing + value = getBooleanInput(inputName, { required }); + } else if (type === 'array') { + // Handle array inputs with special parsing + const input = getInput(inputName, { required }); + + if (!input || input.trim() === '') { + value = []; + } else { + value = Array.from( + new Set( + input + .split(',') + .map((item: string) => item.trim()) + .filter(Boolean), + ), + ); + } + } else if (type === 'number') { + // Handle number inputs with parseInt + const input = getInput(inputName, { required }); + value = Number.parseInt(input, 10); + } else { + // Handle string inputs + value = getInput(inputName, { required }); + } + + // Safely assign to config using the configKey + Object.assign(config, { [configKey]: value }); + } catch (error) { + throw new Error( + `Failed to process input '${inputName}': ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + return config; +} From 7b3cd79292c83157d7f1c8a732a047f312c72a60 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:12:33 +0000 Subject: [PATCH 02/16] refactor: update config system to use centralized action metadata - Replace manual input processing with createConfigFromInputs() - Add comprehensive validation for tag directory separator - Add validation for default first tag format - Simplify config initialization using metadata-driven approach - Update config mock to use real action metadata system - Improve error messages with more specific validation details --- __mocks__/config.ts | 49 +++++++++++---------------------- src/config.ts | 66 +++++++++++++++++---------------------------- 2 files changed, 41 insertions(+), 74 deletions(-) diff --git a/__mocks__/config.ts b/__mocks__/config.ts index 26a0a7e..6335d5c 100644 --- a/__mocks__/config.ts +++ b/__mocks__/config.ts @@ -1,4 +1,7 @@ +import { ACTION_INPUTS, createConfigFromInputs } from '@/utils/action-metadata'; import type { Config } from '@/types'; +import type { ActionInputMetadata } from '@/types'; +import { setupTestInputs } from '@/tests/helpers/inputs'; /** * Configuration interface with added utility methods @@ -9,44 +12,24 @@ interface ConfigWithMethods extends Config { } /** - * Default configuration object. + * Load default configuration from action.yml. */ -const defaultConfig: Config = { - majorKeywords: ['BREAKING CHANGE', '!', 'MAJOR CHANGE'], - minorKeywords: ['feat', 'feature'], - patchKeywords: ['fix', 'chore'], - defaultFirstTag: 'v1.0.0', - terraformDocsVersion: 'v0.20.0', - deleteLegacyTags: false, - disableWiki: false, - wikiSidebarChangelogMax: 10, - disableBranding: false, - modulePathIgnore: ['tf-modules/kms/examples/complete'], - moduleChangeExcludePatterns: ['.gitignore', '*.md'], - moduleAssetExcludePatterns: ['tests/**', 'examples/**'], - githubToken: 'ghp_test_token_2c6912E7710c838347Ae178B4', - useSSHSourceFormat: false, +const createDefaultConfig = (): Config => { + setupTestInputs(); + return createConfigFromInputs(); }; /** - * Valid configuration keys. + * Default configuration object loaded from action.yml. */ -const validConfigKeys = [ - 'majorKeywords', - 'minorKeywords', - 'patchKeywords', - 'defaultFirstTag', - 'terraformDocsVersion', - 'deleteLegacyTags', - 'disableWiki', - 'wikiSidebarChangelogMax', - 'disableBranding', - 'modulePathIgnore', - 'moduleChangeExcludePatterns', - 'moduleAssetExcludePatterns', - 'githubToken', - 'useSSHSourceFormat', -] as const; +const defaultConfig: Config = createDefaultConfig(); + +/** + * Valid configuration keys derived from ACTION_INPUTS. + */ +const validConfigKeys = (Object.values(ACTION_INPUTS) as ActionInputMetadata[]).map( + (metadata) => metadata.configKey, +) as Array; type ValidConfigKey = (typeof validConfigKeys)[number]; diff --git a/src/config.ts b/src/config.ts index 4539021..bc9933f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,30 +1,11 @@ import type { Config } from '@/types'; -import { endGroup, getBooleanInput, getInput, info, startGroup } from '@actions/core'; +import { createConfigFromInputs } from '@/utils/action-metadata'; +import { VERSION_TAG_REGEX } from '@/utils/constants'; +import { endGroup, info, startGroup } from '@actions/core'; // Keep configInstance private to this module let configInstance: Config | null = null; -/** - * Retrieves an array of values from a comma-separated input string. Duplicates any empty values - * are removed and each value is trimmed of whitespace. - * - * @param inputName - Name of the input to retrieve. - * @param required - Whether the input is required. - * @returns An array of trimmed and filtered values. - */ -const getArrayInput = (inputName: string, required: boolean): string[] => { - const input = getInput(inputName, { required }); - - return Array.from( - new Set( - input - .split(',') - .map((item: string) => item.trim()) - .filter(Boolean), - ), - ); -}; - /** * Clears the cached config instance during testing. * @@ -58,23 +39,8 @@ function initializeConfig(): Config { try { startGroup('Initializing Config'); - // Initialize the config instance - configInstance = { - majorKeywords: getArrayInput('major-keywords', true), - minorKeywords: getArrayInput('minor-keywords', true), - patchKeywords: getArrayInput('patch-keywords', true), - defaultFirstTag: getInput('default-first-tag', { required: true }), - terraformDocsVersion: getInput('terraform-docs-version', { required: true }), - deleteLegacyTags: getBooleanInput('delete-legacy-tags', { required: true }), - disableWiki: getBooleanInput('disable-wiki', { required: true }), - wikiSidebarChangelogMax: Number.parseInt(getInput('wiki-sidebar-changelog-max', { required: true }), 10), - disableBranding: getBooleanInput('disable-branding', { required: true }), - githubToken: getInput('github_token', { required: true }), - modulePathIgnore: getArrayInput('module-path-ignore', false), - moduleChangeExcludePatterns: getArrayInput('module-change-exclude-patterns', false), - moduleAssetExcludePatterns: getArrayInput('module-asset-exclude-patterns', false), - useSSHSourceFormat: getBooleanInput('use-ssh-source-format', { required: true }), - }; + // Initialize the config instance using action metadata + configInstance = createConfigFromInputs(); // Validate that *.tf is not in excludePatterns if (configInstance.moduleChangeExcludePatterns.some((pattern) => pattern === '*.tf')) { @@ -84,13 +50,29 @@ function initializeConfig(): Config { throw new TypeError('Asset exclude patterns cannot contain "*.tf" as these files are required'); } - // Validate that we have a valid first tag. For now, must be v#.#.# - // Validate WikiSidebar Changelog Max is a number and greater than zero if (configInstance.wikiSidebarChangelogMax < 1 || Number.isNaN(configInstance.wikiSidebarChangelogMax)) { throw new TypeError('Wiki Sidebar Change Log Max must be an integer greater than or equal to one'); } + // Validate tag directory separator + const validSeparators = ['-', '_', '/', '.']; + if (configInstance.tagDirectorySeparator.length !== 1) { + throw new TypeError('Tag directory separator must be exactly one character'); + } + if (!validSeparators.includes(configInstance.tagDirectorySeparator)) { + throw new TypeError( + `Tag directory separator must be one of: ${validSeparators.join(', ')}. Got: '${configInstance.tagDirectorySeparator}'`, + ); + } + + // Validate default first tag format + if (!VERSION_TAG_REGEX.test(configInstance.defaultFirstTag)) { + throw new TypeError( + `Default first tag must be in format v#.#.# or #.#.# (e.g., v1.0.0 or 1.0.0). Got: '${configInstance.defaultFirstTag}'`, + ); + } + info(`Major Keywords: ${configInstance.majorKeywords.join(', ')}`); info(`Minor Keywords: ${configInstance.minorKeywords.join(', ')}`); info(`Patch Keywords: ${configInstance.patchKeywords.join(', ')}`); @@ -103,6 +85,8 @@ function initializeConfig(): Config { info(`Module Change Exclude Patterns: ${configInstance.moduleChangeExcludePatterns.join(', ')}`); info(`Module Asset Exclude Patterns: ${configInstance.moduleAssetExcludePatterns.join(', ')}`); info(`Use SSH Source Format: ${configInstance.useSSHSourceFormat}`); + info(`Tag Directory Separator: ${configInstance.tagDirectorySeparator}`); + info(`Use Version Prefix: ${configInstance.useVersionPrefix}`); return configInstance; } finally { From 19ffbfd9e001ee7bb641b02d941f65755847b89f Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:20:08 +0000 Subject: [PATCH 03/16] test: improve test infrastructure and input helpers - Refactor input helpers to use centralized ACTION_INPUTS metadata - Add getActionDefaults() to load defaults from action.yml - Generate input type arrays dynamically from metadata - Add getConfigKey() helper for type-safe config access - Simplify setupTestInputs() to use action.yml defaults - Remove hardcoded input arrays in favor of metadata-driven approach --- __tests__/_setup.ts | 6 +- __tests__/helpers/action-defaults.ts | 48 ++++++++ __tests__/helpers/inputs.ts | 170 +++++++++++---------------- 3 files changed, 122 insertions(+), 102 deletions(-) create mode 100644 __tests__/helpers/action-defaults.ts diff --git a/__tests__/_setup.ts b/__tests__/_setup.ts index 7a9ac6f..da84e9c 100644 --- a/__tests__/_setup.ts +++ b/__tests__/_setup.ts @@ -1,3 +1,4 @@ +import { setupTestInputs } from '@/tests/helpers/inputs'; import { afterEach, beforeEach, vi } from 'vitest'; // Mocked node modules (./__mocks__/*) @@ -20,11 +21,14 @@ const defaultEnvironmentVariables = { }; beforeEach(() => { - // Initialize environment + // Initialize GitHub mock pull request environment for (const [key, value] of Object.entries(defaultEnvironmentVariables)) { vi.stubEnv(key, value); } + // Set up action input defaults for testing + setupTestInputs(); + // Clear all mocked functions usage data and state vi.clearAllMocks(); }); diff --git a/__tests__/helpers/action-defaults.ts b/__tests__/helpers/action-defaults.ts new file mode 100644 index 0000000..f647dc6 --- /dev/null +++ b/__tests__/helpers/action-defaults.ts @@ -0,0 +1,48 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { ACTION_INPUTS } from '@/utils/action-metadata'; +import * as yaml from 'js-yaml'; + +/** + * Interface for action.yml structure + */ +interface ActionYml { + inputs: Record< + string, + { + description: string; + required: boolean; + default?: string; + } + >; +} + +/** + * Gets action defaults from action.yml file. + * Returns a record of all input names to their default values (or undefined if no default). + */ +/** + * Extracts default values for GitHub Action inputs from action.yml file. + * + * This function reads the action.yml file from the current working directory, + * parses its content, and retrieves the default values for all inputs defined + * in ACTION_INPUTS. + * + * @returns A record mapping each input name to its default value from the action.yml file. + * If an input has no default value, its entry will contain undefined. + */ +export function getActionDefaults(): Record { + const actionYmlPath = path.join(process.cwd(), 'action.yml'); + const actionYmlContent = fs.readFileSync(actionYmlPath, 'utf8'); + const actionYml = yaml.load(actionYmlContent) as ActionYml; + + const defaults: Record = {}; + + // Process all inputs from ACTION_INPUTS to ensure we have entries for all inputs + for (const [inputName] of Object.entries(ACTION_INPUTS)) { + const actionInput = actionYml.inputs[inputName]; + defaults[inputName] = actionInput?.default; + } + + return defaults; +} diff --git a/__tests__/helpers/inputs.ts b/__tests__/helpers/inputs.ts index 5fa41ad..2a0526f 100644 --- a/__tests__/helpers/inputs.ts +++ b/__tests__/helpers/inputs.ts @@ -1,121 +1,89 @@ +import { getActionDefaults } from '@/tests/helpers/action-defaults'; import type { Config } from '@/types'; +import { ACTION_INPUTS } from '@/utils/action-metadata'; import { vi } from 'vitest'; -const INPUT_KEY = 'INPUT_'; +// Load action defaults once globally +const ACTION_DEFAULTS = getActionDefaults(); + +// Generate input type arrays from centralized metadata +export const requiredInputs = Object.entries(ACTION_INPUTS) + .filter(([, metadata]) => metadata.required) + .map(([inputName]) => inputName); + +export const optionalInputs = Object.entries(ACTION_INPUTS) + .filter(([, metadata]) => !metadata.required) + .map(([inputName]) => inputName); + +export const booleanInputs = Object.entries(ACTION_INPUTS) + .filter(([, metadata]) => metadata.type === 'boolean') + .map(([inputName]) => inputName); + +export const arrayInputs = Object.entries(ACTION_INPUTS) + .filter(([, metadata]) => metadata.type === 'array') + .map(([inputName]) => inputName); + +export const stringInputs = Object.entries(ACTION_INPUTS) + .filter(([, metadata]) => metadata.type === 'string') + .map(([inputName]) => inputName); + +export const numberInputs = Object.entries(ACTION_INPUTS) + .filter(([, metadata]) => metadata.type === 'number') + .map(([inputName]) => inputName); /** - * Type-safe mapping from input names to config keys. - * This ensures that each value is a valid key in the Config type. + * Converts an input name to its corresponding config key. + * @param inputName The input name (e.g., 'github_token', 'module-path-ignore') + * @returns The corresponding config key as a keyof Config */ -export const inputToConfigKeyMap: Record = { - 'major-keywords': 'majorKeywords', - 'minor-keywords': 'minorKeywords', - 'patch-keywords': 'patchKeywords', - 'default-first-tag': 'defaultFirstTag', - 'terraform-docs-version': 'terraformDocsVersion', - 'delete-legacy-tags': 'deleteLegacyTags', - 'disable-wiki': 'disableWiki', - 'wiki-sidebar-changelog-max': 'wikiSidebarChangelogMax', - 'disable-branding': 'disableBranding', - github_token: 'githubToken', - 'module-path-ignore': 'modulePathIgnore', - 'module-change-exclude-patterns': 'moduleChangeExcludePatterns', - 'module-asset-exclude-patterns': 'moduleAssetExcludePatterns', - 'use-ssh-source-format': 'useSSHSourceFormat', -}; +export function getConfigKey(inputName: string): keyof Config { + const metadata = ACTION_INPUTS[inputName]; + if (!metadata) { + throw new Error(`Unknown input: ${inputName}`); + } -// Create reverse mapping from config keys to input names -export const configKeyToInputMap = Object.entries(inputToConfigKeyMap).reduce( - (acc, [inputName, configKey]) => { - acc[configKey] = inputName; - return acc; - }, - {} as Record, -); + return metadata.configKey; +} -// Default inputs used for testing @actions/core behavior -export const defaultInputs = { - 'major-keywords': 'MAJOR CHANGE,BREAKING CHANGE,!', - 'minor-keywords': 'feat,feature', - 'patch-keywords': 'fix,chore', - 'default-first-tag': 'v0.1.0', - 'terraform-docs-version': 'v0.19.0', - 'delete-legacy-tags': 'false', - 'disable-wiki': 'false', - 'wiki-sidebar-changelog-max': '10', - 'disable-branding': 'false', - 'module-path-ignore': 'tf-modules/kms/examples/complete', - 'module-change-exclude-patterns': '.gitignore,*.md', - 'module-asset-exclude-patterns': 'tests/**,examples/**', - github_token: 'ghp_test_token_2c6912E7710c838347Ae178B4', - 'use-ssh-source-format': 'false', -}; -export const requiredInputs = [ - 'major-keywords', - 'minor-keywords', - 'patch-keywords', - 'default-first-tag', - 'terraform-docs-version', - 'delete-legacy-tags', - 'disable-wiki', - 'wiki-sidebar-changelog-max', - 'disable-branding', - 'github_token', - 'use-ssh-source-format', -]; -export const optionalInputs = Object.keys(defaultInputs).filter((key) => !requiredInputs.includes(key)); -export const booleanInputs = ['delete-legacy-tags', 'disable-wiki', 'disable-branding', 'use-ssh-source-format']; -export const arrayInputs = [ - 'major-keywords', - 'minor-keywords', - 'patch-keywords', - 'module-path-ignore', - 'module-change-exclude-patterns', - 'module-asset-exclude-patterns', -]; -export const stringInputs = ['default-first-tag', 'terraform-docs-version', 'github_token']; -export const numberInputs = ['wiki-sidebar-changelog-max']; +/** + * Converts an input name to its corresponding environment variable name. + * This is the exact inverse of what @actions/core getInput() does. + * @param inputName The input name (e.g., 'github_token', 'module-path-ignore') + * @returns The environment variable name (e.g., 'INPUT_GITHUB_TOKEN', 'INPUT_MODULE_PATH_IGNORE') + */ +function inputToEnvVar(inputName: string): string { + return `INPUT_${inputName.replace(/ /g, '_').toUpperCase()}`; +} /** - * Converts a dash-case input name to its corresponding camelCase config key - * Prefer using the inputToConfigKeyMap directly for known input keys + * Sets up test environment with action defaults and optional overrides. + * This replaces the previous stubInputEnv function with a cleaner approach + * that loads defaults from action.yml and applies test-specific overrides. * - * @param inputName The input name to convert - * @returns The corresponding config key as a string + * @param overrides - Test-specific overrides for input values. */ -export function inputToConfigKey(inputName: string): string { - // Check if the input name is in our mapping first - if (inputName in inputToConfigKeyMap) { - return inputToConfigKeyMap[inputName]; - } +export function setupTestInputs(overrides: Record = {}) { + // Start with action.yml defaults and apply test-specific defaults + const allInputs = { + ...ACTION_DEFAULTS, + github_token: 'ghp_test_token_2c6912E7710c838347Ae178B4', + ...overrides, + }; - // Fallback to the conversion logic - return inputName.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + // Set environment variables for all values (undefined is valid for vi.stubEnv) + for (const [inputName, value] of Object.entries(allInputs)) { + vi.stubEnv(inputToEnvVar(inputName), value); + } } /** - * Stubs environment variables with an `INPUT_` prefix using a set of default values, - * while allowing specific overrides. By default, this function sets a baseline of - * sane default environment values that would typically be used in tests. + * Clears a specific action input environment variable. * - * Overrides can be provided as key-value pairs, where: - * - A `string` value sets or replaces the environment variable. - * - A `null` value skips the setting, allowing for flexibility in customizing the stubbed environment. + * Useful for testing scenarios where you need to remove a specific input. Wrapper around + * vi.stubEnv which has an unsual syntax for clearing environment variables/ * - * @param {Record} overrides - An object specifying environment variable overrides. - * Keys in this object correspond to the environment variable names (without the `INPUT_` prefix), - * and values specify the desired values or `null` to skip setting. + * @param inputName The input name to clear (e.g., 'github_token', 'module-path-ignore') */ -export function stubInputEnv(inputs: Record = {}) { - // Merge default inputs with overrides, giving precedence to overrides - const mergedInputs = { ...defaultInputs, ...inputs }; - - for (const [key, value] of Object.entries(mergedInputs)) { - if (value === null) { - continue; - } - - const prefixedKey = `${INPUT_KEY}${key.replace(/ /g, '_').toUpperCase()}`; - vi.stubEnv(prefixedKey, value); - } +export function clearEnvironmentInput(inputName: string): void { + vi.stubEnv(inputToEnvVar(inputName), undefined); } From 2b9b3d4af9d8faca5d02ee930cd34a2e97bf5097 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:21:04 +0000 Subject: [PATCH 04/16] test: add comprehensive config validation test coverage - Add tests for tag directory separator validation (length and character) - Add tests for default first tag format validation - Add tests for required vs optional array input handling - Add tests for array parsing edge cases and deduplication - Improve test organization with metadata-driven approach - Achieve 100% test coverage for config.ts validation logic --- __tests__/config.test.ts | 212 +++++++++++++++++++++++++-------------- 1 file changed, 138 insertions(+), 74 deletions(-) diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index be14bc2..a3647f1 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -2,14 +2,13 @@ import { clearConfigForTesting, config, getConfig } from '@/config'; import { arrayInputs, booleanInputs, - inputToConfigKey, - inputToConfigKeyMap, + clearEnvironmentInput, + getConfigKey, optionalInputs, requiredInputs, + setupTestInputs, stringInputs, - stubInputEnv, } from '@/tests/helpers/inputs'; -import type { Config } from '@/types'; import { endGroup, getBooleanInput, getInput, info, startGroup } from '@actions/core'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -22,38 +21,40 @@ describe('config', () => { }); beforeEach(() => { + // The config is cached. To ensure each test starts with a clean slate, we implicity clear it. + // We don't do this globally in setup as it's not necessary for all tests. clearConfigForTesting(); - // Note: We don't stubInputEnv here as there are cases where we want to test default inputs and - // in this case we would have to do a full reset of the config. }); describe('input validation', () => { for (const input of requiredInputs) { it(`should throw error when required input "${input}" is missing`, () => { - stubInputEnv({ [input]: null }); - expect(() => getConfig()).toThrow(new Error(`Input required and not supplied: ${input}`)); + clearEnvironmentInput(input); + expect(() => getConfig()).toThrow( + new Error(`Failed to process input '${input}': Input required and not supplied: ${input}`), + ); expect(getInput).toHaveBeenCalled(); }); } for (const input of optionalInputs) { it(`should handle optional input "${input}" when not present`, () => { - stubInputEnv({ [input]: null }); + clearEnvironmentInput(input); // Simply verify it doesn't throw without the specific error object expect(() => getConfig()).not.toThrow(); // Get the config and check the actual value const config = getConfig(); - // Get the config key using the mapping directly if possible - const configKey = inputToConfigKeyMap[input] || inputToConfigKey(input); + // Get the config key using the new getConfigKey function + const configKey = getConfigKey(input); // Type-safe access using the mapping if (arrayInputs.includes(input)) { - // Cast configKey to keyof Config to ensure type safety - expect(config[configKey as keyof Config]).toEqual([]); + // Now configKey is properly typed as keyof Config + expect(config[configKey]).toEqual([]); } if (stringInputs.includes(input)) { - expect(config[configKey as keyof Config]).toEqual(''); + expect(config[configKey]).toEqual(''); } expect(getInput).toHaveBeenCalled(); @@ -62,10 +63,10 @@ describe('config', () => { for (const input of booleanInputs) { it(`should throw error when input "${input}" has an invalid boolean value`, () => { - stubInputEnv({ [input]: 'invalid-boolean' }); + setupTestInputs({ [input]: 'invalid-boolean' }); expect(() => getConfig()).toThrow( - new TypeError( - `Input does not meet YAML 1.2 "Core Schema" specification: ${input}\nSupport boolean input list: \`true | True | TRUE | false | False | FALSE\``, + new Error( + `Failed to process input '${input}': Input does not meet YAML 1.2 "Core Schema" specification: ${input}\nSupport boolean input list: \`true | True | TRUE | false | False | FALSE\``, ), ); expect(getBooleanInput).toHaveBeenCalled(); @@ -73,20 +74,20 @@ describe('config', () => { } it('should throw error when moduleChangeExcludePatterns includes *.tf', () => { - stubInputEnv({ 'module-change-exclude-patterns': '*.tf,tests/**' }); + setupTestInputs({ 'module-change-exclude-patterns': '*.tf,tests/**' }); expect(() => getConfig()).toThrow( new TypeError('Exclude patterns cannot contain "*.tf" as it is required for module detection'), ); }); it('should throw error when moduleAssetExcludePatterns includes *.tf', () => { - stubInputEnv({ 'module-asset-exclude-patterns': '*.tf,tests/**' }); + setupTestInputs({ 'module-asset-exclude-patterns': '*.tf,tests/**' }); expect(() => getConfig()).toThrow( new TypeError('Asset exclude patterns cannot contain "*.tf" as these files are required'), ); }); - it('should handle boolean conversions for various formats', async () => { + it('should handle boolean conversions for various formats', () => { const booleanCases = ['true', 'True', 'TRUE', 'false', 'False', 'FALSE']; for (const booleanValue of booleanCases) { @@ -100,36 +101,124 @@ describe('config', () => { return acc; }, {}); - stubInputEnv(booleanInputValuesTest); + setupTestInputs(booleanInputValuesTest); // Check the boolean conversion for each key in booleanInputs const config = getConfig(); for (const booleanInput of booleanInputs) { // Get config key from the mapping, which is already typed as keyof Config - const configKey = inputToConfigKeyMap[booleanInput]; + const configKey = getConfigKey(booleanInput); expect(config[configKey]).toBe(booleanValue.toLowerCase() === 'true'); } } }); + it('should handle array input parsing and deduplication', () => { + const arrayTestCases = [ + { input: 'item1,item2,item3', expected: ['item1', 'item2', 'item3'] }, + { input: ' item4 , item5 , item6 ', expected: ['item4', 'item5', 'item6'] }, + { input: 'item7,item7,item8', expected: ['item7', 'item8'] }, + { input: 'item10,,item11,,,item12', expected: ['item10', 'item11', 'item12'] }, + ]; + + for (const testCase of arrayTestCases) { + // Ensure we reset the configuration since this is looping inside the test + clearConfigForTesting(); + vi.unstubAllEnvs(); + + // Create test inputs for all array inputs + const arrayInputValuesTest = arrayInputs.reduce((acc: Record, key) => { + acc[key] = testCase.input; + return acc; + }, {}); + + setupTestInputs(arrayInputValuesTest); + + // Check array parsing for each array input + const config = getConfig(); + for (const arrayInput of arrayInputs) { + const configKey = getConfigKey(arrayInput); + expect(config[configKey]).toEqual(testCase.expected); + } + } + }); + + it('should throw error for required array inputs when empty string is provided', () => { + const requiredArrayInputs = arrayInputs.filter((input) => requiredInputs.includes(input)); + + for (const input of requiredArrayInputs) { + clearConfigForTesting(); + vi.unstubAllEnvs(); + + // Set the required array input to empty string + setupTestInputs({ [input]: '' }); + + expect(() => getConfig()).toThrow( + new Error(`Failed to process input '${input}': Input required and not supplied: ${input}`), + ); + } + }); + + it('should return empty array for optional array inputs when empty string is provided', () => { + const optionalArrayInputs = arrayInputs.filter((input) => optionalInputs.includes(input)); + + for (const input of optionalArrayInputs) { + clearConfigForTesting(); + vi.unstubAllEnvs(); + + // Set the optional array input to empty string + setupTestInputs({ [input]: '' }); + + const config = getConfig(); + const configKey = getConfigKey(input); + expect(config[configKey]).toEqual([]); + } + }); + it('should throw error for non-numeric wiki-sidebar-changelog-max', () => { - stubInputEnv({ 'wiki-sidebar-changelog-max': 'invalid' }); + setupTestInputs({ 'wiki-sidebar-changelog-max': 'invalid' }); expect(() => getConfig()).toThrow( new TypeError('Wiki Sidebar Change Log Max must be an integer greater than or equal to one'), ); }); it('should throw error for 0 wiki-sidebar-changelog-max', () => { - stubInputEnv({ 'wiki-sidebar-changelog-max': '0' }); + setupTestInputs({ 'wiki-sidebar-changelog-max': '0' }); expect(() => getConfig()).toThrow( new TypeError('Wiki Sidebar Change Log Max must be an integer greater than or equal to one'), ); }); + + it('should throw error for invalid tag directory separator length', () => { + setupTestInputs({ 'tag-directory-separator': 'ab' }); + expect(() => getConfig()).toThrow( + new TypeError('Tag directory separator must be exactly one character'), + ); + }); + + it('should throw error for invalid tag directory separator character', () => { + setupTestInputs({ 'tag-directory-separator': '@' }); + expect(() => getConfig()).toThrow( + new TypeError("Tag directory separator must be one of: -, _, /, .. Got: '@'"), + ); + }); + + it('should throw error for invalid default first tag format', () => { + setupTestInputs({ 'default-first-tag': 'invalid-tag' }); + expect(() => getConfig()).toThrow( + new TypeError("Default first tag must be in format v#.#.# or #.#.# (e.g., v1.0.0 or 1.0.0). Got: 'invalid-tag'"), + ); + + clearConfigForTesting(); + setupTestInputs({ 'default-first-tag': 'v1.0' }); + expect(() => getConfig()).toThrow( + new TypeError("Default first tag must be in format v#.#.# or #.#.# (e.g., v1.0.0 or 1.0.0). Got: 'v1.0'"), + ); + }); }); describe('initialization', () => { it('should maintain singleton instance across multiple imports', () => { - stubInputEnv(); const firstInstance = getConfig(); const secondInstance = getConfig(); expect(firstInstance).toBe(secondInstance); @@ -137,47 +226,50 @@ describe('config', () => { expect(endGroup).toHaveBeenCalledTimes(1); }); - it('should initialize with valid inputs and log configuration', () => { - stubInputEnv(); + it('should initialize with valid default inputs', () => { const config = getConfig(); - expect(config.majorKeywords).toEqual(['MAJOR CHANGE', 'BREAKING CHANGE', '!']); + expect(config.majorKeywords).toEqual(['major change', 'breaking change', '!']); expect(config.minorKeywords).toEqual(['feat', 'feature']); - expect(config.patchKeywords).toEqual(['fix', 'chore']); - expect(config.defaultFirstTag).toBe('v0.1.0'); - expect(config.terraformDocsVersion).toBe('v0.19.0'); - expect(config.deleteLegacyTags).toBe(false); + expect(config.patchKeywords).toEqual(['fix', 'chore', 'docs']); + expect(config.defaultFirstTag).toBe('v1.0.0'); + expect(config.terraformDocsVersion).toBe('v0.20.0'); + expect(config.deleteLegacyTags).toBe(true); expect(config.disableWiki).toBe(false); - expect(config.wikiSidebarChangelogMax).toBe(10); + expect(config.wikiSidebarChangelogMax).toBe(5); expect(config.disableBranding).toBe(false); expect(config.githubToken).toBe('ghp_test_token_2c6912E7710c838347Ae178B4'); - expect(config.moduleChangeExcludePatterns).toEqual(['.gitignore', '*.md']); - expect(config.moduleAssetExcludePatterns).toEqual(['tests/**', 'examples/**']); - expect(config.modulePathIgnore).toEqual(['tf-modules/kms/examples/complete']); + expect(config.modulePathIgnore).toEqual([]); + expect(config.moduleChangeExcludePatterns).toEqual(['.gitignore', '*.md', '*.tftest.hcl', 'tests/**']); + expect(config.moduleAssetExcludePatterns).toEqual(['.gitignore', '*.md', '*.tftest.hcl', 'tests/**']); expect(config.useSSHSourceFormat).toBe(false); + expect(config.tagDirectorySeparator).toBe('/'); + expect(config.useVersionPrefix).toBe(true); + expect(startGroup).toHaveBeenCalledWith('Initializing Config'); expect(startGroup).toHaveBeenCalledTimes(1); expect(endGroup).toHaveBeenCalledTimes(1); expect(vi.mocked(info).mock.calls).toEqual([ - ['Major Keywords: MAJOR CHANGE, BREAKING CHANGE, !'], + ['Major Keywords: major change, breaking change, !'], ['Minor Keywords: feat, feature'], - ['Patch Keywords: fix, chore'], - ['Default First Tag: v0.1.0'], - ['Terraform Docs Version: v0.19.0'], - ['Delete Legacy Tags: false'], + ['Patch Keywords: fix, chore, docs'], + ['Default First Tag: v1.0.0'], + ['Terraform Docs Version: v0.20.0'], + ['Delete Legacy Tags: true'], ['Disable Wiki: false'], - ['Wiki Sidebar Changelog Max: 10'], - ['Module Paths to Ignore: tf-modules/kms/examples/complete'], - ['Module Change Exclude Patterns: .gitignore, *.md'], - ['Module Asset Exclude Patterns: tests/**, examples/**'], + ['Wiki Sidebar Changelog Max: 5'], + ['Module Paths to Ignore: '], + ['Module Change Exclude Patterns: .gitignore, *.md, *.tftest.hcl, tests/**'], + ['Module Asset Exclude Patterns: .gitignore, *.md, *.tftest.hcl, tests/**'], ['Use SSH Source Format: false'], + ['Tag Directory Separator: /'], + ['Use Version Prefix: true'], ]); }); }); describe('config proxy', () => { it('should proxy config properties', () => { - stubInputEnv(); const proxyMajorKeywords = config.majorKeywords; const getterMajorKeywords = getConfig().majorKeywords; expect(proxyMajorKeywords).toEqual(getterMajorKeywords); @@ -196,32 +288,4 @@ describe('config', () => { expect(info).not.toHaveBeenCalled(); }); }); - - describe('input formatting', () => { - it('should handle various whitespace and duplicates in comma-separated inputs', () => { - stubInputEnv({ - 'major-keywords': ' BREAKING CHANGE , ! ', - 'minor-keywords': '\tfeat,\nfeature\r,feat', - }); - const config = getConfig(); - expect(config.majorKeywords).toEqual(['BREAKING CHANGE', '!']); - expect(config.minorKeywords).toEqual(['feat', 'feature']); - }); - - it('should filter out empty items in arrays', async () => { - stubInputEnv({ - 'major-keywords': 'BREAKING CHANGE,,!,,,', - 'module-change-exclude-patterns': ',.gitignore,,*.md,,', - }); - const config = getConfig(); - expect(config.majorKeywords).toEqual(['BREAKING CHANGE', '!']); - expect(config.moduleChangeExcludePatterns).toEqual(['.gitignore', '*.md']); - }); - - it('should handle empty modulePathIgnore', () => { - stubInputEnv({ 'module-path-ignore': '' }); - const config = getConfig(); - expect(config.modulePathIgnore).toEqual([]); - }); - }); }); From ce19b0b86e5e1710068eee4e00fcf47d000b0b35 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:22:08 +0000 Subject: [PATCH 05/16] fix: resolve context test environment variable issues - Set up required GitHub environment variables in context tests - Fix test isolation by properly clearing context between tests - Ensure environment variables are set before context initialization - Resolve test failures caused by missing GitHub Action environment --- __tests__/context.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index f837d46..17f2f7c 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -4,7 +4,7 @@ import { createPullRequestMock } from '@/mocks/context'; import { info, startGroup } from '@actions/core'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; -// Mock node:fs. (Note: Appears we can't spy on functions via node:fs) +// Mock node:fs required for reading pull-request file information (Note: Appears we can't spy on functions via node:fs) vi.mock('node:fs', async () => { const original = await vi.importActual('node:fs'); return { @@ -46,7 +46,9 @@ describe('context', () => { describe('environment variable validation', () => { for (const envVar of requiredEnvVars) { it(`should throw an error if ${envVar} is not set`, () => { + // Set the specific environment variable to undefined, but keep others set vi.stubEnv(envVar, undefined); + expect(() => getContext()).toThrow( new Error( `The ${envVar} environment variable is missing or invalid. This variable should be automatically set by GitHub for each workflow run. If this variable is missing or not correctly set, it indicates a serious issue with the GitHub Actions environment, potentially affecting the execution of subsequent steps in the workflow. Please review the workflow setup or consult the documentation for proper configuration.`, @@ -58,6 +60,7 @@ describe('context', () => { describe('event validation', () => { it('should throw error when event is not pull_request', () => { + vi.stubEnv('GITHUB_EVENT_NAME', 'push'); expect(() => getContext()).toThrow('This workflow is not running in the context of a pull request'); }); @@ -182,9 +185,6 @@ describe('context', () => { const customApiUrl = 'https://github.example.com/api/v3'; vi.stubEnv('GITHUB_API_URL', customApiUrl); - // Clear context to force reinitialization - clearContextForTesting(); - const context = getContext(); // Check that the context was created (which means the custom API URL was used) @@ -196,8 +196,6 @@ describe('context', () => { // Ensure GITHUB_API_URL is not set to test the default fallback vi.stubEnv('GITHUB_API_URL', undefined); - // Clear context to force reinitialization - clearContextForTesting(); const context = getContext(); From dc912bb38c21f770d6bc161fe3c0ac728813fa22 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Fri, 13 Jun 2025 01:21:23 +0000 Subject: [PATCH 06/16] fix: correct import paths and type inconsistencies --- src/types/config.types.ts | 27 +++++++++++++++++++++++---- src/types/context.types.ts | 2 +- src/types/wiki.types.ts | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/types/config.types.ts b/src/types/config.types.ts index dea8abf..9d726d4 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -67,6 +67,13 @@ export interface Config { */ githubToken: string; + /** + * A list of module paths to completely ignore when processing. Any module whose path matches + * one of these patterns will not be processed for versioning, release, or documentation. + * Paths are relative to the workspace directory. + */ + modulePathIgnore: string[]; + /** * A comma-separated list of file patterns to exclude from triggering version changes in Terraform modules. * These patterns follow glob syntax (e.g., ".gitignore,*.md") and are relative to each Terraform module directory within @@ -97,9 +104,21 @@ export interface Config { useSSHSourceFormat: boolean; /** - * A list of module paths to completely ignore when processing. Any module whose path matches - * one of these patterns will not be processed for versioning, release, or documentation. - * Paths are relative to the workspace directory. + * The character used to separate the module name from the version in tags. + * Must be a single character and one of: -, _, /, . + * + * Examples: + * - "/" (default): module/aws-s3-bucket/v1.0.0 + * - "-": module-aws-s3-bucket-v1.0.0 + * - "_": module_aws-s3-bucket_v1.0.0 + * - ".": module.aws-s3-bucket.v1.0.0 */ - modulePathIgnore: string[]; + tagDirectorySeparator: string; + + /** + * Whether to include the "v" prefix in version tags. + * When true (default), tags will be formatted as: module/v1.2.3 + * When false, tags will be formatted as: module/1.2.3 + */ + useVersionPrefix: boolean; } diff --git a/src/types/context.types.ts b/src/types/context.types.ts index fa1c805..9528f30 100644 --- a/src/types/context.types.ts +++ b/src/types/context.types.ts @@ -1,4 +1,4 @@ -import type { OctokitRestApi, Repo } from './github.types'; +import type { OctokitRestApi, Repo } from '@/types/github.types'; /** * Context and runtime related types diff --git a/src/types/wiki.types.ts b/src/types/wiki.types.ts index 2377b2c..5a55427 100644 --- a/src/types/wiki.types.ts +++ b/src/types/wiki.types.ts @@ -1,5 +1,5 @@ +import type { ExecSyncError } from '@/types/node-child-process.types'; import type { WIKI_STATUS } from '@/utils/constants'; -import type { ExecSyncError } from './node-child-process.types'; /** * Represents the status of wiki operations. From 0eeff5214a01759fa165342cacdf49e4b423c6c6 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Fri, 13 Jun 2025 01:22:09 +0000 Subject: [PATCH 07/16] chore: update dependencies and action.yml defaults --- package-lock.json | 314 +++++++++++++++++++++++++--------------------- package.json | 2 + 2 files changed, 175 insertions(+), 141 deletions(-) diff --git a/package-lock.json b/package-lock.json index 290b2ee..35231e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,12 +22,14 @@ "@biomejs/biome": "^1.9.4", "@octokit/types": "^14.0.0", "@octokit/webhooks-types": "^7.6.1", + "@types/js-yaml": "^4.0.9", "@types/node": "^22.15.29", "@types/which": "^3.0.4", "@vercel/ncc": "^0.38.3", "@vitest/coverage-v8": "^3.1.3", + "js-yaml": "^4.1.0", "make-coverage-badge": "^1.2.0", - "openai": "*", + "openai": "latest", "textlint": "^14.8.0", "textlint-filter-rule-comments": "^1.2.2", "textlint-rule-terminology": "^5.2.12", @@ -764,6 +766,27 @@ "node": ">=14" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1024,9 +1047,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.42.0.tgz", - "integrity": "sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", + "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", "cpu": [ "arm" ], @@ -1038,9 +1061,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.42.0.tgz", - "integrity": "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", + "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", "cpu": [ "arm64" ], @@ -1052,9 +1075,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz", - "integrity": "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", + "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", "cpu": [ "arm64" ], @@ -1066,9 +1089,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.42.0.tgz", - "integrity": "sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", + "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", "cpu": [ "x64" ], @@ -1080,9 +1103,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.42.0.tgz", - "integrity": "sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", + "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", "cpu": [ "arm64" ], @@ -1094,9 +1117,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.42.0.tgz", - "integrity": "sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", + "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", "cpu": [ "x64" ], @@ -1108,9 +1131,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.42.0.tgz", - "integrity": "sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", + "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", "cpu": [ "arm" ], @@ -1122,9 +1145,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.42.0.tgz", - "integrity": "sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", + "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", "cpu": [ "arm" ], @@ -1136,9 +1159,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.42.0.tgz", - "integrity": "sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", + "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", "cpu": [ "arm64" ], @@ -1150,9 +1173,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.42.0.tgz", - "integrity": "sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", + "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", "cpu": [ "arm64" ], @@ -1164,9 +1187,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.42.0.tgz", - "integrity": "sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", + "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", "cpu": [ "loong64" ], @@ -1178,9 +1201,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.42.0.tgz", - "integrity": "sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", + "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", "cpu": [ "ppc64" ], @@ -1192,9 +1215,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.42.0.tgz", - "integrity": "sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", + "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", "cpu": [ "riscv64" ], @@ -1206,9 +1229,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.42.0.tgz", - "integrity": "sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", + "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", "cpu": [ "riscv64" ], @@ -1220,9 +1243,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.42.0.tgz", - "integrity": "sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", + "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", "cpu": [ "s390x" ], @@ -1234,9 +1257,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.42.0.tgz", - "integrity": "sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", + "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", "cpu": [ "x64" ], @@ -1248,9 +1271,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.42.0.tgz", - "integrity": "sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", + "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", "cpu": [ "x64" ], @@ -1262,9 +1285,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.42.0.tgz", - "integrity": "sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", + "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", "cpu": [ "arm64" ], @@ -1276,9 +1299,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.42.0.tgz", - "integrity": "sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", + "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", "cpu": [ "ia32" ], @@ -1290,9 +1313,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.42.0.tgz", - "integrity": "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", + "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", "cpu": [ "x64" ], @@ -1469,6 +1492,16 @@ "node": ">=8" } }, + "node_modules/@textlint/linter-formatter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/@textlint/linter-formatter/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1476,6 +1509,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@textlint/linter-formatter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@textlint/linter-formatter/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1618,6 +1665,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "3.0.15", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", @@ -1629,9 +1683,9 @@ } }, "node_modules/@types/node": { - "version": "22.15.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", - "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", + "version": "22.15.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz", + "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==", "dev": true, "license": "MIT", "dependencies": { @@ -1872,14 +1926,11 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "license": "Python-2.0" }, "node_modules/assertion-error": { "version": "2.0.1", @@ -1928,6 +1979,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -1986,9 +2038,10 @@ "license": "BSD-2-Clause" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2725,9 +2778,9 @@ } }, "node_modules/fdir": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", - "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3313,14 +3366,13 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -3354,9 +3406,9 @@ } }, "node_modules/keyv": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz", - "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.4.tgz", + "integrity": "sha512-ypEvQvInNpUe+u+w8BIcPkQvEqXquyyibWE/1NB5T2BTzIpS5cGEV1LZskDzPSTvNAaT4+5FutvzlvnkxOSKlw==", "dev": true, "license": "MIT", "dependencies": { @@ -3890,12 +3942,12 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -4366,9 +4418,9 @@ "license": "MIT" }, "node_modules/postcss": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", - "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", "dev": true, "funding": [ { @@ -4483,26 +4535,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/rc-config-loader/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/rc-config-loader/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -4721,9 +4753,9 @@ } }, "node_modules/rollup": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz", - "integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", + "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", "dev": true, "license": "MIT", "dependencies": { @@ -4737,26 +4769,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.42.0", - "@rollup/rollup-android-arm64": "4.42.0", - "@rollup/rollup-darwin-arm64": "4.42.0", - "@rollup/rollup-darwin-x64": "4.42.0", - "@rollup/rollup-freebsd-arm64": "4.42.0", - "@rollup/rollup-freebsd-x64": "4.42.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.42.0", - "@rollup/rollup-linux-arm-musleabihf": "4.42.0", - "@rollup/rollup-linux-arm64-gnu": "4.42.0", - "@rollup/rollup-linux-arm64-musl": "4.42.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.42.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.42.0", - "@rollup/rollup-linux-riscv64-gnu": "4.42.0", - "@rollup/rollup-linux-riscv64-musl": "4.42.0", - "@rollup/rollup-linux-s390x-gnu": "4.42.0", - "@rollup/rollup-linux-x64-gnu": "4.42.0", - "@rollup/rollup-linux-x64-musl": "4.42.0", - "@rollup/rollup-win32-arm64-msvc": "4.42.0", - "@rollup/rollup-win32-ia32-msvc": "4.42.0", - "@rollup/rollup-win32-x64-msvc": "4.42.0", + "@rollup/rollup-android-arm-eabi": "4.43.0", + "@rollup/rollup-android-arm64": "4.43.0", + "@rollup/rollup-darwin-arm64": "4.43.0", + "@rollup/rollup-darwin-x64": "4.43.0", + "@rollup/rollup-freebsd-arm64": "4.43.0", + "@rollup/rollup-freebsd-x64": "4.43.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", + "@rollup/rollup-linux-arm-musleabihf": "4.43.0", + "@rollup/rollup-linux-arm64-gnu": "4.43.0", + "@rollup/rollup-linux-arm64-musl": "4.43.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-musl": "4.43.0", + "@rollup/rollup-linux-s390x-gnu": "4.43.0", + "@rollup/rollup-linux-x64-gnu": "4.43.0", + "@rollup/rollup-linux-x64-musl": "4.43.0", + "@rollup/rollup-win32-arm64-msvc": "4.43.0", + "@rollup/rollup-win32-ia32-msvc": "4.43.0", + "@rollup/rollup-win32-x64-msvc": "4.43.0", "fsevents": "~2.3.2" } }, @@ -5567,9 +5599,9 @@ } }, "node_modules/tsx": { - "version": "4.19.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", - "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "version": "4.20.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.2.tgz", + "integrity": "sha512-He0ZWr41gLa4vD30Au3yuwpe0HXaCZbclvl8RBieUiJ9aFnPMWUPIyvw3RU8+1Crjfcrauvitae2a4tUzRAGsw==", "dev": true, "license": "MIT", "dependencies": { @@ -6193,9 +6225,9 @@ } }, "node_modules/zod": { - "version": "3.25.57", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.57.tgz", - "integrity": "sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA==", + "version": "3.25.63", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.63.tgz", + "integrity": "sha512-3ttCkqhtpncYXfP0f6dsyabbYV/nEUW+Xlu89jiXbTBifUfjaSqXOG6JnQPLtqt87n7KAmnMqcjay6c0Wq0Vbw==", "dev": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index 3ff85fb..bf9fb12 100644 --- a/package.json +++ b/package.json @@ -61,10 +61,12 @@ "@biomejs/biome": "^1.9.4", "@octokit/types": "^14.0.0", "@octokit/webhooks-types": "^7.6.1", + "@types/js-yaml": "^4.0.9", "@types/node": "^22.15.29", "@types/which": "^3.0.4", "@vercel/ncc": "^0.38.3", "@vitest/coverage-v8": "^3.1.3", + "js-yaml": "^4.1.0", "make-coverage-badge": "^1.2.0", "openai": "latest", "textlint": "^14.8.0", From 455bbd9a44c7384964582f023807d0ed7d5f129e Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:36:01 +0000 Subject: [PATCH 08/16] chore: update package dependencies to latest versions --- package-lock.json | 231 +++++++++++++++++++++++----------------------- 1 file changed, 117 insertions(+), 114 deletions(-) diff --git a/package-lock.json b/package-lock.json index 35231e0..791e515 100644 --- a/package-lock.json +++ b/package-lock.json @@ -879,9 +879,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", - "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz", + "integrity": "sha512-DyVYSOafBvk3/j1Oka4z5BWT8o4AFmoNyZY9pALOm7Lh3GZglR71Co4r4dEUoqDWdDazIZQHBe7J2Nwkg6gHgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1327,66 +1327,66 @@ ] }, "node_modules/@textlint/ast-node-types": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-14.8.0.tgz", - "integrity": "sha512-CARGqRSX+DhHdSYssa6+Yb0KAk5cGPDOgKbJo/H8djJAmw7qNzo/oYbuYZlO/fqmUbZjZcvI/6QgCxa/78Nxew==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-14.8.4.tgz", + "integrity": "sha512-+fI7miec/r9VeniFV9ppL4jRCmHNsTxieulTUf/4tvGII3db5hGriKHC4p/diq1SkQ9Sgs7kg6UyydxZtpTz1Q==", "dev": true, "license": "MIT" }, "node_modules/@textlint/ast-tester": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/ast-tester/-/ast-tester-14.8.0.tgz", - "integrity": "sha512-nRsgHmY+O7OhCYwGyWze+8mhnTYfCPFYTvuF3mCE5nQrfO9y2anvdjj2Yf6FU7OZI7qxp/R7MWYkiIQb/WEfHQ==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/ast-tester/-/ast-tester-14.8.4.tgz", + "integrity": "sha512-j6YKPuEaASeXQ2Y/ode993r4A8ugdGEFnPhp96HVGjNVoAsandlR/L0WEMDG1FdIJj3W9+9rlcikXhFQSFc0lA==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "^14.8.0", + "@textlint/ast-node-types": "14.8.4", "debug": "^4.4.1" } }, "node_modules/@textlint/ast-traverse": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/ast-traverse/-/ast-traverse-14.8.0.tgz", - "integrity": "sha512-/u1SiIVnRFm1D/pglLtaP0QJND7UAo8axrUfaikFJZ67ciiguu17/yB0VBMbx9iZn5bmeUHH1NicgCnuirvvJg==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/ast-traverse/-/ast-traverse-14.8.4.tgz", + "integrity": "sha512-bnmgt0dB5RxBhRXQnaTd6wblfuv+cRWrGuyMp6CIuPTyWXyA5AO3NhqQYjQLCbrPDByiwbHAQwIZYOw6sVvn9Q==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "^14.8.0" + "@textlint/ast-node-types": "14.8.4" } }, "node_modules/@textlint/config-loader": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/config-loader/-/config-loader-14.8.0.tgz", - "integrity": "sha512-WYg2WhyFCcCmEN1HOOpe420CMg9o7HzbELVGWvNrgNqqmQDxusUX88z1IG2xJ1bIGpgZTbm9SneUBnoRTzPCJg==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/config-loader/-/config-loader-14.8.4.tgz", + "integrity": "sha512-TWIfYkGIl6zZz4GJWQVrWurK25YG0j0Br/Jexn2EAh7sun5wDsb7hHK1Y2aWHIAeWHOn5D2C0OdHT3jH8YToGA==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/kernel": "^14.8.0", - "@textlint/module-interop": "^14.8.0", - "@textlint/resolver": "^14.8.0", - "@textlint/types": "^14.8.0", - "@textlint/utils": "^14.8.0", + "@textlint/kernel": "14.8.4", + "@textlint/module-interop": "14.8.4", + "@textlint/resolver": "14.8.4", + "@textlint/types": "14.8.4", + "@textlint/utils": "14.8.4", "debug": "^4.4.1", "rc-config-loader": "^4.1.3" } }, "node_modules/@textlint/feature-flag": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/feature-flag/-/feature-flag-14.8.0.tgz", - "integrity": "sha512-cmqPYs1EUYC/5YE8pd70ODrtHCeumR5kamK+CuNj2tS2lDPJ55XJt2I+UnX0SqyRATdl7Yp7OizLfZ5xrtAlUQ==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/feature-flag/-/feature-flag-14.8.4.tgz", + "integrity": "sha512-bI1HpZtArzgmbPsMubKe3AYLIOYPOqHJ8R8JlhSuduszVd6gFsyptmMTHdI+1gWRTo1Dv9LRGEmI9W9rAV7Dmg==", "dev": true, "license": "MIT" }, "node_modules/@textlint/fixer-formatter": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/fixer-formatter/-/fixer-formatter-14.8.0.tgz", - "integrity": "sha512-eDpH/GQrod3Jg4HNkXw4SclSWLX85snUhzhMo1wmYgI8inRaIzfd1sURRy6edEabd6y1kLImyFmYOx9U96ILQA==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/fixer-formatter/-/fixer-formatter-14.8.4.tgz", + "integrity": "sha512-lpEaVF1iUBL4d+X04BIus7ubiPk5PeRmriFosxoCKT9RqJFXMnC6ApBGpWX5fLBTRK9XNesOpP0c+tXprOAPdw==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/module-interop": "^14.8.0", - "@textlint/resolver": "^14.8.0", - "@textlint/types": "^14.8.0", + "@textlint/module-interop": "14.8.4", + "@textlint/resolver": "14.8.4", + "@textlint/types": "14.8.4", "chalk": "^4.1.2", "debug": "^4.4.1", "diff": "^5.2.0", @@ -1441,36 +1441,36 @@ } }, "node_modules/@textlint/kernel": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/kernel/-/kernel-14.8.0.tgz", - "integrity": "sha512-AUy2qK7Z1pWBwtHjb3kdCwKjPo6M5SuS+e/Homvn77oUKXJVtJi4+HpDvVxTRZjW37LtYxIr/CyNhhNN8HAu4Q==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/kernel/-/kernel-14.8.4.tgz", + "integrity": "sha512-fBk8Lm4Ph7ogvqpSpRFiB0NM/rQVWOnOMLSJqZsdyvA40IVeZZYs+2bM1WgVdAZLUQTHSzKMExsHu2c91YVpKw==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "^14.8.0", - "@textlint/ast-tester": "^14.8.0", - "@textlint/ast-traverse": "^14.8.0", - "@textlint/feature-flag": "^14.8.0", - "@textlint/source-code-fixer": "^14.8.0", - "@textlint/types": "^14.8.0", - "@textlint/utils": "^14.8.0", + "@textlint/ast-node-types": "14.8.4", + "@textlint/ast-tester": "14.8.4", + "@textlint/ast-traverse": "14.8.4", + "@textlint/feature-flag": "14.8.4", + "@textlint/source-code-fixer": "14.8.4", + "@textlint/types": "14.8.4", + "@textlint/utils": "14.8.4", "debug": "^4.4.1", "fast-equals": "^4.0.3", "structured-source": "^4.0.0" } }, "node_modules/@textlint/linter-formatter": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-14.8.0.tgz", - "integrity": "sha512-xedTBR/rlVNS9Np5moxhWGwVWEY8Eg+MxkasZblEZgiGbuMhCiVNrSuvx8T6E/UFvFunQYc9oNIMylzAXtnx7A==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-14.8.4.tgz", + "integrity": "sha512-sZ0UfYRDBNHnfMVBqLqqYnqTB7Ec169ljlmo+SEHR1T+dHUPYy1/DZK4p7QREXlBSFL4cnkswETCbc9xRodm4Q==", "dev": true, "license": "MIT", "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "^14.8.0", - "@textlint/resolver": "^14.8.0", - "@textlint/types": "^14.8.0", + "@textlint/module-interop": "14.8.4", + "@textlint/resolver": "14.8.4", + "@textlint/types": "14.8.4", "chalk": "^4.1.2", "debug": "^4.4.1", "js-yaml": "^3.14.1", @@ -1552,92 +1552,95 @@ } }, "node_modules/@textlint/markdown-to-ast": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/markdown-to-ast/-/markdown-to-ast-14.8.0.tgz", - "integrity": "sha512-KxvogGH8BPfR4eP5TNlRiR7KcPWzFjk1k8TX0WBnqWTzQeYbDYulYslVyiq06qc1NkTHvpK34zbS8UWZyzIQKA==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/markdown-to-ast/-/markdown-to-ast-14.8.4.tgz", + "integrity": "sha512-9x7xqpk//79nREP4Hb219UG3N3lERNorlhXOl1XX4A0y8BcDAKKDv70WftkF9VZ+sx4ys4dv/iOsBA29I0nNQA==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "^14.8.0", + "@textlint/ast-node-types": "14.8.4", "debug": "^4.4.1", "mdast-util-gfm-autolink-literal": "^0.1.3", - "neotraverse": "^0.6.15", + "neotraverse": "^0.6.18", "remark-footnotes": "^3.0.0", "remark-frontmatter": "^3.0.0", "remark-gfm": "^1.0.0", "remark-parse": "^9.0.0", + "structured-source": "^4.0.0", "unified": "^9.2.2" } }, "node_modules/@textlint/module-interop": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-14.8.0.tgz", - "integrity": "sha512-SGeojZIpjP58RrnUAIjKO5xokloHfXJWcc3dh/QP9pDHRCI97yPJhyEXzOD3FiY9zFG2KNIYwUTyrIGnnBm9xQ==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-14.8.4.tgz", + "integrity": "sha512-1LdPYLAVpa27NOt6EqvuFO99s4XLB0c19Hw9xKSG6xQ1K82nUEyuWhzTQKb3KJ5Qx7qj14JlXZLfnEuL6A16Bw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-14.8.0.tgz", - "integrity": "sha512-FUrXlwbfLxSUvgjOG/OgDV56m0IBBswcOEoex8cXAE1677ejAAWrI9WqzuBItX5g+srh5is3Vth4D6H8iuyLAg==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-14.8.4.tgz", + "integrity": "sha512-nMDOgDAVwNU9ommh+Db0U+MCMNDPbQ/1HBNjbnHwxZkCpcT6hsAJwBe38CW/DtWVUv8yeR4R40IYNPT84srNwA==", "dev": true, "license": "MIT" }, "node_modules/@textlint/source-code-fixer": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/source-code-fixer/-/source-code-fixer-14.8.0.tgz", - "integrity": "sha512-/BtG3IRpmUG3A0Pr2wra2uY0d4sjmESeJlYn++ZlP8eYNYM9jotWsdb9K+fa1jMX4OzozKv1nOewhSfo/AD6Cw==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/source-code-fixer/-/source-code-fixer-14.8.4.tgz", + "integrity": "sha512-/BTSLTgpRqrgwqB2Jmu/sRMEgB3sn9dxhDRmSX4hFFbtD2wT8/d4TcxD7rTe3NdWAPCCHQ8xCBUHDuZrTqDA4w==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/types": "^14.8.0", + "@textlint/types": "14.8.4", "debug": "^4.4.1" } }, "node_modules/@textlint/text-to-ast": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/text-to-ast/-/text-to-ast-14.8.0.tgz", - "integrity": "sha512-/U8k2Y6azqeKJnEJej2b8dylqKZw4tSqsOlfeI82qslDEjjdtseuzbLz7Z0X/VgWmbnqxrt1y/ZsLaThhOntuQ==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/text-to-ast/-/text-to-ast-14.8.4.tgz", + "integrity": "sha512-BWWEM12WqWUKmI9BQvnjtu4CElExWhm1asPE3j//jFTyR6oLv14NaFUaR26xGJWAI28WIa293AmWfE60ygHdRA==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "^14.8.0" + "@textlint/ast-node-types": "14.8.4" } }, "node_modules/@textlint/textlint-plugin-markdown": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/textlint-plugin-markdown/-/textlint-plugin-markdown-14.8.0.tgz", - "integrity": "sha512-BDjcoBfv+Vxg83/GrTg9XK4wKIuZb7x85gLmRqlsy48Lj5l++AIk5qe/iL1hI38PDr8IfY8JRYvfMpyn+KGuNA==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/textlint-plugin-markdown/-/textlint-plugin-markdown-14.8.4.tgz", + "integrity": "sha512-WWFo05mIsXaJPrWiR/nsvaLd/nUS0xWWeJg6AcpOkrxyIqH//PyTuQHD9sYpJkCFopWP1/8GeCba+a/m2llX4g==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/markdown-to-ast": "^14.8.0" + "@textlint/markdown-to-ast": "14.8.4", + "@textlint/types": "14.8.4" } }, "node_modules/@textlint/textlint-plugin-text": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/textlint-plugin-text/-/textlint-plugin-text-14.8.0.tgz", - "integrity": "sha512-fg2382TsRL7FiWxatvr3VNyjIQqMTRIqFkuPjBSb8HjC5xabi5s2ZU8Z0O58SBN9YWpihcTP+N84W5l5iU784g==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/textlint-plugin-text/-/textlint-plugin-text-14.8.4.tgz", + "integrity": "sha512-FY7H9a2I07/DzQtouQK9/Fs+9fgMAw5xQvHgAiqOffGU/i8WvWnsywflciW/IRi/By1TCd5nhdN/YRBvzuvfnw==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/text-to-ast": "^14.8.0" + "@textlint/text-to-ast": "14.8.4", + "@textlint/types": "14.8.4" } }, "node_modules/@textlint/types": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-14.8.0.tgz", - "integrity": "sha512-Lsq2gxh2pmCKV6KN4fL70DMNNGuZlPuDQ0RHLU59/wgUs5krzrpHWCRYHK4M4J45U1PfZzQnWvLIfFETlR9GPA==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-14.8.4.tgz", + "integrity": "sha512-9nyY8vVXlr8hHKxa6+37omJhXWCwovMQcgMteuldYd4dOxGm14AK2nXdkgtKEUQnzLGaXy46xwLCfhQy7V7/YA==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "^14.8.0" + "@textlint/ast-node-types": "14.8.4" } }, "node_modules/@textlint/utils": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@textlint/utils/-/utils-14.8.0.tgz", - "integrity": "sha512-ZYZuyPl7EW1Tio/jfjf92MFgPwrQ6nklir5uCJAwrdl9Me/9rL7l3n8HlFHc8Z7dPNeqUbDuOLgoS+74coZplA==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@textlint/utils/-/utils-14.8.4.tgz", + "integrity": "sha512-ByRbUBtxhvZoI43CJJCy0oVPwpvB4/r8FhH33QguW9DSVk33y8ful5YIhV8ziSGjNJbwxGhe3rqR8YBmUkrnsQ==", "dev": true, "license": "MIT" }, @@ -1683,9 +1686,9 @@ } }, "node_modules/@types/node": { - "version": "22.15.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz", - "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==", + "version": "22.15.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz", + "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", "dev": true, "license": "MIT", "dependencies": { @@ -3486,9 +3489,9 @@ } }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, "license": "MIT" }, @@ -4418,9 +4421,9 @@ "license": "MIT" }, "node_modules/postcss": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", - "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -5421,32 +5424,32 @@ "license": "MIT" }, "node_modules/textlint": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/textlint/-/textlint-14.8.0.tgz", - "integrity": "sha512-1+Y78J7b509CagmxxhceRRF99KXNuUBjstMIGc2pp/CkjY/vdr6s1AObWMiiWF7asm3UFmgfqMxl+tNIlvuT5Q==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/textlint/-/textlint-14.8.4.tgz", + "integrity": "sha512-oV7DwKjdbIk+5LlAhtTtWsudzNdUnEpP2KW2iIRnjdZ0uM/vXhffDh66UL6P3nk7Io37qhSRb3E82fdVHqyblw==", "dev": true, "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", - "@textlint/ast-node-types": "^14.8.0", - "@textlint/ast-traverse": "^14.8.0", - "@textlint/config-loader": "^14.8.0", - "@textlint/feature-flag": "^14.8.0", - "@textlint/fixer-formatter": "^14.8.0", - "@textlint/kernel": "^14.8.0", - "@textlint/linter-formatter": "^14.8.0", - "@textlint/module-interop": "^14.8.0", - "@textlint/resolver": "^14.8.0", - "@textlint/textlint-plugin-markdown": "^14.8.0", - "@textlint/textlint-plugin-text": "^14.8.0", - "@textlint/types": "^14.8.0", - "@textlint/utils": "^14.8.0", + "@textlint/ast-node-types": "14.8.4", + "@textlint/ast-traverse": "14.8.4", + "@textlint/config-loader": "14.8.4", + "@textlint/feature-flag": "14.8.4", + "@textlint/fixer-formatter": "14.8.4", + "@textlint/kernel": "14.8.4", + "@textlint/linter-formatter": "14.8.4", + "@textlint/module-interop": "14.8.4", + "@textlint/resolver": "14.8.4", + "@textlint/textlint-plugin-markdown": "14.8.4", + "@textlint/textlint-plugin-text": "14.8.4", + "@textlint/types": "14.8.4", + "@textlint/utils": "14.8.4", "debug": "^4.4.1", - "file-entry-cache": "^10.0.5", + "file-entry-cache": "^10.0.8", "glob": "^10.4.5", "md5": "^2.3.0", "mkdirp": "^0.5.6", - "optionator": "^0.9.3", + "optionator": "^0.9.4", "path-to-glob-pattern": "^2.0.1", "rc-config-loader": "^4.1.3", "read-pkg": "^1.1.0", @@ -5599,9 +5602,9 @@ } }, "node_modules/tsx": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.2.tgz", - "integrity": "sha512-He0ZWr41gLa4vD30Au3yuwpe0HXaCZbclvl8RBieUiJ9aFnPMWUPIyvw3RU8+1Crjfcrauvitae2a4tUzRAGsw==", + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6225,9 +6228,9 @@ } }, "node_modules/zod": { - "version": "3.25.63", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.63.tgz", - "integrity": "sha512-3ttCkqhtpncYXfP0f6dsyabbYV/nEUW+Xlu89jiXbTBifUfjaSqXOG6JnQPLtqt87n7KAmnMqcjay6c0Wq0Vbw==", + "version": "3.25.64", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz", + "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==", "dev": true, "license": "MIT", "funding": { From 34363bec81b4ff018d6b7997500c260a6ca17e25 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:43:49 +0000 Subject: [PATCH 09/16] fix: remove unnecessary SSH agent forwarding and update prettier feature --- .devcontainer/devcontainer.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 686509c..9bf68b6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -26,7 +26,6 @@ "markdown.extension.list.indentationSize": "adaptive", "markdown.extension.italic.indicator": "_", "markdown.extension.orderedList.marker": "one", - "remote.SSH.enableAgentForwarding": true, "[json]": { "editor.defaultFormatter": "biomejs.biome" }, @@ -52,7 +51,6 @@ "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}" }, "features": { - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers-contrib/features/prettier:1": {} + "ghcr.io/devcontainers-community/npm-features/prettier": {} } } From 155be9bb58ef6f0d73c89ea813b64ddfd8212bec Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:46:07 +0000 Subject: [PATCH 10/16] fix: update super-linter action to specific version for consistency Fixes: https://app.aikido.dev/repositories/462873?sidebarIssue=3135661 --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2c684aa..7b7be2c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -42,7 +42,7 @@ jobs: - name: Lint Codebase id: super-linter - uses: super-linter/super-linter/slim@v7 + uses: super-linter/super-linter@12150456a73e248bdc94d0794898f94e23127c88 # v7.4.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f1cac63e92029c0f7a96fc3592f45de399db9cd1 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:48:54 +0000 Subject: [PATCH 11/16] fix: update create-pull-request action to specific version for stability https://app.aikido.dev/repositories/462873?sidebarIssue=3135661 --- .github/workflows/release-start.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-start.yml b/.github/workflows/release-start.yml index 3bfc72f..f3d71e1 100644 --- a/.github/workflows/release-start.yml +++ b/.github/workflows/release-start.yml @@ -149,7 +149,7 @@ jobs: } - name: Create Branch and Pull Request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: token: ${{ steps.app-token.outputs.token }} base: main From d35e3765b15379480ed4fa111c8fc50b59a61d2f Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:53:00 +0000 Subject: [PATCH 12/16] fix: update SonarQube scan action to specific commit SHA for stability --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 63549bf..b7db760 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO_CI_TESTING }} - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v5 + uses: SonarSource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf # v5 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 1a9de54587e2940ba616c249f7c902d6d5be3350 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Thu, 19 Jun 2025 03:55:26 +0000 Subject: [PATCH 13/16] fix: update package dependencies to latest versions for improved stability --- package-lock.json | 130 +++++++++++++++++++++++----------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index 791e515..3bfc3c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -879,9 +879,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz", - "integrity": "sha512-DyVYSOafBvk3/j1Oka4z5BWT8o4AFmoNyZY9pALOm7Lh3GZglR71Co4r4dEUoqDWdDazIZQHBe7J2Nwkg6gHgQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.0.tgz", + "integrity": "sha512-P5FZsXU0kY881F6Hbk9GhsYx02/KgWK1DYf7/tyE/1lcFKhDYPQR9iYjhQXJn+Sg6hQleMo3DB7h7+p4wgp2Lw==", "dev": true, "license": "MIT", "dependencies": { @@ -962,9 +962,9 @@ "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.0.1.tgz", - "integrity": "sha512-m1KvHlueScy4mQJWvFDCxFBTIdXS0K1SgFGLmqHyX90mZdCIv6gWBbKRhatxRjhGlONuTK/hztYdaqrTXcFZdQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.0.tgz", + "integrity": "sha512-16iNOa4rTTjaWtfsPGJcYYL79FJakseX8TQFIPfVuSPC3s5nkS/DSNQPFPc5lJHgEDBWNMxSApHrEymNblhA9w==", "license": "MIT", "dependencies": { "@octokit/types": "^14.1.0" @@ -1720,9 +1720,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.3.tgz", - "integrity": "sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1744,8 +1744,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.3", - "vitest": "3.2.3" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1754,15 +1754,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz", - "integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.3", - "@vitest/utils": "3.2.3", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1771,13 +1771,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz", - "integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.3", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1798,9 +1798,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", - "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -1811,13 +1811,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz", - "integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.3", + "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -1826,13 +1826,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz", - "integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.3", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1841,9 +1841,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz", - "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { @@ -1854,14 +1854,14 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz", - "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.3", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -4118,9 +4118,9 @@ } }, "node_modules/openai": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-5.3.0.tgz", - "integrity": "sha512-VIKmoF7y4oJCDOwP/oHXGzM69+x0dpGFmN9QmYO+uPbLFOmmnwO+x1GbsgUtI+6oraxomGZ566Y421oYVu191w==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.5.1.tgz", + "integrity": "sha512-5i19097mGotHA1eFsM6Tjd/tJ8uo9sa5Ysv4Q6bKJ2vtN6rc0MzMrUefXnLXYAJcmMQrC1Efhj0AvfIkXrQamw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5541,9 +5541,9 @@ } }, "node_modules/tinypool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz", - "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -5976,9 +5976,9 @@ } }, "node_modules/vite-node": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz", - "integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { @@ -5999,20 +5999,20 @@ } }, "node_modules/vitest": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz", - "integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.3", - "@vitest/mocker": "3.2.3", - "@vitest/pretty-format": "^3.2.3", - "@vitest/runner": "3.2.3", - "@vitest/snapshot": "3.2.3", - "@vitest/spy": "3.2.3", - "@vitest/utils": "3.2.3", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -6023,10 +6023,10 @@ "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", - "tinypool": "^1.1.0", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.3", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -6042,8 +6042,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.3", - "@vitest/ui": "3.2.3", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, @@ -6228,9 +6228,9 @@ } }, "node_modules/zod": { - "version": "3.25.64", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz", - "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==", + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", "dev": true, "license": "MIT", "funding": { From 20c7844a8bb51d86e727b7c51c9f64802de801ef Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Thu, 19 Jun 2025 06:08:50 +0000 Subject: [PATCH 14/16] feat: support optional v prefix with directory separator config - Removed deprecated action-metadata types and utility functions. - Introduced new metadata structure for action inputs in `metadata.ts`. - Updated configuration handling to support dynamic input mapping. - Enhanced tag processing to accommodate various directory separators. - Added tests for metadata input handling and error scenarios. - Improved regex patterns for module tag validation. - Updated action.yml to include new input parameters for tag directory separator and version prefix usage. - Refactored TerraformModule class to utilize new configuration settings for versioning. --- __mocks__/config.ts | 4 +- __tests__/config.test.ts | 25 +- __tests__/context.test.ts | 4 +- __tests__/helpers/action-defaults.ts | 2 +- __tests__/helpers/inputs.ts | 2 +- __tests__/terraform-module.test.ts | 248 +++++++++++++++++- __tests__/utils/metadata.test.ts | 24 ++ action.yml | 8 + src/config.ts | 11 +- src/terraform-module.ts | 131 +++++---- src/types/config.types.ts | 3 +- src/types/index.ts | 2 +- ...on-metadata.types.ts => metadata.types.ts} | 0 src/utils/constants.ts | 35 ++- src/utils/{action-metadata.ts => metadata.ts} | 0 15 files changed, 417 insertions(+), 82 deletions(-) create mode 100644 __tests__/utils/metadata.test.ts rename src/types/{action-metadata.types.ts => metadata.types.ts} (100%) rename src/utils/{action-metadata.ts => metadata.ts} (100%) diff --git a/__mocks__/config.ts b/__mocks__/config.ts index 6335d5c..2909b5a 100644 --- a/__mocks__/config.ts +++ b/__mocks__/config.ts @@ -1,7 +1,7 @@ -import { ACTION_INPUTS, createConfigFromInputs } from '@/utils/action-metadata'; +import { setupTestInputs } from '@/tests/helpers/inputs'; import type { Config } from '@/types'; import type { ActionInputMetadata } from '@/types'; -import { setupTestInputs } from '@/tests/helpers/inputs'; +import { ACTION_INPUTS, createConfigFromInputs } from '@/utils/metadata'; /** * Configuration interface with added utility methods diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index a3647f1..1cc95cf 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -9,6 +9,7 @@ import { setupTestInputs, stringInputs, } from '@/tests/helpers/inputs'; +import { VALID_TAG_DIRECTORY_SEPARATORS } from '@/utils/constants'; import { endGroup, getBooleanInput, getInput, info, startGroup } from '@actions/core'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -191,22 +192,32 @@ describe('config', () => { it('should throw error for invalid tag directory separator length', () => { setupTestInputs({ 'tag-directory-separator': 'ab' }); - expect(() => getConfig()).toThrow( - new TypeError('Tag directory separator must be exactly one character'), - ); + expect(() => getConfig()).toThrow(new TypeError('Tag directory separator must be exactly one character')); }); it('should throw error for invalid tag directory separator character', () => { setupTestInputs({ 'tag-directory-separator': '@' }); expect(() => getConfig()).toThrow( - new TypeError("Tag directory separator must be one of: -, _, /, .. Got: '@'"), + new TypeError(`Tag directory separator must be one of: ${VALID_TAG_DIRECTORY_SEPARATORS.join(', ')}. Got: '@'`), ); }); + it('should allow valid tag directory separators', () => { + for (const separator of VALID_TAG_DIRECTORY_SEPARATORS) { + clearConfigForTesting(); + vi.unstubAllEnvs(); + setupTestInputs({ 'tag-directory-separator': separator }); + const config = getConfig(); + expect(config.tagDirectorySeparator).toBe(separator); + } + }); + it('should throw error for invalid default first tag format', () => { setupTestInputs({ 'default-first-tag': 'invalid-tag' }); expect(() => getConfig()).toThrow( - new TypeError("Default first tag must be in format v#.#.# or #.#.# (e.g., v1.0.0 or 1.0.0). Got: 'invalid-tag'"), + new TypeError( + "Default first tag must be in format v#.#.# or #.#.# (e.g., v1.0.0 or 1.0.0). Got: 'invalid-tag'", + ), ); clearConfigForTesting(); @@ -229,7 +240,7 @@ describe('config', () => { it('should initialize with valid default inputs', () => { const config = getConfig(); - expect(config.majorKeywords).toEqual(['major change', 'breaking change', '!']); + expect(config.majorKeywords).toEqual(['major change', 'breaking change']); expect(config.minorKeywords).toEqual(['feat', 'feature']); expect(config.patchKeywords).toEqual(['fix', 'chore', 'docs']); expect(config.defaultFirstTag).toBe('v1.0.0'); @@ -250,7 +261,7 @@ describe('config', () => { expect(startGroup).toHaveBeenCalledTimes(1); expect(endGroup).toHaveBeenCalledTimes(1); expect(vi.mocked(info).mock.calls).toEqual([ - ['Major Keywords: major change, breaking change, !'], + ['Major Keywords: major change, breaking change'], ['Minor Keywords: feat, feature'], ['Patch Keywords: fix, chore, docs'], ['Default First Tag: v1.0.0'], diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index 17f2f7c..b84b20c 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -48,7 +48,7 @@ describe('context', () => { it(`should throw an error if ${envVar} is not set`, () => { // Set the specific environment variable to undefined, but keep others set vi.stubEnv(envVar, undefined); - + expect(() => getContext()).toThrow( new Error( `The ${envVar} environment variable is missing or invalid. This variable should be automatically set by GitHub for each workflow run. If this variable is missing or not correctly set, it indicates a serious issue with the GitHub Actions environment, potentially affecting the execution of subsequent steps in the workflow. Please review the workflow setup or consult the documentation for proper configuration.`, @@ -60,7 +60,6 @@ describe('context', () => { describe('event validation', () => { it('should throw error when event is not pull_request', () => { - vi.stubEnv('GITHUB_EVENT_NAME', 'push'); expect(() => getContext()).toThrow('This workflow is not running in the context of a pull request'); }); @@ -196,7 +195,6 @@ describe('context', () => { // Ensure GITHUB_API_URL is not set to test the default fallback vi.stubEnv('GITHUB_API_URL', undefined); - const context = getContext(); // Check that the context was created with default API URL diff --git a/__tests__/helpers/action-defaults.ts b/__tests__/helpers/action-defaults.ts index f647dc6..ff549e9 100644 --- a/__tests__/helpers/action-defaults.ts +++ b/__tests__/helpers/action-defaults.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { ACTION_INPUTS } from '@/utils/action-metadata'; +import { ACTION_INPUTS } from '@/utils/metadata'; import * as yaml from 'js-yaml'; /** diff --git a/__tests__/helpers/inputs.ts b/__tests__/helpers/inputs.ts index 2a0526f..0801dbd 100644 --- a/__tests__/helpers/inputs.ts +++ b/__tests__/helpers/inputs.ts @@ -1,6 +1,6 @@ import { getActionDefaults } from '@/tests/helpers/action-defaults'; import type { Config } from '@/types'; -import { ACTION_INPUTS } from '@/utils/action-metadata'; +import { ACTION_INPUTS } from '@/utils/metadata'; import { vi } from 'vitest'; // Load action defaults once globally diff --git a/__tests__/terraform-module.test.ts b/__tests__/terraform-module.test.ts index 13818d2..e99cdcf 100644 --- a/__tests__/terraform-module.test.ts +++ b/__tests__/terraform-module.test.ts @@ -33,6 +33,7 @@ describe('TerraformModule', () => { defaultFirstTag: 'v0.1.0', moduleChangeExcludePatterns: [], modulePathIgnore: [], + useVersionPrefix: true, }); }); @@ -245,6 +246,38 @@ describe('TerraformModule', () => { expect(module.getLatestTagVersion()).toBeNull(); }); + it('should handle tags with different separators in getLatestTagVersion', () => { + // Test with different separators to ensure regex works correctly + module.setTags(['tf-modules/test-module/v1.0.0']); + expect(module.getLatestTagVersion()).toBe('v1.0.0'); + + // Test with hyphen separator + module.setTags(['tf-modules-test-module-v2.0.0']); + expect(module.getLatestTagVersion()).toBe('v2.0.0'); + + // Test with underscore separator + module.setTags(['tf-modules_test_module_v3.0.0']); + expect(module.getLatestTagVersion()).toBe('v3.0.0'); + + // Test with dot separator + module.setTags(['tf-modules.test.module.v4.0.0']); + expect(module.getLatestTagVersion()).toBe('v4.0.0'); + + // Test without v prefix + module.setTags(['tf-modules/test-module/5.0.0']); + expect(module.getLatestTagVersion()).toBe('5.0.0'); + }); + + it('should return null when latest tag does not match MODULE_TAG_REGEX', () => { + // Mock getLatestTag to return an invalid format that won't match the regex + vi.spyOn(module, 'getLatestTag').mockReturnValue('invalid-tag-format'); + + expect(module.getLatestTagVersion()).toBeNull(); + + // Restore the original method + vi.restoreAllMocks(); + }); + it('should handle complex version sorting', () => { const tags = [ 'tf-modules/test-module/v1.2.10', @@ -275,14 +308,14 @@ describe('TerraformModule', () => { it('should throw error for tag with no slash (invalid format)', () => { const tags = ['v1.2.3']; expect(() => module.setTags(tags)).toThrow( - "Invalid tag format: 'v1.2.3'. Expected format: 'tf-modules/test-module/v#.#.#' or 'tf-modules/test-module/#.#.#' for module.", + "Invalid tag format: 'v1.2.3'. Expected format: 'tf-modules/test-module[separator]v#.#.#' or 'tf-modules/test-module[separator]#.#.#'.", ); }); it('should throw error for tag with incorrect module name', () => { const tags = ['foo/bar/v9.8.7']; expect(() => module.setTags(tags)).toThrow( - "Invalid tag format: 'foo/bar/v9.8.7'. Expected format: 'tf-modules/test-module/v#.#.#' or 'tf-modules/test-module/#.#.#' for module.", + "Invalid tag format: 'foo/bar/v9.8.7'. Expected format: 'tf-modules/test-module[separator]v#.#.#' or 'tf-modules/test-module[separator]#.#.#'.", ); }); @@ -304,7 +337,6 @@ describe('TerraformModule', () => { expect(module.tags[1]).toBe('tf-modules/test-module/1.2.3'); }); - // Add tests for extractVersionFromTag describe('extractVersionFromTag()', () => { let module: TerraformModule; @@ -473,7 +505,7 @@ describe('TerraformModule', () => { ]; expect(() => module.setReleases(releases)).toThrow( - "Invalid tag format: 'tf-modules/test-module/v1.0'. Expected format: 'tf-modules/test-module/v#.#.#' or 'tf-modules/test-module/#.#.#' for module.", + "Invalid tag format: 'tf-modules/test-module/v1.0'. Expected format: 'tf-modules/test-module[separator]v#.#.#' or 'tf-modules/test-module[separator]#.#.#'.", ); }); @@ -494,7 +526,7 @@ describe('TerraformModule', () => { ]; expect(() => module.setReleases(releases)).toThrow( - "Invalid tag format: 'tf-modules/test-module/vbeta.1.0'. Expected format: 'tf-modules/test-module/v#.#.#' or 'tf-modules/test-module/#.#.#' for module.", + "Invalid tag format: 'tf-modules/test-module/vbeta.1.0'. Expected format: 'tf-modules/test-module[separator]v#.#.#' or 'tf-modules/test-module[separator]#.#.#'.", ); }); @@ -788,6 +820,38 @@ describe('TerraformModule', () => { "Invalid version format: 'invalid-format'. Expected v#.#.# or #.#.# format.", ); }); + + it('should respect useVersionPrefix setting when true (with v prefix)', () => { + // Set useVersionPrefix to true + config.set({ + useVersionPrefix: true, + }); + + module.setTags(['tf-modules/test-module/v1.2.3']); + module.addCommit({ + sha: 'abc123', + message: 'fix: bug fix', + files: ['main.tf'], + }); + + expect(module.getReleaseTagVersion()).toBe('v1.2.4'); + }); + + it('should respect useVersionPrefix setting when false (without v prefix)', () => { + // Set useVersionPrefix to false + config.set({ + useVersionPrefix: false, + }); + + module.setTags(['tf-modules/test-module/v1.2.3']); + module.addCommit({ + sha: 'abc123', + message: 'fix: bug fix', + files: ['main.tf'], + }); + + expect(module.getReleaseTagVersion()).toBe('1.2.4'); // No 'v' prefix + }); }); describe('getReleaseTag()', () => { @@ -954,17 +1018,48 @@ describe('TerraformModule', () => { TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules/vpc-endpoint/v1.0.0'), ).toBe(true); expect( - TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules/vpc-endpoint/1.0.0'), + TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules-vpc-endpoint-v1.0.0'), + ).toBe(true); + expect( + TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules_vpc_endpoint_v1.0.0'), + ).toBe(true); + expect( + TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules.vpc.endpoint.v1.0.0'), ).toBe(true); - expect(TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules/vpc/v1.0.0')).toBe( - false, - ); }); it('should be case sensitive', () => { expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'My-Module/v1.0.0')).toBe(false); expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'my-module/V1.0.0')).toBe(false); }); + + it('should handle tags with different directory separators', () => { + // Test that tags with different separators are properly associated after normalization + expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'my-module/v1.0.0')).toBe(true); + expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'my-module-v1.0.0')).toBe(true); + expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'my-module_v1.0.0')).toBe(true); + expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'my-module.v1.0.0')).toBe(true); + + // Test complex module names with separators + expect( + TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules/vpc-endpoint/v1.0.0'), + ).toBe(true); + expect( + TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules-vpc-endpoint-v1.0.0'), + ).toBe(true); + expect( + TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules_vpc_endpoint_v1.0.0'), + ).toBe(true); + expect( + TerraformModule.isModuleAssociatedWithTag('tf-modules/vpc-endpoint', 'tf-modules.vpc.endpoint.v1.0.0'), + ).toBe(true); + + // Test that wrong associations still return false + expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'other-module/v1.0.0')).toBe(false); + expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'other-module-v1.0.0')).toBe(false); + expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'other-module_v1.0.0')).toBe(false); + expect(TerraformModule.isModuleAssociatedWithTag('my-module', 'other-module.v1.0.0')).toBe(false); + }); }); describe('getTagsForModule()', () => { @@ -1147,6 +1242,45 @@ describe('TerraformModule', () => { expect(tagsToDelete).toEqual(['apple-module/v1.0.0', 'banana-module/v1.0.0', 'zebra-module/v1.0.0']); }); + + it('should handle tags with different directory separators for same module', () => { + // Scenario: Module was originally using / separator, but tags exist with various separators + const allTags = [ + 'test-module/v1.0.0', // Forward slash (current format) + 'test-module-v1.1.0', // Hyphen (old format) + 'test-module_v1.2.0', // Underscore (old format) + 'test-module.v1.3.0', // Dot (old format) + 'other-module/v1.0.0', // Different module that no longer exists + ]; + const existingModules = [createMockTerraformModule({ directory: join(tmpDir, 'test-module') })]; + + const tagsToDelete = TerraformModule.getTagsToDelete(allTags, existingModules); + + // Only tags for non-existent modules should be deleted + // All test-module tags should be kept regardless of separator + expect(tagsToDelete).toEqual(['other-module/v1.0.0']); + }); + + it('should handle complex module names with various separators', () => { + const allTags = [ + 'tf-modules/vpc-endpoint/v1.0.0', // Current format + 'tf-modules-vpc-endpoint-v1.1.0', // All hyphens + 'tf-modules_vpc_endpoint_v1.2.0', // All underscores + 'tf-modules.vpc.endpoint.v1.3.0', // All dots + 'tf-modules/vpc-endpoint-v1.4.0', // Mixed separators + 'removed-module/v1.0.0', // Module that no longer exists + ]; + const existingModules = [ + createMockTerraformModule({ + directory: join(tmpDir, 'tf-modules', 'vpc-endpoint'), + }), + ]; + + const tagsToDelete = TerraformModule.getTagsToDelete(allTags, existingModules); + + // Only tags for non-existent modules should be deleted + expect(tagsToDelete).toEqual(['removed-module/v1.0.0']); + }); }); describe('getReleasesToDelete()', () => { @@ -1536,6 +1670,102 @@ describe('TerraformModule', () => { expect(releasesToDelete).toHaveLength(2); expect(releasesToDelete.map((r) => r.tagName)).toEqual(['legacy-module/v1.0.0', 'legacy-module/v1.2.0']); }); + + it('should handle releases with different directory separators for same module', () => { + // Scenario: Module was originally using / separator, but releases exist with various separators + const allReleases: GitHubRelease[] = [ + { + id: 1, + title: 'test-module/v1.0.0', + tagName: 'test-module/v1.0.0', + body: 'Forward slash format', + }, + { + id: 2, + title: 'test-module-v1.1.0', + tagName: 'test-module-v1.1.0', + body: 'Hyphen format', + }, + { + id: 3, + title: 'test-module_v1.2.0', + tagName: 'test-module_v1.2.0', + body: 'Underscore format', + }, + { + id: 4, + title: 'test-module.v1.3.0', + tagName: 'test-module.v1.3.0', + body: 'Dot format', + }, + { + id: 5, + title: 'other-module/v1.0.0', + tagName: 'other-module/v1.0.0', + body: 'Different module that no longer exists', + }, + ]; + const existingModules = [createMockTerraformModule({ directory: join(tmpDir, 'test-module') })]; + + const releasesToDelete = TerraformModule.getReleasesToDelete(allReleases, existingModules); + + // Only releases for non-existent modules should be deleted + // All test-module releases should be kept regardless of separator + expect(releasesToDelete).toHaveLength(1); + expect(releasesToDelete[0].tagName).toBe('other-module/v1.0.0'); + }); + + it('should handle complex module names with various separators in releases', () => { + const allReleases: GitHubRelease[] = [ + { + id: 1, + title: 'tf-modules/vpc-endpoint/v1.0.0', + tagName: 'tf-modules/vpc-endpoint/v1.0.0', + body: 'Current format', + }, + { + id: 2, + title: 'tf-modules-vpc-endpoint-v1.1.0', + tagName: 'tf-modules-vpc-endpoint-v1.1.0', + body: 'All hyphens', + }, + { + id: 3, + title: 'tf-modules_vpc_endpoint_v1.2.0', + tagName: 'tf-modules_vpc_endpoint_v1.2.0', + body: 'All underscores', + }, + { + id: 4, + title: 'tf-modules.vpc.endpoint.v1.3.0', + tagName: 'tf-modules.vpc.endpoint.v1.3.0', + body: 'All dots', + }, + { + id: 5, + title: 'tf-modules/vpc-endpoint-v1.4.0', + tagName: 'tf-modules/vpc-endpoint-v1.4.0', + body: 'Mixed separators', + }, + { + id: 6, + title: 'removed-module/v1.0.0', + tagName: 'removed-module/v1.0.0', + body: 'Module that no longer exists', + }, + ]; + const existingModules = [ + createMockTerraformModule({ + directory: join(tmpDir, 'tf-modules', 'vpc-endpoint'), + }), + ]; + + const releasesToDelete = TerraformModule.getReleasesToDelete(allReleases, existingModules); + + // Only releases for non-existent modules should be deleted + expect(releasesToDelete).toHaveLength(1); + expect(releasesToDelete[0].tagName).toBe('removed-module/v1.0.0'); + }); }); // Test private helper methods via the public interface diff --git a/__tests__/utils/metadata.test.ts b/__tests__/utils/metadata.test.ts new file mode 100644 index 0000000..8220146 --- /dev/null +++ b/__tests__/utils/metadata.test.ts @@ -0,0 +1,24 @@ +import { createConfigFromInputs } from '@/utils/metadata'; +import { getInput } from '@actions/core'; +import { describe, expect, it, vi } from 'vitest'; + +describe('utils/metadata', () => { + it('should throw a custom error if getInput fails', () => { + const errorMessage = 'Input retrieval failed'; + vi.mocked(getInput).mockImplementation(() => { + throw new Error(errorMessage); + }); + + expect(() => createConfigFromInputs()).toThrow(`Failed to process input 'major-keywords': ${errorMessage}`); + }); + + it('should handle non-Error objects thrown during input processing', () => { + const errorObject = 'A plain string error'; + vi.mocked(getInput).mockImplementation(() => { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw errorObject; + }); + + expect(() => createConfigFromInputs()).toThrow(`Failed to process input 'major-keywords': ${String(errorObject)}`); + }); +}); diff --git a/action.yml b/action.yml index 124dd78..941b6d8 100644 --- a/action.yml +++ b/action.yml @@ -109,6 +109,14 @@ inputs: specific requirements. required: true default: ${{ github.token }} + tag-directory-separator: + description: The separator character to use between module directory parts in the git tag. + required: true + default: / + use-version-prefix: + description: Whether to include the 'v' prefix on version tags (e.g., v1.2.3). + required: true + default: "true" outputs: changed-module-names: diff --git a/src/config.ts b/src/config.ts index bc9933f..cd54879 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ import type { Config } from '@/types'; -import { createConfigFromInputs } from '@/utils/action-metadata'; -import { VERSION_TAG_REGEX } from '@/utils/constants'; +import { VALID_TAG_DIRECTORY_SEPARATORS, VERSION_TAG_REGEX } from '@/utils/constants'; +import { createConfigFromInputs } from '@/utils/metadata'; import { endGroup, info, startGroup } from '@actions/core'; // Keep configInstance private to this module @@ -56,13 +56,14 @@ function initializeConfig(): Config { } // Validate tag directory separator - const validSeparators = ['-', '_', '/', '.']; if (configInstance.tagDirectorySeparator.length !== 1) { throw new TypeError('Tag directory separator must be exactly one character'); } - if (!validSeparators.includes(configInstance.tagDirectorySeparator)) { + if (!VALID_TAG_DIRECTORY_SEPARATORS.includes(configInstance.tagDirectorySeparator)) { throw new TypeError( - `Tag directory separator must be one of: ${validSeparators.join(', ')}. Got: '${configInstance.tagDirectorySeparator}'`, + `Tag directory separator must be one of: ${VALID_TAG_DIRECTORY_SEPARATORS.join(', ')}. Got: '${ + configInstance.tagDirectorySeparator + }'`, ); } diff --git a/src/terraform-module.ts b/src/terraform-module.ts index 4d2894e..23618ef 100644 --- a/src/terraform-module.ts +++ b/src/terraform-module.ts @@ -2,7 +2,13 @@ import { relative } from 'node:path'; import { config } from '@/config'; import { context } from '@/context'; import type { CommitDetails, GitHubRelease, ReleaseReason, ReleaseType } from '@/types'; -import { MODULE_TAG_REGEX, RELEASE_REASON, RELEASE_TYPE, VERSION_TAG_REGEX } from '@/utils/constants'; +import { + MODULE_TAG_REGEX, + RELEASE_REASON, + RELEASE_TYPE, + VALID_TAG_DIRECTORY_SEPARATORS, + VERSION_TAG_REGEX, +} from '@/utils/constants'; import { removeTrailingCharacters } from '@/utils/string'; import { endGroup, info, startGroup } from '@actions/core'; @@ -213,11 +219,14 @@ export class TerraformModule { * @returns {string | null} The version string including any prefixes (e.g., 'v1.2.3' or '1.2.3'), or null if no tags exist. */ public getLatestTagVersion(): string | null { - if (this.tags.length === 0) { + const latestTag = this.getLatestTag(); + if (latestTag === null) { return null; } - return this.tags[0].replace(`${this.name}/`, ''); + const match = latestTag.match(MODULE_TAG_REGEX); + + return match ? match[3] : null; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -439,8 +448,7 @@ export class TerraformModule { semver[2]++; } - // Hard coding "v" for now. Potentially fixing in the future. - return `v${semver.join('.')}`; + return `${config.useVersionPrefix ? 'v' : ''}${semver.join('.')}`; } /** @@ -469,35 +477,43 @@ export class TerraformModule { ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Helper ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /** - * Safely extracts the numerical version string from a tag, avoiding regex vulnerabilities. - * Handles tags in format: moduleName/vX.Y.Z or moduleName/X.Y.Z - * Also validates that tag format matches expected pattern and returns only the numerical part. + * Extracts and validates the version string from a Terraform module tag. * - * @param {string} tag - The tag string to extract version from - * @returns {string} The numerical version string (e.g., "1.2.3") - * @throws {Error} If the tag does not match the required format + * Uses the MODULE_TAG_REGEX to validate the tag format and extract version components. + * This method leverages the static validation logic to ensure consistent tag processing + * across different separator formats (/, -, _, .). + * + * @param {string} tag - The tag string to extract version from (e.g., "module/v1.2.3", "module-v1.2.3") + * @returns {string} The numerical version string without prefix (e.g., "1.2.3") + * @throws {Error} If the tag does not match the required format or is not associated with this module */ private extractVersionFromTag(tag: string): string { - // Validate tag format - must start with module name followed by slash - if (!tag.startsWith(`${this.name}/`)) { + // Use the static validation method to ensure the tag is associated with this module + if (!TerraformModule.isModuleAssociatedWithTag(this.name, tag)) { throw new Error( - `Invalid tag format: '${tag}'. Expected format: '${this.name}/v#.#.#' or '${this.name}/#.#.#' for module.`, + `Invalid tag format: '${tag}'. Expected format: '${this.name}[separator]v#.#.#' or '${this.name}[separator]#.#.#'.`, ); } - // Extract everything after the last slash - const versionPart = tag.substring(tag.lastIndexOf('/') + 1); - - // Validate that the version part matches the expected format - if (!VERSION_TAG_REGEX.test(versionPart)) { + // Parse the tag using MODULE_TAG_REGEX to extract version components + // Note: This will never be null since TerraformModule.isModuleAssociatedWithTag already checks for this; + // however, for typing, we'll just recheck. + const match = MODULE_TAG_REGEX.exec(tag); + /* v8 ignore next 5 */ + if (!match) { throw new Error( - `Invalid tag format: '${tag}'. Expected format: '${this.name}/v#.#.#' or '${this.name}/#.#.#' for module.`, + `Invalid tag format: '${tag}'. Expected format: '${this.name}[separator]v#.#.#' or '${this.name}[separator]#.#.#'.`, ); } - // Return only the numerical part, stripping the 'v' prefix if present - return versionPart.startsWith('v') ? versionPart.substring(1) : versionPart; + // Extract the numerical version components (groups 4, 5, 6 are major.minor.patch) + const major = match[4]; + const minor = match[5]; + const patch = match[6]; + + return `${major}.${minor}.${patch}`; } /** @@ -628,23 +644,42 @@ export class TerraformModule { /** * Static utility to check if a tag is associated with a given module name. - * Supports both versioned tags ({moduleName}/v#.#.#) and non-versioned tags ({moduleName}/#.#.#). + * Supports multiple directory separators and handles cases where tagging schemes + * may have changed over time (e.g., from 'module-name/v1.0.0' to 'module-name-v1.1.0'). * - * @param {string} moduleName - The Terraform module name + * @param {string} moduleName - The Terraform module name (assumed to be cleaned) * @param {string} tag - The tag to check * @returns {boolean} True if the tag belongs to the module and has valid version format */ public static isModuleAssociatedWithTag(moduleName: string, tag: string): boolean { - // Check if tag starts with exactly the module name followed by a slash - if (!tag.startsWith(`${moduleName}/`)) { + // Use the existing MODULE_TAG_REGEX to parse the tag and extract module name + version + const match = MODULE_TAG_REGEX.exec(tag); + if (!match) { + // The tag doesn't match the expected "module-name/version" format return false; } - // Extract the version part after the module name and slash - const versionPart = tag.substring(moduleName.length + 1); + // Extract the module name part from the tag (group 1) + const moduleNameFromTag = match[1]; + + // Define a consistent separator to normalize module names + const NORMALIZE_SEPARATOR = '|'; + + // Normalize both the input moduleName and the extracted module name from the tag. + // This allows for comparison even if the tagging scheme changed over time + // (e.g., from 'my/module/v1.0.0' to 'my-module-v1.1.0'). + const normalizeName = (name: string): string => { + // Replace all valid tag directory separators with a consistent separator + // This handles cases where different separators were used in different tags + let normalized = name; + for (const separator of VALID_TAG_DIRECTORY_SEPARATORS) { + normalized = normalized.replaceAll(separator, NORMALIZE_SEPARATOR); + } + return normalized; + }; - // Check if version part matches either v#.#.# or #.#.# format - return VERSION_TAG_REGEX.test(versionPart); + // Compare the normalized names to determine if they match + return normalizeName(moduleName) === normalizeName(moduleNameFromTag); } /** @@ -683,8 +718,9 @@ export class TerraformModule { * Determines an array of Terraform tags that need to be deleted. * * Identifies tags that belong to modules no longer present in the current - * module list by filtering tags that match the pattern {moduleName}/vX.Y.Z - * where the module name is not in the current modules. + * module list by checking if any current module is associated with each tag. + * This approach leverages the robust tag association logic that handles + * different separator schemes over time. * * @param {string[]} allTags - A list of all tags associated with the modules. * @param {TerraformModule[]} terraformModules - An array of Terraform modules. @@ -693,16 +729,12 @@ export class TerraformModule { public static getTagsToDelete(allTags: string[], terraformModules: TerraformModule[]): string[] { startGroup('Finding all Terraform tags that should be deleted'); - // Get module names from current terraformModules (these exist in source) - const moduleNamesFromModules = new Set(terraformModules.map((module) => module.name)); - - // Filter tags that belong to modules no longer in the current module list + // Filter tags that are not associated with any current module const tagsToRemove = allTags .filter((tag) => { - // Extract the Terraform module name from tag by removing the version suffix - const match = MODULE_TAG_REGEX.exec(tag); - const moduleName = match ? match[1] : tag; - return !moduleNamesFromModules.has(moduleName); + // Check if ANY current module is associated with this tag + // This handles cases where tagging schemes changed over time + return !terraformModules.some((module) => TerraformModule.isModuleAssociatedWithTag(module.name, tag)); }) .sort((a, b) => a.localeCompare(b)); @@ -718,8 +750,9 @@ export class TerraformModule { * Determines an array of Terraform releases that need to be deleted. * * Identifies releases that belong to modules no longer present in the current - * module list by filtering releases that match the pattern {moduleName}/vX.Y.Z - * where the module name is not in the current modules. + * module list by checking if any current module is associated with each release tag. + * This approach leverages the robust tag association logic that handles + * different separator schemes over time. * * @param {GitHubRelease[]} allReleases - A list of all releases associated with the modules. * @param {TerraformModule[]} terraformModules - An array of Terraform modules. @@ -736,16 +769,14 @@ export class TerraformModule { ): GitHubRelease[] { startGroup('Finding all Terraform releases that should be deleted'); - // Get module names from current terraformModules (these exist in source) - const moduleNamesFromModules = new Set(terraformModules.map((module) => module.name)); - - // Filter releases that belong to modules no longer in the current module list + // Filter releases that are not associated with any current module const releasesToRemove = allReleases .filter((release) => { - // Extract module name from versioned release tag - const match = MODULE_TAG_REGEX.exec(release.tagName); - const moduleName = match ? match[1] : release.tagName; - return !moduleNamesFromModules.has(moduleName); + // Check if ANY current module is associated with this release tag + // This handles cases where tagging schemes changed over time + return !terraformModules.some((module) => + TerraformModule.isModuleAssociatedWithTag(module.name, release.tagName), + ); }) .sort((a, b) => a.tagName.localeCompare(b.tagName)); diff --git a/src/types/config.types.ts b/src/types/config.types.ts index 9d726d4..72086d4 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -26,7 +26,8 @@ export interface Config { /** * Default first tag for initializing repositories without existing tags. - * This serves as the fallback tag when no tags are found in the repository. + * This serves as the fallback tag when no tags are found in the repository. Note this may + * be in the format of `v#.#.#` or `#.#.#` (e.g., `v1.0.0` or `1.0.0`). */ defaultFirstTag: string; diff --git a/src/types/index.ts b/src/types/index.ts index ca0629d..dd55ef1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ // Action metadata types -export * from './action-metadata.types'; +export * from './metadata.types'; // Common types export * from './common.types'; diff --git a/src/types/action-metadata.types.ts b/src/types/metadata.types.ts similarity index 100% rename from src/types/action-metadata.types.ts rename to src/types/metadata.types.ts diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 922f45d..630f664 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,3 +1,19 @@ +/** + * Defines valid separator characters for tag directory paths in Terraform module releases. + * + * When finding a Terraform module like `modules/aws/s3-bucket`, the release tag would typically be + * `modules/aws/s3-bucket/v1.0.0`. This constant allows for alternative separators in the tag path. + * + * For example, with these separators, the following tag formats would all be valid: + * - `modules/aws/s3-bucket/v1.0.0` (using '/') + * - `modules-aws-s3-bucket-v1.0.0` (using '-') + * - `modules_aws_s3_bucket_v1.0.0` (using '_') + * - `modules.aws.s3.bucket.v1.0.0` (using '.') + * + * The default separator is '/' as defined in action.yml.dddd + */ +export const VALID_TAG_DIRECTORY_SEPARATORS = ['-', '_', '/', '.']; + /** * Regular expression that matches version tags in the format of semantic versioning. * This regex validates version strings like "1.2.3" or "v1.2.3" and includes capture groups. @@ -8,13 +24,28 @@ * It allows either a numerical portion (e.g., "1.2.3") or one prefixed with 'v' (e.g., "v1.2.3"), * which is the proper semver default format. */ -export const VERSION_TAG_REGEX = /^v?(\d+)\.(\d+)\.(\d+)$/; +export const VERSION_TAG_REGEX = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/; /** * Matches a Terraform module tag in the format: module-name/v1.2.3 or module-name/1.2.3 * Group 1: module name, Group 2: version (with or without 'v' prefix) */ -export const MODULE_TAG_REGEX = /^(.+)\/(v?\d+\.\d+\.\d+)$/; +/** + * Regular expression pattern to match a module tag in the format: prefix + separator + version + * Where: + * - Group 1: prefix (e.g., "module", "feature") + * - Group 2: separator (one of: '-', '_', '/', '.') + * - Group 3: Complete version string with optional 'v' prefix (e.g., "v1.0.0", "1.0.0") + * - Group 4: Major version number + * - Group 5: Minor version number + * - Group 6: Patch version number + * + * Example matches: + * - "module-v1.0.0" → ["module-v1.0.0", "module", "-", "v1.0.0", "1", "0", "0"] + * - "feature_2.3.4" → ["feature_2.3.4", "feature", "_", "2.3.4", "2", "3", "4"] + * - "service/v0.1.0" → ["service/v0.1.0", "service", "/", "v0.1.0", "0", "1", "0"] + */ +export const MODULE_TAG_REGEX = /^(.+)([-_\/\.])(v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))$/; /** * Release type constants for semantic versioning diff --git a/src/utils/action-metadata.ts b/src/utils/metadata.ts similarity index 100% rename from src/utils/action-metadata.ts rename to src/utils/metadata.ts From aac46ea88056b9d4b7770bccfd5c8c7062d076f7 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Thu, 19 Jun 2025 19:48:20 +0000 Subject: [PATCH 15/16] fix: update rollup packages to version 4.44.0 for improved compatibility --- package-lock.json | 175 ++++++++++++++++++++++------------------------ 1 file changed, 84 insertions(+), 91 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bfc3c8..af3108a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1047,9 +1047,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", - "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", + "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", "cpu": [ "arm" ], @@ -1061,9 +1061,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", - "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", + "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", "cpu": [ "arm64" ], @@ -1075,9 +1075,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", - "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", "cpu": [ "arm64" ], @@ -1089,9 +1089,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", - "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", "cpu": [ "x64" ], @@ -1103,9 +1103,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", - "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", + "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", "cpu": [ "arm64" ], @@ -1117,9 +1117,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", - "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", + "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", "cpu": [ "x64" ], @@ -1131,9 +1131,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", - "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", + "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", "cpu": [ "arm" ], @@ -1145,9 +1145,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", - "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", + "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", "cpu": [ "arm" ], @@ -1159,9 +1159,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", - "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", "cpu": [ "arm64" ], @@ -1173,9 +1173,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", - "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", "cpu": [ "arm64" ], @@ -1187,9 +1187,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", - "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", + "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", "cpu": [ "loong64" ], @@ -1201,9 +1201,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", - "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", + "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", "cpu": [ "ppc64" ], @@ -1215,9 +1215,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", - "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", + "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", "cpu": [ "riscv64" ], @@ -1229,9 +1229,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", - "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", + "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", "cpu": [ "riscv64" ], @@ -1243,9 +1243,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", - "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", + "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", "cpu": [ "s390x" ], @@ -1257,9 +1257,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", - "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", "cpu": [ "x64" ], @@ -1271,9 +1271,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", - "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", "cpu": [ "x64" ], @@ -1285,9 +1285,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", - "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", "cpu": [ "arm64" ], @@ -1299,9 +1299,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", - "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", + "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", "cpu": [ "ia32" ], @@ -1313,9 +1313,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", - "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", "cpu": [ "x64" ], @@ -4756,13 +4756,13 @@ } }, "node_modules/rollup": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", - "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", + "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -4772,36 +4772,29 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.43.0", - "@rollup/rollup-android-arm64": "4.43.0", - "@rollup/rollup-darwin-arm64": "4.43.0", - "@rollup/rollup-darwin-x64": "4.43.0", - "@rollup/rollup-freebsd-arm64": "4.43.0", - "@rollup/rollup-freebsd-x64": "4.43.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", - "@rollup/rollup-linux-arm-musleabihf": "4.43.0", - "@rollup/rollup-linux-arm64-gnu": "4.43.0", - "@rollup/rollup-linux-arm64-musl": "4.43.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", - "@rollup/rollup-linux-riscv64-gnu": "4.43.0", - "@rollup/rollup-linux-riscv64-musl": "4.43.0", - "@rollup/rollup-linux-s390x-gnu": "4.43.0", - "@rollup/rollup-linux-x64-gnu": "4.43.0", - "@rollup/rollup-linux-x64-musl": "4.43.0", - "@rollup/rollup-win32-arm64-msvc": "4.43.0", - "@rollup/rollup-win32-ia32-msvc": "4.43.0", - "@rollup/rollup-win32-x64-msvc": "4.43.0", + "@rollup/rollup-android-arm-eabi": "4.44.0", + "@rollup/rollup-android-arm64": "4.44.0", + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-freebsd-arm64": "4.44.0", + "@rollup/rollup-freebsd-x64": "4.44.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", + "@rollup/rollup-linux-arm-musleabihf": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-musl": "4.44.0", + "@rollup/rollup-linux-s390x-gnu": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-ia32-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0", "fsevents": "~2.3.2" } }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", From a43d6865a99796fddeb305cf368f63b2db3e8c31 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Thu, 19 Jun 2025 19:50:00 +0000 Subject: [PATCH 16/16] feat: enhance tag generation configuration with detailed descriptions and examples for directory separator and version prefix options --- .github/workflows/ci.yml | 2 + .github/workflows/test.yml | 24 ++- README.md | 2 + __tests__/helpers/octokit.ts | 5 +- __tests__/terraform-module.test.ts | 234 +++++++++++++++++++++++++---- __tests__/utils/string.test.ts | 66 +++++--- action.yml | 17 ++- src/config.ts | 7 +- src/terraform-module.ts | 37 +++-- src/types/config.types.ts | 33 +++- src/types/metadata.types.ts | 35 ++++- src/utils/constants.ts | 6 +- src/utils/string.ts | 62 ++++---- 13 files changed, 405 insertions(+), 125 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71e5730..44bf736 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,8 @@ jobs: module-change-exclude-patterns: .gitignore,*.md,*.tftest.hcl,tests/**,examples/** module-asset-exclude-patterns: .gitignore,*.md,*.tftest.hcl,tests/** use-ssh-source-format: true + tag-directory-separator: "-" + use-version-prefix: false - name: Test Action Outputs id: test-outputs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7db760..bc65c08 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,9 +10,9 @@ on: types: [opened, synchronize, reopened] jobs: - tests: + typescript-tests: runs-on: ubuntu-latest - name: Test + name: TypeScript Tests permissions: contents: read steps: @@ -35,6 +35,26 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO_CI_TESTING }} + sonarqube-scan: + runs-on: ubuntu-latest + name: SonarQube Analysis + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + # Disabling shallow clone is recommended for improving relevancy of reporting + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: npm + + - name: Install Dependencies + run: npm ci --no-fund + - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf # v5 env: diff --git a/README.md b/README.md index 191101e..a7fcff0 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,8 @@ configuring the following optional input parameters as needed. | `module-change-exclude-patterns` | Comma-separated list of file patterns (relative to each module) to exclude from triggering version changes. Lets you release a module but control which files inside it do not force a version bump.
[Read more here](#understanding-the-filtering-options) | `.gitignore,*.md,*.tftest.hcl,tests/**` | | `module-asset-exclude-patterns` | A comma-separated list of file patterns to exclude when bundling a Terraform module for tag/release. Patterns follow glob syntax (e.g., `tests/\*\*`) and are relative to each Terraform module directory. Files matching these patterns will be excluded from the bundled output. | `.gitignore,*.md,*.tftest.hcl,tests/**` | | `use-ssh-source-format` | If enabled, all links to source code in generated Wiki documentation will use SSH standard format (e.g., `git::ssh://git@github.com/owner/repo.git`) instead of HTTPS format (`git::https://github.com/owner/repo.git`) | `false` | +| `tag-directory-separator` | Character used to separate directory path components in Git tags. Supports `/`, `-`, `_`, or `.` | `/` | +| `use-version-prefix` | Whether to include the 'v' prefix on version tags (e.g., v1.2.3 vs 1.2.3) | `true` | ### Understanding the filtering options diff --git a/__tests__/helpers/octokit.ts b/__tests__/helpers/octokit.ts index 36ddc83..cc8b794 100644 --- a/__tests__/helpers/octokit.ts +++ b/__tests__/helpers/octokit.ts @@ -1,5 +1,5 @@ import type { OctokitRestApi } from '@/types'; -import { trimSlashes } from '@/utils/string'; +import { removeTrailingCharacters } from '@/utils/string'; import { paginateRest } from '@octokit/plugin-paginate-rest'; import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'; import type { RestEndpointMethodTypes } from '@octokit/plugin-rest-endpoint-methods'; @@ -384,5 +384,6 @@ function getLinkHeader(slug: string, page: number, perPage: number, totalCount: const nextPage = page + 1; const lastPage = totalPages; - return `; rel="next", ; rel="last"`; + const slugTrimmed = removeTrailingCharacters(slug, ['/']); + return `; rel="next", ; rel="last"`; } diff --git a/__tests__/terraform-module.test.ts b/__tests__/terraform-module.test.ts index e99cdcf..7be18aa 100644 --- a/__tests__/terraform-module.test.ts +++ b/__tests__/terraform-module.test.ts @@ -58,7 +58,7 @@ describe('TerraformModule', () => { mkdirSync(specialDir, { recursive: true }); const module = new TerraformModule(specialDir); - expect(module.name).toBe('complex_module-name-with/chars'); + expect(module.name).toBe('complex_module-name.with/chars'); }); it('should handle nested directory paths', () => { @@ -949,44 +949,222 @@ describe('TerraformModule', () => { describe('static utilities', () => { describe('getTerraformModuleNameFromRelativePath()', () => { - it('should generate valid module names from paths', () => { - expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe( - 'tf-modules/simple-module', - ); + beforeEach(() => { + // Reset to default config for each test + config.set({ + tagDirectorySeparator: '/', + majorKeywords: ['BREAKING CHANGE', 'major change'], + minorKeywords: ['feat:', 'feature:'], + defaultFirstTag: 'v0.1.0', + moduleChangeExcludePatterns: [], + modulePathIgnore: [], + useVersionPrefix: true, + }); + }); - expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex_module-name.with/chars')).toBe( - 'complex_module-name-with/chars', - ); + describe('with different tag directory separators', () => { + it('should use forward slash separator by default', () => { + config.set({ tagDirectorySeparator: '/' }); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe( + 'tf-modules/simple-module', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex\\module\\windows\\path')).toBe( + 'complex/module/windows/path', + ); + }); - expect(TerraformModule.getTerraformModuleNameFromRelativePath('/leading/slash/')).toBe('leading/slash'); + it('should use hyphen separator when configured', () => { + config.set({ tagDirectorySeparator: '-' }); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe( + 'tf-modules-simple-module', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex\\module\\windows\\path')).toBe( + 'complex-module-windows-path', + ); + }); - expect(TerraformModule.getTerraformModuleNameFromRelativePath('module...with...dots')).toBe('module-with-dots'); - }); + it('should use underscore separator when configured', () => { + config.set({ tagDirectorySeparator: '_' }); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe( + 'tf-modules_simple-module', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex\\module\\windows\\path')).toBe( + 'complex_module_windows_path', + ); + }); - it('should handle leading and trailing slashes', () => { - expect(TerraformModule.getTerraformModuleNameFromRelativePath('/test-module/')).toBe('test-module'); + it('should use dot separator when configured', () => { + config.set({ tagDirectorySeparator: '.' }); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe( + 'tf-modules.simple-module', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex\\module\\windows\\path')).toBe( + 'complex.module.windows.path', + ); + }); }); - it('should handle multiple consecutive slashes', () => { - expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules//vpc//endpoint')).toBe( - 'tf-modules/vpc/endpoint', - ); - }); + describe('character normalization and cleanup', () => { + beforeEach(() => { + config.set({ tagDirectorySeparator: '/' }); + }); - it('should handle whitespace', () => { - expect(TerraformModule.getTerraformModuleNameFromRelativePath(' test module ')).toBe('test-module'); - }); + it('should normalize Windows backslashes to configured separator', () => { + expect(TerraformModule.getTerraformModuleNameFromRelativePath('windows\\path\\module')).toBe( + 'windows/path/module', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('mixed\\and/path\\separators')).toBe( + 'mixed/and/path/separators', + ); + }); - it('should convert to lowercase', () => { - expect(TerraformModule.getTerraformModuleNameFromRelativePath('Test-Module')).toBe('test-module'); - }); + it('should convert to lowercase', () => { + expect(TerraformModule.getTerraformModuleNameFromRelativePath('Test-Module')).toBe('test-module'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('UPPERCASE/MODULE')).toBe('uppercase/module'); + }); + + it('should replace invalid characters with hyphens', () => { + expect(TerraformModule.getTerraformModuleNameFromRelativePath('test@module!#$')).toBe('test-module'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('module%with&special*chars')).toBe( + 'module-with-special-chars', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('test module with spaces')).toBe( + 'test-module-with-spaces', + ); + }); + + it('should normalize consecutive special characters', () => { + expect(TerraformModule.getTerraformModuleNameFromRelativePath('module...with...dots')).toBe( + 'module.with.dots', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules//vpc//endpoint')).toBe( + 'tf-modules/vpc/endpoint', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('module---with---hyphens')).toBe( + 'module-with-hyphens', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('module___with___underscores')).toBe( + 'module_with_underscores', + ); + }); + + it('should remove leading and trailing special characters', () => { + expect(TerraformModule.getTerraformModuleNameFromRelativePath('/leading/slash/')).toBe('leading/slash'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('...leading.dots')).toBe('leading.dots'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('trailing.dots...')).toBe('trailing.dots'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('---leading-hyphens')).toBe('leading-hyphens'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('trailing-hyphens---')).toBe( + 'trailing-hyphens', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('___leading_underscores')).toBe( + 'leading_underscores', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('trailing_underscores___')).toBe( + 'trailing_underscores', + ); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('/.-_mixed_leading')).toBe('mixed_leading'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('mixed_trailing_.-/')).toBe('mixed_trailing'); + }); - it('should clean up invalid characters', () => { - expect(TerraformModule.getTerraformModuleNameFromRelativePath('test@module!#$')).toBe('test-module'); + it('should handle edge cases', () => { + expect(TerraformModule.getTerraformModuleNameFromRelativePath(' whitespace ')).toBe('whitespace'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('/test-module/')).toBe('test-module'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('single')).toBe('single'); + expect(TerraformModule.getTerraformModuleNameFromRelativePath('a')).toBe('a'); + }); }); - it('should remove trailing special characters', () => { - expect(TerraformModule.getTerraformModuleNameFromRelativePath('test-module-.')).toBe('test-module'); + describe('comprehensive scenarios with different separators', () => { + const testScenarios = [ + { + separator: '/', + input: 'tf-modules/aws/vpc-endpoint', + expected: 'tf-modules/aws/vpc-endpoint', + }, + { + separator: '-', + input: 'tf-modules/aws/vpc-endpoint', + expected: 'tf-modules-aws-vpc-endpoint', + }, + { + separator: '_', + input: 'tf-modules/aws/vpc-endpoint', + expected: 'tf-modules_aws_vpc-endpoint', + }, + { + separator: '.', + input: 'tf-modules/aws/vpc-endpoint', + expected: 'tf-modules.aws.vpc-endpoint', + }, + ]; + + for (const { separator, input, expected } of testScenarios) { + it(`should handle complex paths with ${separator} separator`, () => { + config.set({ tagDirectorySeparator: separator }); + expect(TerraformModule.getTerraformModuleNameFromRelativePath(input)).toBe(expected); + }); + } + + const complexTestScenarios = [ + { + separator: '/', + input: '//tf-modules//aws..vpc--endpoint__', + expected: 'tf-modules/aws.vpc-endpoint', + }, + { + separator: '-', + input: '//tf-modules//aws..vpc--endpoint__', + expected: 'tf-modules-aws.vpc-endpoint', + }, + { + separator: '_', + input: '//tf-modules//aws..vpc--endpoint__', + expected: 'tf-modules_aws.vpc-endpoint', + }, + { + separator: '.', + input: '//tf-modules//aws..vpc--endpoint__', + expected: 'tf-modules.aws.vpc-endpoint', + }, + ]; + + for (const { separator, input, expected } of complexTestScenarios) { + it(`should handle complex normalization with ${separator} separator`, () => { + config.set({ tagDirectorySeparator: separator }); + expect(TerraformModule.getTerraformModuleNameFromRelativePath(input)).toBe(expected); + }); + } + }); + + describe('real-world terraform module scenarios', () => { + it('should handle typical terraform module paths', () => { + config.set({ tagDirectorySeparator: '/' }); + + const testCases = [ + { input: 'modules/networking/vpc', expected: 'modules/networking/vpc' }, + { input: 'modules/compute/ec2-instance', expected: 'modules/compute/ec2-instance' }, + { input: 'modules/storage/s3-bucket', expected: 'modules/storage/s3-bucket' }, + { input: 'terraform/aws/rds_cluster', expected: 'terraform/aws/rds_cluster' }, + { input: 'tf-modules/azure/storage.account', expected: 'tf-modules/azure/storage.account' }, + ]; + + for (const { input, expected } of testCases) { + expect(TerraformModule.getTerraformModuleNameFromRelativePath(input)).toBe(expected); + } + }); + + it('should handle module paths with various separators configured', () => { + const separatorTests = [ + { separator: '-', input: 'modules/aws/vpc', expected: 'modules-aws-vpc' }, + { separator: '_', input: 'modules/aws/vpc', expected: 'modules_aws_vpc' }, + { separator: '.', input: 'modules/aws/vpc', expected: 'modules.aws.vpc' }, + ]; + + for (const { separator, input, expected } of separatorTests) { + config.set({ tagDirectorySeparator: separator }); + expect(TerraformModule.getTerraformModuleNameFromRelativePath(input)).toBe(expected); + } + }); }); }); diff --git a/__tests__/utils/string.test.ts b/__tests__/utils/string.test.ts index b2ce45d..5c68178 100644 --- a/__tests__/utils/string.test.ts +++ b/__tests__/utils/string.test.ts @@ -1,36 +1,49 @@ -import { removeTrailingCharacters, trimSlashes } from '@/utils/string'; +import { removeLeadingCharacters, removeTrailingCharacters } from '@/utils/string'; import { describe, expect, it } from 'vitest'; describe('utils/string', () => { - describe('trimSlashes', () => { - it('should remove leading and trailing slashes while preserving internal ones', () => { - const testCases = [ - { input: '/example/path/', expected: 'example/path' }, - { input: '///another/example///', expected: 'another/example' }, - { input: 'no/slashes', expected: 'no/slashes' }, - { input: '/', expected: '' }, - { input: '//', expected: '' }, - { input: '', expected: '' }, - { input: '/single/', expected: 'single' }, - { input: 'leading/', expected: 'leading' }, - { input: '/trailing', expected: 'trailing' }, - { input: '////multiple////slashes////', expected: 'multiple////slashes' }, - ]; - for (const { input, expected } of testCases) { - expect(trimSlashes(input)).toBe(expected); - } + describe('removeLeadingCharacters', () => { + it('should remove leading dots', () => { + expect(removeLeadingCharacters('...hello', ['.'])).toBe('hello'); + expect(removeLeadingCharacters('..module-name', ['.'])).toBe('module-name'); + expect(removeLeadingCharacters('.....test', ['.'])).toBe('test'); }); - it('should handle strings without any slashes', () => { - expect(trimSlashes('hello')).toBe('hello'); + it('should remove leading hyphens and underscores', () => { + expect(removeLeadingCharacters('--module-name', ['-'])).toBe('module-name'); + expect(removeLeadingCharacters('__module_name', ['_'])).toBe('module_name'); + expect(removeLeadingCharacters('-_module-name', ['-', '_'])).toBe('module-name'); }); - it('should return empty string when given only slashes', () => { - expect(trimSlashes('//////')).toBe(''); + it('should remove multiple leading character types', () => { + expect(removeLeadingCharacters('._-module-name', ['.', '-', '_'])).toBe('module-name'); + expect(removeLeadingCharacters('.--__test', ['.', '-', '_'])).toBe('test'); + expect(removeLeadingCharacters('___...---example', ['.', '-', '_'])).toBe('example'); }); - it('should preserve internal multiple slashes', () => { - expect(trimSlashes('/path//with///internal////slashes/')).toBe('path//with///internal////slashes'); + it('should preserve internal characters', () => { + expect(removeLeadingCharacters('.hello.world', ['.'])).toBe('hello.world'); + expect(removeLeadingCharacters('.-module-name.test', ['.', '-'])).toBe('module-name.test'); + expect(removeLeadingCharacters('_test_module_name', ['_'])).toBe('test_module_name'); + }); + + it('should handle edge cases', () => { + expect(removeLeadingCharacters('', ['.'])).toBe(''); + expect(removeLeadingCharacters('...', ['.'])).toBe(''); + expect(removeLeadingCharacters('---', ['-'])).toBe(''); + expect(removeLeadingCharacters('hello', ['.', '-', '_'])).toBe('hello'); + expect(removeLeadingCharacters('module', [])).toBe('module'); + }); + + it('should handle complex terraform module names', () => { + expect(removeLeadingCharacters('._-aws-vpc-module', ['.', '-', '_'])).toBe('aws-vpc-module'); + expect(removeLeadingCharacters('--tf-modules/vpc-endpoint', ['-', '_'])).toBe('tf-modules/vpc-endpoint'); + expect(removeLeadingCharacters('__modules/networking/vpc', ['_'])).toBe('modules/networking/vpc'); + }); + + it('should handle forward slashes in leading characters', () => { + expect(removeLeadingCharacters('/./module-name', ['/', '.'])).toBe('module-name'); + expect(removeLeadingCharacters('/./_-example', ['/', '.', '_', '-'])).toBe('example'); }); }); @@ -72,5 +85,10 @@ describe('utils/string', () => { expect(removeTrailingCharacters('tf-modules/vpc-endpoint--', ['-', '_'])).toBe('tf-modules/vpc-endpoint'); expect(removeTrailingCharacters('modules/networking/vpc__', ['_'])).toBe('modules/networking/vpc'); }); + + it('should handle forward slashes in trailing characters', () => { + expect(removeTrailingCharacters('module-name/.', ['/', '.'])).toBe('module-name'); + expect(removeTrailingCharacters('example-_./', ['/', '.', '_', '-'])).toBe('example'); + }); }); }); diff --git a/action.yml b/action.yml index 941b6d8..c00e812 100644 --- a/action.yml +++ b/action.yml @@ -110,11 +110,24 @@ inputs: required: true default: ${{ github.token }} tag-directory-separator: - description: The separator character to use between module directory parts in the git tag. + description: > + Character used to separate directory path components in Git tags. This separator is used to convert + module directory paths into tag names (e.g., 'modules/aws/s3-bucket' becomes 'modules-aws-s3-bucket-v1.0.0' + when using '-'). Must be a single character from: /, -, _, or . + + Examples with different separators: + - "/" (default): modules/aws/s3-bucket/v1.0.0 + - "-": modules-aws-s3-bucket-v1.0.0 + - "_": modules_aws_s3_bucket_v1.0.0 + - ".": modules.aws.s3.bucket.v1.0.0 required: true default: / use-version-prefix: - description: Whether to include the 'v' prefix on version tags (e.g., v1.2.3). + description: > + Whether to include the 'v' prefix on version tags (e.g., v1.2.3 vs 1.2.3). When enabled, all new version + tags will include the 'v' prefix. For initial releases, this setting takes precedence over any 'v' prefix + specified in the default-first-tag - if use-version-prefix is false and default-first-tag contains 'v', + the 'v' will be automatically removed to ensure consistency. required: true default: "true" diff --git a/src/config.ts b/src/config.ts index cd54879..e116d9f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -66,7 +66,6 @@ function initializeConfig(): Config { }'`, ); } - // Validate default first tag format if (!VERSION_TAG_REGEX.test(configInstance.defaultFirstTag)) { throw new TypeError( @@ -74,6 +73,12 @@ function initializeConfig(): Config { ); } + // If we aren't using "v" prefix but the default first tag was specified with a "v" + // prefix, then strip this to enforce. + if (!configInstance.useVersionPrefix && configInstance.defaultFirstTag.startsWith('v')) { + configInstance.defaultFirstTag = configInstance.defaultFirstTag.substring(1); + } + info(`Major Keywords: ${configInstance.majorKeywords.join(', ')}`); info(`Minor Keywords: ${configInstance.minorKeywords.join(', ')}`); info(`Patch Keywords: ${configInstance.patchKeywords.join(', ')}`); diff --git a/src/terraform-module.ts b/src/terraform-module.ts index 23618ef..047b0dd 100644 --- a/src/terraform-module.ts +++ b/src/terraform-module.ts @@ -9,7 +9,7 @@ import { VALID_TAG_DIRECTORY_SEPARATORS, VERSION_TAG_REGEX, } from '@/utils/constants'; -import { removeTrailingCharacters } from '@/utils/string'; +import { removeLeadingCharacters, removeTrailingCharacters } from '@/utils/string'; import { endGroup, info, startGroup } from '@actions/core'; /** @@ -224,7 +224,7 @@ export class TerraformModule { return null; } - const match = latestTag.match(MODULE_TAG_REGEX); + const match = MODULE_TAG_REGEX.exec(latestTag); return match ? match[3] : null; } @@ -471,7 +471,7 @@ export class TerraformModule { return null; } - return `${this.name}/${releaseTagVersion}`; + return `${this.name}${config.tagDirectorySeparator}${releaseTagVersion}`; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -617,29 +617,28 @@ export class TerraformModule { * * The function transforms the directory path by: * - Trimming whitespace - * - Replacing invalid characters with hyphens - * - Normalizing slashes - * - Removing leading/trailing slashes - * - Handling consecutive dots and hyphens - * - Removing any remaining whitespace * - Converting to lowercase (for consistency) - * - Removing trailing dots, hyphens, and underscores + * - Normalizing path separators (both backslashes and forward slashes) to the configured tag directory separator + * - Replacing invalid characters with hyphens (preserving only alphanumeric, "/", ".", "-", "_") + * - Normalizing consecutive special characters ("/", ".", "-", "_") to single instances + * - Removing leading/trailing special characters ("/", ".", "-", "_") using safe string operations * * @param {string} terraformDirectory - The relative directory path from which to generate the module name. * @returns {string} A valid Terraform module name based on the provided directory path. */ public static getTerraformModuleNameFromRelativePath(terraformDirectory: string): string { - const cleanedDirectory = terraformDirectory + let name = terraformDirectory .trim() - .replace(/[^a-zA-Z0-9/_-]+/g, '-') - .replace(/\/{2,}/g, '/') - .replace(/\/\.+/g, '/') - .replace(/(^\/|\/$)/g, '') - .replace(/\.\.+/g, '.') - .replace(/--+/g, '-') - .replace(/\s+/g, '') - .toLowerCase(); - return removeTrailingCharacters(cleanedDirectory, ['.', '-', '_']); + .toLowerCase() + .replace(/[/\\]/g, config.tagDirectorySeparator) // Normalize backslashes and forward slashes to configured separator + .replace(/[^a-zA-Z0-9/._-]+/g, '-') // Replace invalid characters with hyphens (preserve alphanumeric, /, ., _, -) + .replace(/[/._-]{2,}/g, (match) => match[0]); // Normalize consecutive special characters to single instances + + // Remove leading/trailing special characters safely without regex backtracking + name = removeLeadingCharacters(name, VALID_TAG_DIRECTORY_SEPARATORS); + name = removeTrailingCharacters(name, VALID_TAG_DIRECTORY_SEPARATORS); + + return name; } /** diff --git a/src/types/config.types.ts b/src/types/config.types.ts index 72086d4..8961f8c 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -105,21 +105,38 @@ export interface Config { useSSHSourceFormat: boolean; /** - * The character used to separate the module name from the version in tags. + * The character used to separate directory path components when creating Git tags from module paths. + * This separator is applied throughout the entire directory structure conversion process, not just + * between the module name and version. + * * Must be a single character and one of: -, _, /, . * - * Examples: - * - "/" (default): module/aws-s3-bucket/v1.0.0 - * - "-": module-aws-s3-bucket-v1.0.0 - * - "_": module_aws-s3-bucket_v1.0.0 - * - ".": module.aws-s3-bucket.v1.0.0 + * When converting a module path like 'modules/aws/s3-bucket' to a Git tag, this separator determines + * how directory separators (/) are replaced in the tag name portion: + * + * Examples with module path 'modules/aws/s3-bucket' and version 'v1.0.0': + * - "/" (default): modules/aws/s3-bucket/v1.0.0 + * - "-": modules-aws-s3-bucket-v1.0.0 + * - "_": modules_aws_s3_bucket_v1.0.0 + * - ".": modules.aws.s3-bucket.v1.0.0 + * + * This setting affects tag creation, tag parsing, and tag association logic throughout the system. */ tagDirectorySeparator: string; /** * Whether to include the "v" prefix in version tags. - * When true (default), tags will be formatted as: module/v1.2.3 - * When false, tags will be formatted as: module/1.2.3 + * + * When true (default), version tags will include the "v" prefix: + * - Example: module/v1.2.3 + * + * When false, version tags will not include the "v" prefix: + * - Example: module/1.2.3 + * + * For initial releases, this setting takes precedence over any "v" prefix specified in the + * defaultFirstTag configuration. If useVersionPrefix is false and defaultFirstTag contains + * a "v" prefix (e.g., "v1.0.0"), the "v" will be automatically removed to ensure consistency + * with the useVersionPrefix setting (resulting in "1.0.0"). */ useVersionPrefix: boolean; } diff --git a/src/types/metadata.types.ts b/src/types/metadata.types.ts index 0850844..4ebfff8 100644 --- a/src/types/metadata.types.ts +++ b/src/types/metadata.types.ts @@ -1,18 +1,41 @@ import type { Config } from '@/types/config.types'; /** - * Metadata about GitHub Action inputs, including their types, defaults, and mapping to config properties. - * This serves as the single source of truth for action configuration. + * Metadata definition for GitHub Action inputs that enables dynamic configuration mapping. * - * @todo update doc - defaults at runtime come from action.yml, testing see helpers/inputs.ts + * This interface serves as the translation layer between GitHub Action inputs defined in + * action.yml and our internal Config type. It provides the necessary metadata to: + * - Parse input values according to their expected types + * - Map action inputs to the corresponding config property names + * - Enforce required/optional input validation + * - Support dynamic config creation in createConfigFromInputs() + * + * The metadata is used by the ACTION_INPUTS constant in metadata.ts to create a + * comprehensive mapping of all action inputs, which then drives the automatic + * config generation process. + * + * @see {@link /workspaces/terraform-module-releaser/src/utils/metadata.ts} for usage + * @see {@link https://docs.github.com/en/actions/reference/metadata-syntax-for-github-actions#inputs} GitHub Actions input reference */ export interface ActionInputMetadata { - /** The config property name this input maps to */ + /** + * The config property name this input maps to. + * Must be a valid key from the Config interface. + */ configKey: keyof Config; - /** Whether this input is required */ + /** + * Whether this input is required by the GitHub Action. + * When true, the action will fail if the input is not provided. + */ required: boolean; - /** The input type for proper parsing */ + /** + * The expected data type of the input for proper parsing and validation. + * - 'string': Direct string value + * - 'boolean': Parsed using getBooleanInput for proper true/false handling + * - 'number': Parsed using parseInt for integer conversion + * - 'array': Comma-separated string parsed into array with deduplication + */ type: 'string' | 'boolean' | 'number' | 'array'; } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 630f664..7ed6719 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -44,8 +44,12 @@ export const VERSION_TAG_REGEX = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/; * - "module-v1.0.0" → ["module-v1.0.0", "module", "-", "v1.0.0", "1", "0", "0"] * - "feature_2.3.4" → ["feature_2.3.4", "feature", "_", "2.3.4", "2", "3", "4"] * - "service/v0.1.0" → ["service/v0.1.0", "service", "/", "v0.1.0", "0", "1", "0"] + * + * Note: In the character class [-_/.], only the dot (.) requires escaping to match literal periods. + * The hyphen (-) doesn't need escaping when at the start/end of the character class. + * The forward slash (/) doesn't need escaping in JavaScript regex character classes. */ -export const MODULE_TAG_REGEX = /^(.+)([-_\/\.])(v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))$/; +export const MODULE_TAG_REGEX = /^(.+)([-_/.])(v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))$/; /** * Release type constants for semantic versioning diff --git a/src/utils/string.ts b/src/utils/string.ts index 95e8817..6a83bb1 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -1,35 +1,3 @@ -/** - * Removes any leading and trailing slashes (/) from the given string. - * - * @param {string} str - The input string from which to trim slashes. - * @returns {string} - The string without leading or trailing slashes. - * - * @example - * // Returns "example/path" - * trimSlashes("/example/path/"); - * - * @example - * // Returns "another/example" - * trimSlashes("///another/example///"); - */ -export function trimSlashes(str: string): string { - let start = 0; - let end = str.length; - - // Remove leading slashes by adjusting start index - while (start < end && str[start] === '/') { - start++; - } - - // Remove trailing slashes by adjusting end index - while (end > start && str[end - 1] === '/') { - end--; - } - - // Return the substring without leading and trailing slashes - return str.slice(start, end); -} - /** * Removes trailing characters from a string without using regex. * @@ -55,5 +23,35 @@ export function removeTrailingCharacters(input: string, charactersToRemove: stri while (endIndex > 0 && charactersToRemove.includes(input[endIndex - 1])) { endIndex--; } + return input.slice(0, endIndex); } + +/** + * Removes leading characters from a string without using regex. + * + * This function iteratively checks each character from the beginning of the string + * and removes any consecutive characters that match the specified characters to remove. + * It uses a direct character-by-character approach instead of regex to avoid potential + * backtracking issues and ensure consistent O(n) performance. + * + * @param {string} input - The string to process + * @param {string[]} charactersToRemove - Array of characters to remove from the beginning + * @returns {string} The input string with all leading specified characters removed + * + * @example + * // Returns "example" + * removeLeadingCharacters("...example", ["."]) + * + * @example + * // Returns "module-name" + * removeLeadingCharacters("._-module-name", [".", "-", "_"]) + */ +export function removeLeadingCharacters(input: string, charactersToRemove: string[]): string { + let startIndex = 0; + while (startIndex < input.length && charactersToRemove.includes(input[startIndex])) { + startIndex++; + } + + return input.slice(startIndex); +}