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": {}
}
}
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/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 }}
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
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 63549bf..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,7 +35,27 @@ 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@v5
+ uses: SonarSource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf # v5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
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/__mocks__/config.ts b/__mocks__/config.ts
index 26a0a7e..2909b5a 100644
--- a/__mocks__/config.ts
+++ b/__mocks__/config.ts
@@ -1,4 +1,7 @@
+import { setupTestInputs } from '@/tests/helpers/inputs';
import type { Config } from '@/types';
+import type { ActionInputMetadata } from '@/types';
+import { ACTION_INPUTS, createConfigFromInputs } from '@/utils/metadata';
/**
* 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/__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__/config.test.ts b/__tests__/config.test.ts
index be14bc2..1cc95cf 100644
--- a/__tests__/config.test.ts
+++ b/__tests__/config.test.ts
@@ -2,14 +2,14 @@ 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 { 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';
@@ -22,38 +22,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 +64,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 +75,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 +102,134 @@ 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: ${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'",
+ ),
+ );
+
+ 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 +237,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 +299,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([]);
- });
- });
});
diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts
index f837d46..b84b20c 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.`,
@@ -182,9 +184,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,9 +195,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();
// Check that the context was created with default API URL
diff --git a/__tests__/helpers/action-defaults.ts b/__tests__/helpers/action-defaults.ts
new file mode 100644
index 0000000..ff549e9
--- /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/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..0801dbd 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/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);
}
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 13818d2..7be18aa 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,
});
});
@@ -57,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', () => {
@@ -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()', () => {
@@ -885,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 clean up invalid characters', () => {
- expect(TerraformModule.getTerraformModuleNameFromRelativePath('test@module!#$')).toBe('test-module');
+ 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 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);
+ }
+ });
});
});
@@ -954,17 +1196,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 +1420,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 +1848,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/__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 124dd78..c00e812 100644
--- a/action.yml
+++ b/action.yml
@@ -109,6 +109,27 @@ inputs:
specific requirements.
required: true
default: ${{ github.token }}
+ tag-directory-separator:
+ 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 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"
outputs:
changed-module-names:
diff --git a/package-lock.json b/package-lock.json
index 290b2ee..af3108a 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",
@@ -856,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.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": {
@@ -939,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"
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz",
+ "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==",
"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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -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.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"
],
@@ -1304,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",
@@ -1418,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",
@@ -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",
@@ -1505,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"
},
@@ -1618,6 +1668,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 +1686,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.32",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz",
+ "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1663,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": {
@@ -1687,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": {
@@ -1697,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"
},
@@ -1714,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"
},
@@ -1741,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": {
@@ -1754,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"
},
@@ -1769,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"
},
@@ -1784,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": {
@@ -1797,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": {
@@ -1872,14 +1929,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 +1982,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 +2041,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 +2781,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 +3369,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 +3409,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": {
@@ -3434,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"
},
@@ -3890,12 +3945,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"
@@ -4063,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": {
@@ -4366,9 +4421,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.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
@@ -4483,26 +4538,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,13 +4756,13 @@
}
},
"node_modules/rollup": {
- "version": "4.42.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz",
- "integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==",
+ "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"
@@ -4737,36 +4772,29 @@
"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.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",
@@ -5389,32 +5417,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",
@@ -5506,9 +5534,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": {
@@ -5567,9 +5595,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.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": {
@@ -5941,9 +5969,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": {
@@ -5964,20 +5992,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",
@@ -5988,10 +6016,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": {
@@ -6007,8 +6035,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": "*"
},
@@ -6193,9 +6221,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.67",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz",
+ "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==",
"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",
diff --git a/src/config.ts b/src/config.ts
index 4539021..e116d9f 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 { 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
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,35 @@ 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
+ if (configInstance.tagDirectorySeparator.length !== 1) {
+ throw new TypeError('Tag directory separator must be exactly one character');
+ }
+ if (!VALID_TAG_DIRECTORY_SEPARATORS.includes(configInstance.tagDirectorySeparator)) {
+ throw new TypeError(
+ `Tag directory separator must be one of: ${VALID_TAG_DIRECTORY_SEPARATORS.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}'`,
+ );
+ }
+
+ // 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(', ')}`);
@@ -103,6 +91,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 {
diff --git a/src/terraform-module.ts b/src/terraform-module.ts
index 4d2894e..047b0dd 100644
--- a/src/terraform-module.ts
+++ b/src/terraform-module.ts
@@ -2,8 +2,14 @@ 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 { removeTrailingCharacters } from '@/utils/string';
+import {
+ MODULE_TAG_REGEX,
+ RELEASE_REASON,
+ RELEASE_TYPE,
+ VALID_TAG_DIRECTORY_SEPARATORS,
+ VERSION_TAG_REGEX,
+} from '@/utils/constants';
+import { removeLeadingCharacters, 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 = MODULE_TAG_REGEX.exec(latestTag);
+
+ 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('.')}`;
}
/**
@@ -463,41 +471,49 @@ export class TerraformModule {
return null;
}
- return `${this.name}/${releaseTagVersion}`;
+ return `${this.name}${config.tagDirectorySeparator}${releaseTagVersion}`;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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}`;
}
/**
@@ -601,50 +617,68 @@ 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;
}
/**
* 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 +717,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 +728,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 +749,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 +768,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 dea8abf..8961f8c 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;
@@ -67,6 +68,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 +105,38 @@ 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 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: -, _, /, .
+ *
+ * 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.
*/
- modulePathIgnore: string[];
+ tagDirectorySeparator: string;
+
+ /**
+ * Whether to include the "v" prefix in version tags.
+ *
+ * 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/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/index.ts b/src/types/index.ts
index 1cd04a7..dd55ef1 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,3 +1,6 @@
+// Action metadata types
+export * from './metadata.types';
+
// Common types
export * from './common.types';
diff --git a/src/types/metadata.types.ts b/src/types/metadata.types.ts
new file mode 100644
index 0000000..4ebfff8
--- /dev/null
+++ b/src/types/metadata.types.ts
@@ -0,0 +1,41 @@
+import type { Config } from '@/types/config.types';
+
+/**
+ * Metadata definition for GitHub Action inputs that enables dynamic configuration mapping.
+ *
+ * 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.
+ * Must be a valid key from the Config interface.
+ */
+ configKey: keyof Config;
+
+ /**
+ * Whether this input is required by the GitHub Action.
+ * When true, the action will fail if the input is not provided.
+ */
+ required: boolean;
+
+ /**
+ * 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/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.
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 922f45d..7ed6719 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,32 @@
* 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"]
+ *
+ * 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*))$/;
/**
* Release type constants for semantic versioning
diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts
new file mode 100644
index 0000000..0856b9f
--- /dev/null
+++ b/src/utils/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;
+}
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);
+}