diff --git a/README.md b/README.md index 191101eb..16227690 100644 --- a/README.md +++ b/README.md @@ -184,21 +184,22 @@ resources. While the out-of-the-box defaults are suitable for most use cases, you can further customize the action's behavior by configuring the following optional input parameters as needed. -| Input | Description | Default | -| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -| `major-keywords` | Keywords in commit messages that indicate a major release | `major change,breaking change` | -| `minor-keywords` | Keywords in commit messages that indicate a minor release | `feat,feature` | -| `patch-keywords` | Keywords in commit messages that indicate a patch release | `fix,chore,docs` | -| `default-first-tag` | Specifies the default tag version | `v1.0.0` | -| `terraform-docs-version` | Specifies the terraform-docs version used to generate documentation for the wiki | `v0.19.0` | -| `delete-legacy-tags` | Specifies a boolean that determines whether tags and releases from Terraform modules that have been deleted should be automatically removed | `true` | -| `disable-wiki` | Whether to disable wiki generation for Terraform modules | `false` | -| `wiki-sidebar-changelog-max` | An integer that specifies how many changelog entries are displayed in the sidebar per module | `5` | -| `disable-branding` | Controls whether a small branding link to the action's repository is added to PR comments. Recommended to leave enabled to support OSS. | `false` | -| `module-path-ignore` | Comma-separated list of module paths to completely ignore. Modules matching any pattern here are excluded from all versioning, releases, and documentation.
[Read more here](#understanding-the-filtering-options) | `` (empty string) | -| `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` | +| Input | Description | Default | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `major-keywords` | Keywords in commit messages that indicate a major release | `major change,breaking change` | +| `minor-keywords` | Keywords in commit messages that indicate a minor release | `feat,feature` | +| `patch-keywords` | Keywords in commit messages that indicate a patch release | `fix,chore,docs` | +| `default-first-tag` | Specifies the default tag version | `v1.0.0` | +| `terraform-docs-version` | Specifies the terraform-docs version used to generate documentation for the wiki | `v0.19.0` | +| `delete-legacy-tags` | Specifies a boolean that determines whether tags and releases from Terraform modules that have been deleted should be automatically removed | `true` | +| `disable-wiki` | Whether to disable wiki generation for Terraform modules | `false` | +| `wiki-sidebar-changelog-max` | An integer that specifies how many changelog entries are displayed in the sidebar per module | `5` | +| `disable-branding` | Controls whether a small branding link to the action's repository is added to PR comments. Recommended to leave enabled to support OSS. | `false` | +| `module-path-ignore` | Comma-separated list of module paths to completely ignore. Modules matching any pattern here are excluded from all versioning, releases, and documentation.
[Read more here](#understanding-the-filtering-options) | `` (empty string) | +| `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` | +| `wiki-usage-template` | A raw, multi-line string to override the default 'Usage' section in the generated wiki. Allows using variables like {{module_name}}, {{latest_tag}}, {{latest_tag_version_number}} and more.
[Read more here](#configuring-the-usage-template) | [See action.yml](https://github.com/polleuretan/terraform-module-releaser/blob/main/action.yml#L108) | ### Understanding the filtering options @@ -281,9 +282,22 @@ similar to those used in `.gitignore` files. For more details on the pattern mat [source code](https://github.com/techpivot/terraform-module-releaser/blob/main/src/utils/file.ts) or visit the [minimatch documentation](https://github.com/isaacs/minimatch). +### Configuring the Usage Template + +The `wiki-usage-template` input allows you to customize the "Usage" section of the generated wiki page for each module. +You can use the following dynamic variables in your template: + +| Variable | Description | Example | +| ------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| `{{module_name}}` | The name of the module. | `aws/s3-bucket` | +| `{{latest_tag}}` | The latest Git tag for the module. | `aws/s3-bucket/v1.2.3` | +| `{{latest_tag_version_number}}` | The version number of the latest tag. | `1.2.3` | +| `{{module_source}}` | The Git source URL for the module, respecting the `use-ssh-source-format` input. | `git::https://github.com/owner/repo.git` | +| `{{module_name_terraform}}` | A Terraform-safe version of the module name (e.g., special characters replaced with underscores). | `aws_s3_bucket` | + ### Example Usage with Inputs -```yml +````yml name: Terraform Module Releaser on: pull_request: @@ -317,7 +331,24 @@ jobs: module-change-exclude-patterns: .gitignore,*.md,docs/**,examples/**,*.tftest.hcl,tests/** module-asset-exclude-patterns: .gitignore,*.md,*.tftest.hcl,tests/** use-ssh-source-format: false -``` + wiki-usage-template: | + # My Custom Usage Instructions + + This is a custom usage block. + + You can add any markdown you want here. + + And use variables like {{module_name}}, {{latest_tag}}, {{latest_tag_version_number}}, + {{module_source}} and {{module_name_terraform}}. + + ```hcl + module "{{module_name_terraform}}" { + source = "{{module_source}}?ref={{latest_tag}}" + version = "{{latest_tag_version_number}}" + # ... + } + ``` +```` ## Outputs diff --git a/__mocks__/config.ts b/__mocks__/config.ts index 26a0a7e9..5c43f6b4 100644 --- a/__mocks__/config.ts +++ b/__mocks__/config.ts @@ -26,6 +26,17 @@ const defaultConfig: Config = { moduleAssetExcludePatterns: ['tests/**', 'examples/**'], githubToken: 'ghp_test_token_2c6912E7710c838347Ae178B4', useSSHSourceFormat: false, + wikiUsageTemplate: ` + To use this module in your Terraform, refer to the below module example: + + \`\`\`hcl + module "{{module_name_terraform}}" { + source = "git::{{module_source}}?ref={{latest_tag}}" + + # See inputs below for additional required parameters + } + \`\`\` +` }; /** diff --git a/__tests__/templating.test.ts b/__tests__/templating.test.ts new file mode 100644 index 00000000..82c62d4e --- /dev/null +++ b/__tests__/templating.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { render } from '../src/templating'; + +describe('templating', () => { + it('should replace a single placeholder', () => { + const template = 'Hello, {{name}}!'; + const variables = { name: 'World' }; + const result = render(template, variables); + expect(result).toBe('Hello, World!'); + }); + + it('should replace multiple placeholders', () => { + const template = '{{greeting}}, {{name}}!'; + const variables = { greeting: 'Hi', name: 'There' }; + const result = render(template, variables); + expect(result).toBe('Hi, There!'); + }); + + it('should handle templates with no placeholders', () => { + const template = 'Just a plain string.'; + const variables = { name: 'World' }; + const result = render(template, variables); + expect(result).toBe('Just a plain string.'); + }); + + it('should handle empty string values', () => { + const template = 'A{{key}}B'; + const variables = { key: '' }; + const result = render(template, variables); + expect(result).toBe('AB'); + }); + + it('should leave unmapped placeholders untouched', () => { + const template = 'Hello, {{name}} and {{unmapped}}!'; + const variables = { name: 'World' }; + const result = render(template, variables); + expect(result).toBe('Hello, World and {{unmapped}}!'); + }); +}); diff --git a/__tests__/wiki.test.ts b/__tests__/wiki.test.ts index 5c2563da..2b003ba4 100644 --- a/__tests__/wiki.test.ts +++ b/__tests__/wiki.test.ts @@ -257,6 +257,83 @@ describe('wiki', async () => { 'https://github.com/techpivot/terraform-module-releaser/wiki/aws∕vpc', ); }); + + it('should use the default usage block when custom template is not provided', async () => { + const files = await generateWikiFiles(terraformModules); + for (const file of files) { + if ( + file.endsWith('.md') && + basename(file) !== 'Home.md' && + basename(file) !== '_Sidebar.md' && + basename(file) !== '_Footer.md' + ) { + const content = readFileSync(file, 'utf8'); + expect(content).toContain('To use this module in your Terraform, refer to the below module example:'); + } + } + }); + + it('should use the custom usage template when provided', async () => { + const customUsage = 'This is a custom usage template: {{module_name}}'; + config.set({ wikiUsageTemplate: customUsage }); + const terraformModule = terraformModules[0]; + const files = await generateWikiFiles([terraformModule]); + for (const file of files) { + if ( + file.endsWith('.md') && + basename(file) !== 'Home.md' && + basename(file) !== '_Sidebar.md' && + basename(file) !== '_Footer.md' + ) { + const content = readFileSync(file, 'utf8'); + const moduleName = basename(file, '.md'); + expect(content).toContain(`# Usage\n\nThis is a custom usage template: ${moduleName}`); + } + } + }); + + it('should handle missing variables in the custom usage template', async () => { + const customUsage = 'Module: {{module_name}}, Missing: {{missing_variable}}'; + config.set({ wikiUsageTemplate: customUsage }); + const terraformModule = terraformModules[0]; + const files = await generateWikiFiles([terraformModule]); + for (const file of files) { + if ( + file.endsWith('.md') && + basename(file) !== 'Home.md' && + basename(file) !== '_Sidebar.md' && + basename(file) !== '_Footer.md' + ) { + const content = readFileSync(file, 'utf8'); + const moduleName = basename(file, '.md'); + expect(content).toContain(`# Usage\n\nModule: ${terraformModule.name}, Missing: {{missing_variable}}`); + } + } + }); + + it('should handle all variables in the custom usage template', async () => { + const customUsage = + 'Name: {{module_name}}, Tag: {{latest_tag}}, Version: {{latest_tag_version_number}}, Source: {{module_source}}, TFName: {{module_name_terraform}}'; + config.set({ wikiUsageTemplate: customUsage }); + const files = await generateWikiFiles(terraformModules); + for (const file of files) { + if ( + file.endsWith('.md') && + basename(file) !== 'Home.md' && + basename(file) !== '_Sidebar.md' && + basename(file) !== '_Footer.md' + ) { + const content = readFileSync(file, 'utf8'); + const moduleName = basename(file, '.md'); + // vpc-endpoint is the only one with a tag in the test setup + if (moduleName === 'vpc‒endpoint') { + expect(content).toContain( + 'Name: vpc-endpoint, Tag: vpc-endpoint/v1.0.0, Version: 1.0.0, Source: https://github.com/techpivot/terraform-module-releaser.git, TFName: vpc_endpoint', + ); + } + } + } + }); }); describe('commitAndPushWikiChanges()', () => { diff --git a/action.yml b/action.yml index 124dd781..958186ba 100644 --- a/action.yml +++ b/action.yml @@ -102,6 +102,19 @@ inputs: If enabled, all links to source code in generated Wiki documentation will use SSH format instead of HTTPS format. required: true default: "false" + wiki-usage-template: + description: A raw, multi-line string to override the default 'Usage' section in the generated wiki. If not provided, a default usage block will be generated. + required: false + default: | + To use this module in your Terraform, refer to the below module example: + + ```hcl + module "{{module_name_terraform}}" { + source = "git::{{module_source}}?ref={{latest_tag}}" + + # See inputs below for additional required parameters + } + ``` github_token: description: > Required for retrieving pull request metadata, tags, releases, updating PR comments, wiki, and creating diff --git a/src/config.ts b/src/config.ts index 45390210..4a620f18 100644 --- a/src/config.ts +++ b/src/config.ts @@ -74,6 +74,7 @@ function initializeConfig(): Config { moduleChangeExcludePatterns: getArrayInput('module-change-exclude-patterns', false), moduleAssetExcludePatterns: getArrayInput('module-asset-exclude-patterns', false), useSSHSourceFormat: getBooleanInput('use-ssh-source-format', { required: true }), + wikiUsageTemplate: getInput('wiki-usage-template', { required: false }), }; // Validate that *.tf is not in excludePatterns diff --git a/src/templating.ts b/src/templating.ts new file mode 100644 index 00000000..da3eec67 --- /dev/null +++ b/src/templating.ts @@ -0,0 +1,12 @@ +/** + * Renders a template string by replacing placeholders with provided values. + * + * @param template The template string containing placeholders in the format `{{key}}`. + * @param variables An object where keys correspond to placeholder names and values are their replacements. + * @returns The rendered string with placeholders replaced. + */ +export const render = (template: string, variables: Record): string => { + return template.replace(/\{\{(\w+)\}\}/g, (placeholder, key) => { + return key in variables ? variables[key] : placeholder; + }); +}; diff --git a/src/terraform-module.ts b/src/terraform-module.ts index 4d2894e8..7da8f36f 100644 --- a/src/terraform-module.ts +++ b/src/terraform-module.ts @@ -220,6 +220,19 @@ export class TerraformModule { return this.tags[0].replace(`${this.name}/`, ''); } + /** + * Returns the version part of the latest tag for this module, without any "v" prefix. + * + * @returns {string | null} The version string without any prefixes (e.g., '1.2.3'), or null if no tags exist. + */ + public getLatestTagVersionNumber(): string | null { + const version = this.getLatestTagVersion(); + if (!version) { + return null; + } + return version.replace(/^v/, ''); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Releases ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/types/config.types.ts b/src/types/config.types.ts index dea8abf6..32543a88 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -102,4 +102,10 @@ export interface Config { * Paths are relative to the workspace directory. */ modulePathIgnore: string[]; + + /** + * A raw, multi-line string to override the default 'Usage' section in the generated wiki. + * If not provided, a default usage block will be generated. + */ + wikiUsageTemplate?: string; } diff --git a/src/wiki.ts b/src/wiki.ts index 89b95c9a..49ba5f4c 100644 --- a/src/wiki.ts +++ b/src/wiki.ts @@ -7,6 +7,7 @@ import { join, resolve } from 'node:path'; import { getTerraformModuleFullReleaseChangelog } from '@/changelog'; import { config } from '@/config'; import { context } from '@/context'; +import { render } from '@/templating'; import { generateTerraformDocs } from '@/terraform-docs'; import type { TerraformModule } from '@/terraform-module'; import type { ExecSyncError, WikiStatusResult } from '@/types'; @@ -304,15 +305,18 @@ async function generateWikiTerraformModule(terraformModule: TerraformModule): Pr const changelog = getTerraformModuleFullReleaseChangelog(terraformModule); const tfDocs = await generateTerraformDocs(terraformModule); const moduleSource = getModuleSource(context.repoUrl, config.useSSHSourceFormat); + + const usage = render(config.wikiUsageTemplate, { + module_name: terraformModule.name, + latest_tag: terraformModule.getLatestTag(), + latest_tag_version_number: terraformModule.getLatestTagVersionNumber(), + module_source: moduleSource, + module_name_terraform: terraformModule.name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase(), + }); + const content = [ '# Usage\n', - 'To use this module in your Terraform, refer to the below module example:\n', - '```hcl', - `module "${terraformModule.name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}" {`, - ` source = "git::${moduleSource}?ref=${terraformModule.getLatestTag()}"`, - '\n # See inputs below for additional required parameters', - '}', - '```', + usage, '\n# Attributes\n', '', tfDocs,