Skip to content

Commit a43d686

Browse files
committed
feat: enhance tag generation configuration with detailed descriptions and examples for directory separator and version prefix options
1 parent aac46ea commit a43d686

File tree

13 files changed

+405
-125
lines changed

13 files changed

+405
-125
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ jobs:
5454
module-change-exclude-patterns: .gitignore,*.md,*.tftest.hcl,tests/**,examples/**
5555
module-asset-exclude-patterns: .gitignore,*.md,*.tftest.hcl,tests/**
5656
use-ssh-source-format: true
57+
tag-directory-separator: "-"
58+
use-version-prefix: false
5759

5860
- name: Test Action Outputs
5961
id: test-outputs

.github/workflows/test.yml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ on:
1010
types: [opened, synchronize, reopened]
1111

1212
jobs:
13-
tests:
13+
typescript-tests:
1414
runs-on: ubuntu-latest
15-
name: Test
15+
name: TypeScript Tests
1616
permissions:
1717
contents: read
1818
steps:
@@ -35,6 +35,26 @@ jobs:
3535
env:
3636
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO_CI_TESTING }}
3737

38+
sonarqube-scan:
39+
runs-on: ubuntu-latest
40+
name: SonarQube Analysis
41+
permissions:
42+
contents: read
43+
steps:
44+
- uses: actions/checkout@v4
45+
with:
46+
# Disabling shallow clone is recommended for improving relevancy of reporting
47+
fetch-depth: 0
48+
49+
- name: Setup Node.js
50+
uses: actions/setup-node@v4
51+
with:
52+
node-version-file: .node-version
53+
cache: npm
54+
55+
- name: Install Dependencies
56+
run: npm ci --no-fund
57+
3858
- name: SonarQube Scan
3959
uses: SonarSource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf # v5
4060
env:

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ configuring the following optional input parameters as needed.
199199
| `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.<br><sub>[Read more here](#understanding-the-filtering-options)</sub> | `.gitignore,*.md,*.tftest.hcl,tests/**` |
200200
| `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/**` |
201201
| `use-ssh-source-format` | If enabled, all links to source code in generated Wiki documentation will use SSH standard format (e.g., `git::ssh://[email protected]/owner/repo.git`) instead of HTTPS format (`git::https://github.com/owner/repo.git`) | `false` |
202+
| `tag-directory-separator` | Character used to separate directory path components in Git tags. Supports `/`, `-`, `_`, or `.` | `/` |
203+
| `use-version-prefix` | Whether to include the 'v' prefix on version tags (e.g., v1.2.3 vs 1.2.3) | `true` |
202204

203205
### Understanding the filtering options
204206

__tests__/helpers/octokit.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { OctokitRestApi } from '@/types';
2-
import { trimSlashes } from '@/utils/string';
2+
import { removeTrailingCharacters } from '@/utils/string';
33
import { paginateRest } from '@octokit/plugin-paginate-rest';
44
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods';
55
import type { RestEndpointMethodTypes } from '@octokit/plugin-rest-endpoint-methods';
@@ -384,5 +384,6 @@ function getLinkHeader(slug: string, page: number, perPage: number, totalCount:
384384

385385
const nextPage = page + 1;
386386
const lastPage = totalPages;
387-
return `<https://api.github.com/repos/techpivot/terraform-module-releaser/${trimSlashes(slug)}?per_page=${perPage}&page=${nextPage}>; rel="next", <https://api.github.com/repos/techpivot/terraform-module-releaser/${trimSlashes(slug)}?per_page=${perPage}&page=${lastPage}>; rel="last"`;
387+
const slugTrimmed = removeTrailingCharacters(slug, ['/']);
388+
return `<https://api.github.com/repos/techpivot/terraform-module-releaser/${slugTrimmed}?per_page=${perPage}&page=${nextPage}>; rel="next", <https://api.github.com/repos/techpivot/terraform-module-releaser/${slugTrimmed}?per_page=${perPage}&page=${lastPage}>; rel="last"`;
388389
}

__tests__/terraform-module.test.ts

Lines changed: 206 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('TerraformModule', () => {
5858
mkdirSync(specialDir, { recursive: true });
5959

6060
const module = new TerraformModule(specialDir);
61-
expect(module.name).toBe('complex_module-name-with/chars');
61+
expect(module.name).toBe('complex_module-name.with/chars');
6262
});
6363

6464
it('should handle nested directory paths', () => {
@@ -949,44 +949,222 @@ describe('TerraformModule', () => {
949949

950950
describe('static utilities', () => {
951951
describe('getTerraformModuleNameFromRelativePath()', () => {
952-
it('should generate valid module names from paths', () => {
953-
expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe(
954-
'tf-modules/simple-module',
955-
);
952+
beforeEach(() => {
953+
// Reset to default config for each test
954+
config.set({
955+
tagDirectorySeparator: '/',
956+
majorKeywords: ['BREAKING CHANGE', 'major change'],
957+
minorKeywords: ['feat:', 'feature:'],
958+
defaultFirstTag: 'v0.1.0',
959+
moduleChangeExcludePatterns: [],
960+
modulePathIgnore: [],
961+
useVersionPrefix: true,
962+
});
963+
});
956964

957-
expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex_module-name.with/chars')).toBe(
958-
'complex_module-name-with/chars',
959-
);
965+
describe('with different tag directory separators', () => {
966+
it('should use forward slash separator by default', () => {
967+
config.set({ tagDirectorySeparator: '/' });
968+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe(
969+
'tf-modules/simple-module',
970+
);
971+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex\\module\\windows\\path')).toBe(
972+
'complex/module/windows/path',
973+
);
974+
});
960975

961-
expect(TerraformModule.getTerraformModuleNameFromRelativePath('/leading/slash/')).toBe('leading/slash');
976+
it('should use hyphen separator when configured', () => {
977+
config.set({ tagDirectorySeparator: '-' });
978+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe(
979+
'tf-modules-simple-module',
980+
);
981+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex\\module\\windows\\path')).toBe(
982+
'complex-module-windows-path',
983+
);
984+
});
962985

963-
expect(TerraformModule.getTerraformModuleNameFromRelativePath('module...with...dots')).toBe('module-with-dots');
964-
});
986+
it('should use underscore separator when configured', () => {
987+
config.set({ tagDirectorySeparator: '_' });
988+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe(
989+
'tf-modules_simple-module',
990+
);
991+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex\\module\\windows\\path')).toBe(
992+
'complex_module_windows_path',
993+
);
994+
});
965995

966-
it('should handle leading and trailing slashes', () => {
967-
expect(TerraformModule.getTerraformModuleNameFromRelativePath('/test-module/')).toBe('test-module');
996+
it('should use dot separator when configured', () => {
997+
config.set({ tagDirectorySeparator: '.' });
998+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules/simple-module')).toBe(
999+
'tf-modules.simple-module',
1000+
);
1001+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('complex\\module\\windows\\path')).toBe(
1002+
'complex.module.windows.path',
1003+
);
1004+
});
9681005
});
9691006

970-
it('should handle multiple consecutive slashes', () => {
971-
expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules//vpc//endpoint')).toBe(
972-
'tf-modules/vpc/endpoint',
973-
);
974-
});
1007+
describe('character normalization and cleanup', () => {
1008+
beforeEach(() => {
1009+
config.set({ tagDirectorySeparator: '/' });
1010+
});
9751011

976-
it('should handle whitespace', () => {
977-
expect(TerraformModule.getTerraformModuleNameFromRelativePath(' test module ')).toBe('test-module');
978-
});
1012+
it('should normalize Windows backslashes to configured separator', () => {
1013+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('windows\\path\\module')).toBe(
1014+
'windows/path/module',
1015+
);
1016+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('mixed\\and/path\\separators')).toBe(
1017+
'mixed/and/path/separators',
1018+
);
1019+
});
9791020

980-
it('should convert to lowercase', () => {
981-
expect(TerraformModule.getTerraformModuleNameFromRelativePath('Test-Module')).toBe('test-module');
982-
});
1021+
it('should convert to lowercase', () => {
1022+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('Test-Module')).toBe('test-module');
1023+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('UPPERCASE/MODULE')).toBe('uppercase/module');
1024+
});
1025+
1026+
it('should replace invalid characters with hyphens', () => {
1027+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('test@module!#$')).toBe('test-module');
1028+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('module%with&special*chars')).toBe(
1029+
'module-with-special-chars',
1030+
);
1031+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('test module with spaces')).toBe(
1032+
'test-module-with-spaces',
1033+
);
1034+
});
1035+
1036+
it('should normalize consecutive special characters', () => {
1037+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('module...with...dots')).toBe(
1038+
'module.with.dots',
1039+
);
1040+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('tf-modules//vpc//endpoint')).toBe(
1041+
'tf-modules/vpc/endpoint',
1042+
);
1043+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('module---with---hyphens')).toBe(
1044+
'module-with-hyphens',
1045+
);
1046+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('module___with___underscores')).toBe(
1047+
'module_with_underscores',
1048+
);
1049+
});
1050+
1051+
it('should remove leading and trailing special characters', () => {
1052+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('/leading/slash/')).toBe('leading/slash');
1053+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('...leading.dots')).toBe('leading.dots');
1054+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('trailing.dots...')).toBe('trailing.dots');
1055+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('---leading-hyphens')).toBe('leading-hyphens');
1056+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('trailing-hyphens---')).toBe(
1057+
'trailing-hyphens',
1058+
);
1059+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('___leading_underscores')).toBe(
1060+
'leading_underscores',
1061+
);
1062+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('trailing_underscores___')).toBe(
1063+
'trailing_underscores',
1064+
);
1065+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('/.-_mixed_leading')).toBe('mixed_leading');
1066+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('mixed_trailing_.-/')).toBe('mixed_trailing');
1067+
});
9831068

984-
it('should clean up invalid characters', () => {
985-
expect(TerraformModule.getTerraformModuleNameFromRelativePath('test@module!#$')).toBe('test-module');
1069+
it('should handle edge cases', () => {
1070+
expect(TerraformModule.getTerraformModuleNameFromRelativePath(' whitespace ')).toBe('whitespace');
1071+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('/test-module/')).toBe('test-module');
1072+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('single')).toBe('single');
1073+
expect(TerraformModule.getTerraformModuleNameFromRelativePath('a')).toBe('a');
1074+
});
9861075
});
9871076

988-
it('should remove trailing special characters', () => {
989-
expect(TerraformModule.getTerraformModuleNameFromRelativePath('test-module-.')).toBe('test-module');
1077+
describe('comprehensive scenarios with different separators', () => {
1078+
const testScenarios = [
1079+
{
1080+
separator: '/',
1081+
input: 'tf-modules/aws/vpc-endpoint',
1082+
expected: 'tf-modules/aws/vpc-endpoint',
1083+
},
1084+
{
1085+
separator: '-',
1086+
input: 'tf-modules/aws/vpc-endpoint',
1087+
expected: 'tf-modules-aws-vpc-endpoint',
1088+
},
1089+
{
1090+
separator: '_',
1091+
input: 'tf-modules/aws/vpc-endpoint',
1092+
expected: 'tf-modules_aws_vpc-endpoint',
1093+
},
1094+
{
1095+
separator: '.',
1096+
input: 'tf-modules/aws/vpc-endpoint',
1097+
expected: 'tf-modules.aws.vpc-endpoint',
1098+
},
1099+
];
1100+
1101+
for (const { separator, input, expected } of testScenarios) {
1102+
it(`should handle complex paths with ${separator} separator`, () => {
1103+
config.set({ tagDirectorySeparator: separator });
1104+
expect(TerraformModule.getTerraformModuleNameFromRelativePath(input)).toBe(expected);
1105+
});
1106+
}
1107+
1108+
const complexTestScenarios = [
1109+
{
1110+
separator: '/',
1111+
input: '//tf-modules//aws..vpc--endpoint__',
1112+
expected: 'tf-modules/aws.vpc-endpoint',
1113+
},
1114+
{
1115+
separator: '-',
1116+
input: '//tf-modules//aws..vpc--endpoint__',
1117+
expected: 'tf-modules-aws.vpc-endpoint',
1118+
},
1119+
{
1120+
separator: '_',
1121+
input: '//tf-modules//aws..vpc--endpoint__',
1122+
expected: 'tf-modules_aws.vpc-endpoint',
1123+
},
1124+
{
1125+
separator: '.',
1126+
input: '//tf-modules//aws..vpc--endpoint__',
1127+
expected: 'tf-modules.aws.vpc-endpoint',
1128+
},
1129+
];
1130+
1131+
for (const { separator, input, expected } of complexTestScenarios) {
1132+
it(`should handle complex normalization with ${separator} separator`, () => {
1133+
config.set({ tagDirectorySeparator: separator });
1134+
expect(TerraformModule.getTerraformModuleNameFromRelativePath(input)).toBe(expected);
1135+
});
1136+
}
1137+
});
1138+
1139+
describe('real-world terraform module scenarios', () => {
1140+
it('should handle typical terraform module paths', () => {
1141+
config.set({ tagDirectorySeparator: '/' });
1142+
1143+
const testCases = [
1144+
{ input: 'modules/networking/vpc', expected: 'modules/networking/vpc' },
1145+
{ input: 'modules/compute/ec2-instance', expected: 'modules/compute/ec2-instance' },
1146+
{ input: 'modules/storage/s3-bucket', expected: 'modules/storage/s3-bucket' },
1147+
{ input: 'terraform/aws/rds_cluster', expected: 'terraform/aws/rds_cluster' },
1148+
{ input: 'tf-modules/azure/storage.account', expected: 'tf-modules/azure/storage.account' },
1149+
];
1150+
1151+
for (const { input, expected } of testCases) {
1152+
expect(TerraformModule.getTerraformModuleNameFromRelativePath(input)).toBe(expected);
1153+
}
1154+
});
1155+
1156+
it('should handle module paths with various separators configured', () => {
1157+
const separatorTests = [
1158+
{ separator: '-', input: 'modules/aws/vpc', expected: 'modules-aws-vpc' },
1159+
{ separator: '_', input: 'modules/aws/vpc', expected: 'modules_aws_vpc' },
1160+
{ separator: '.', input: 'modules/aws/vpc', expected: 'modules.aws.vpc' },
1161+
];
1162+
1163+
for (const { separator, input, expected } of separatorTests) {
1164+
config.set({ tagDirectorySeparator: separator });
1165+
expect(TerraformModule.getTerraformModuleNameFromRelativePath(input)).toBe(expected);
1166+
}
1167+
});
9901168
});
9911169
});
9921170

0 commit comments

Comments
 (0)