Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ configuring the following optional input parameters as needed.
| `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` |
| `export-changelog-files` | Whether to export CHANGELOG.md files for each Terraform module. When enabled, generates and commits a CHANGELOG.md file in each module's directory with the full release history.<br><sub>[Read more here](#exporting-changelog-files)</sub> | `false` |
| `wiki-sidebar-changelog-max` | An integer that specifies how many changelog entries are displayed in the sidebar per module | `5` |
| `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.<br><sub>[Read more here](#configuring-the-wiki-usage-template)</sub> | [See action.yml](https://github.com/techpivot/terraform-module-releaser/blob/main/action.yml#L54-L65) |
| `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` |
Expand Down Expand Up @@ -291,6 +292,40 @@ You can use the following dynamic variables in your template:
| `{{module_source}}` | The Git source URL for the module with `git::` prefix, respecting the `use-ssh-source-format` input. | `git::ssh://github.com/techpivot/terraform-module-releaser.git` |
| `{{module_name_terraform}}` | A Terraform-safe version of the module name (e.g., special characters replaced with underscores). | `aws_s3_bucket` |

### Exporting CHANGELOG Files

The `export-changelog-files` option allows you to generate and maintain `CHANGELOG.md` files for each Terraform module
directly in the repository. When enabled, the action will:

- Generate a `CHANGELOG.md` file in each module's directory containing the full release history
- Include both the new release entry and all historical releases
- Automatically commit and push the changes to the repository after creating releases

This feature complements the existing wiki functionality and provides an alternative format for tracking module changes
that lives within the repository itself. This is particularly useful for:

- Organizations that prefer in-repository documentation over wikis
- Scenarios where offline access to changelogs is required
- CI/CD pipelines that need to programmatically access changelog information
- Projects that want to maintain changelogs as part of their version control history

**Example usage:**

```yaml
- name: Terraform Module Releaser
uses: techpivot/terraform-module-releaser@v1
with:
export-changelog-files: true
disable-wiki: false # Both can be enabled simultaneously
```

**Important notes:**

- The changelog files are generated during the merge event (when PR is merged)
- Changes are automatically committed and pushed using the GitHub Actions bot credentials
- Each `CHANGELOG.md` file contains the module name, description, and all release entries in reverse chronological order
- The feature works independently of the wiki generation - you can enable one, both, or neither

### Example Usage with Inputs

````yml
Expand Down Expand Up @@ -322,6 +357,7 @@ jobs:
terraform-docs-version: v0.20.0
delete-legacy-tags: true
disable-wiki: false
export-changelog-files: false
wiki-sidebar-changelog-max: 10
module-path-ignore: path/to/ignore1,path/to/ignore2
module-change-exclude-patterns: .gitignore,*.md,docs/**,examples/**,*.tftest.hcl,tests/**
Expand Down
127 changes: 127 additions & 0 deletions __tests__/changelog.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {
createTerraformModuleChangelog,
generateChangelogFiles,
getPullRequestChangelog,
getTerraformModuleFullReleaseChangelog,
} from '@/changelog';
import { context } from '@/mocks/context';
import type { TerraformModule } from '@/terraform-module';
import { createMockTerraformModule } from '@/tests/helpers/terraform-module';
import { existsSync, promises as fsp } from 'node:fs';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

describe('changelog', () => {
Expand Down Expand Up @@ -215,4 +218,128 @@ describe('changelog', () => {
expect(getTerraformModuleFullReleaseChangelog(terraformModule)).toBe('Single release content');
});
});

describe('generateChangelogFiles()', () => {
const tmpDir = '/tmp/changelog-test';

beforeEach(async () => {
// Create temp directory for test files
await fsp.mkdir(tmpDir, { recursive: true });
});

afterEach(async () => {
// Cleanup test files
if (existsSync(tmpDir)) {
await fsp.rm(tmpDir, { recursive: true, force: true });
}
});

it('should generate CHANGELOG.md files for modules needing release', async () => {
const terraformModules: TerraformModule[] = [
createMockTerraformModule({
directory: 'test/module1',
commitMessages: ['feat: Add new feature', 'fix: Fix bug'],
}),
createMockTerraformModule({
directory: 'test/module2',
commitMessages: ['feat: Another feature'],
}),
];

// Create module directories using actual absolute paths
for (const module of terraformModules) {
const fullPath = join(tmpDir, module.name);
await fsp.mkdir(fullPath, { recursive: true });
// Override the directory with absolute path for file operations
Object.defineProperty(module, 'directory', { value: fullPath, writable: true });
}

const changelogFiles = await generateChangelogFiles(terraformModules);

expect(changelogFiles).toHaveLength(2);
expect(changelogFiles[0]).toContain('CHANGELOG.md');
expect(changelogFiles[1]).toContain('CHANGELOG.md');

// Verify file contents
const changelog1 = await fsp.readFile(changelogFiles[0], 'utf8');
expect(changelog1).toContain('# Changelog - test/module1');
expect(changelog1).toContain('All notable changes to this module will be documented in this file.');
expect(changelog1).toContain('## `v1.0.0` (2024-11-05)');
expect(changelog1).toContain('feat: Add new feature');
expect(changelog1).toContain('fix: Fix bug');

const changelog2 = await fsp.readFile(changelogFiles[1], 'utf8');
expect(changelog2).toContain('# Changelog - test/module2');
expect(changelog2).toContain('feat: Another feature');
});

it('should include historical releases in changelog', async () => {
const terraformModule = createMockTerraformModule({
directory: 'test/module-with-history',
commitMessages: ['feat: New feature'],
tags: ['test/module-with-history/v1.0.0'], // Existing tag
releases: [
{
id: 1,
title: 'test/module-with-history/v1.0.0',
body: '## `v1.0.0` (2024-01-01)\n\n- Initial release',
tagName: 'test/module-with-history/v1.0.0',
},
],
});

const fullPath = join(tmpDir, terraformModule.name);
await fsp.mkdir(fullPath, { recursive: true });
Object.defineProperty(terraformModule, 'directory', { value: fullPath, writable: true });

const changelogFiles = await generateChangelogFiles([terraformModule]);

expect(changelogFiles).toHaveLength(1);

const changelog = await fsp.readFile(changelogFiles[0], 'utf8');
expect(changelog).toContain('# Changelog - test/module-with-history');
expect(changelog).toContain('## `v1.1.0` (2024-11-05)'); // New release
expect(changelog).toContain('feat: New feature');
expect(changelog).toContain('## `v1.0.0` (2024-01-01)'); // Historical release
expect(changelog).toContain('Initial release');
});

it('should return empty array when no modules need release', async () => {
const terraformModules: TerraformModule[] = [
createMockTerraformModule({
directory: 'test/no-release-module',
commitMessages: [], // No commits - no release needed
tags: ['test/no-release-module/v1.0.0'], // Already has a tag
}),
];

const changelogFiles = await generateChangelogFiles(terraformModules);

expect(changelogFiles).toHaveLength(0);
});

it('should handle empty module array', async () => {
const changelogFiles = await generateChangelogFiles([]);

expect(changelogFiles).toHaveLength(0);
});

it('should skip modules where changelog generation returns empty', async () => {
const terraformModule = createMockTerraformModule({
directory: 'test/edge-case-module',
commitMessages: ['feat: some change'],
});

// Mock getReleaseTagVersion to return null (edge case)
vi.spyOn(terraformModule, 'getReleaseTagVersion').mockReturnValue(null);

const fullPath = join(tmpDir, terraformModule.name);
await fsp.mkdir(fullPath, { recursive: true });
Object.defineProperty(terraformModule, 'directory', { value: fullPath, writable: true });

const changelogFiles = await generateChangelogFiles([terraformModule]);

expect(changelogFiles).toHaveLength(0);
});
});
});
2 changes: 2 additions & 0 deletions __tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ describe('config', () => {
expect(config.terraformDocsVersion).toBe('v0.20.0');
expect(config.deleteLegacyTags).toBe(true);
expect(config.disableWiki).toBe(false);
expect(config.exportChangelogFiles).toBe(false);
expect(config.wikiSidebarChangelogMax).toBe(5);
expect(config.disableBranding).toBe(false);
expect(config.githubToken).toBe('ghp_test_token_2c6912E7710c838347Ae178B4');
Expand All @@ -268,6 +269,7 @@ describe('config', () => {
['Terraform Docs Version: v0.20.0'],
['Delete Legacy Tags: true'],
['Disable Wiki: false'],
['Export Changelog Files: false'],
['Wiki Sidebar Changelog Max: 5'],
['Module Paths to Ignore: '],
['Module Change Exclude Patterns: .gitignore, *.md, *.tftest.hcl, tests/**'],
Expand Down
2 changes: 2 additions & 0 deletions __tests__/utils/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('utils/metadata', () => {
'terraform-docs-version',
'delete-legacy-tags',
'disable-wiki',
'export-changelog-files',
'wiki-sidebar-changelog-max',
'wiki-usage-template',
'disable-branding',
Expand Down Expand Up @@ -47,6 +48,7 @@ describe('utils/metadata', () => {
const booleanInputs = [
'delete-legacy-tags',
'disable-wiki',
'export-changelog-files',
'disable-branding',
'use-ssh-source-format',
'use-version-prefix',
Expand Down
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ inputs:
By default, this is set to false. Set to true to prevent wiki documentation from being generated.
required: true
default: "false"
export-changelog-files:
description: >
Whether to export CHANGELOG.md files for each Terraform module in the repository.
When enabled, a CHANGELOG.md file will be generated and committed to each module's directory
containing the full release history. This complements the wiki functionality and provides
an alternative changelog format that lives within the repository itself.
required: true
default: "false"
wiki-sidebar-changelog-max:
description: >
An integer that specifies how many changelog entries are displayed in the sidebar per module.
Expand Down
83 changes: 83 additions & 0 deletions src/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,86 @@ export function getTerraformModuleFullReleaseChangelog(terraformModule: Terrafor
.filter((body): body is string => Boolean(body))
.join('\n\n');
}

/**
* Generates and writes CHANGELOG.md files for Terraform modules that need a release.
*
* This function creates a CHANGELOG.md file in each module's directory containing the complete
* release history. The changelog includes:
* - A header with the module name
* - All release entries in reverse chronological order (newest first)
* - Properly formatted markdown sections for each release
*
* The function only processes modules that need a release and have commit messages.
* It creates the changelog content by combining the new release entry with historical
* release notes from the module's previous releases.
*
* @param {TerraformModule[]} terraformModules - Array of Terraform modules to process
* @returns {Promise<string[]>} Array of file paths to the generated CHANGELOG.md files
*
* @example
* ```typescript
* const changelogFiles = await generateChangelogFiles(terraformModules);
* console.log(changelogFiles);
* // Output: ['/path/to/module1/CHANGELOG.md', '/path/to/module2/CHANGELOG.md']
* ```
*/
export async function generateChangelogFiles(terraformModules: TerraformModule[]): Promise<string[]> {
const { writeFile } = await import('node:fs/promises');
const { join } = await import('node:path');
const { endGroup, info, startGroup } = await import('@actions/core');

console.time('Elapsed time generating changelog files');
startGroup('Generating CHANGELOG.md files');

const changelogFiles: string[] = [];
const modulesToRelease = terraformModules.filter((module) => module.needsRelease());

if (modulesToRelease.length === 0) {
info('No modules need release. Skipping changelog file generation.');
endGroup();
console.timeEnd('Elapsed time generating changelog files');
return changelogFiles;
}

for (const terraformModule of modulesToRelease) {
const changelogPath = join(terraformModule.directory, 'CHANGELOG.md');

// Get the new release entry
const newReleaseEntry = createTerraformModuleChangelog(terraformModule);

if (!newReleaseEntry) {
continue;
}

// Get historical changelog (existing releases)
const historicalChangelog = getTerraformModuleFullReleaseChangelog(terraformModule);

// Combine new entry with historical data
const changelogContent = [
`# Changelog - ${terraformModule.name}`,
'',
'All notable changes to this module will be documented in this file.',
'',
newReleaseEntry,
];

// Add historical changelog if it exists
if (historicalChangelog) {
changelogContent.push('');
changelogContent.push(historicalChangelog);
}

const fullChangelog = changelogContent.join('\n');

await writeFile(changelogPath, fullChangelog, 'utf8');
info(`Generated CHANGELOG.md for module: ${terraformModule.name}`);
changelogFiles.push(changelogPath);
}

info(`Generated ${changelogFiles.length} CHANGELOG.md file${changelogFiles.length !== 1 ? 's' : ''}`);
endGroup();
console.timeEnd('Elapsed time generating changelog files');

return changelogFiles;
}
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function initializeConfig(): Config {
info(`Terraform Docs Version: ${configInstance.terraformDocsVersion}`);
info(`Delete Legacy Tags: ${configInstance.deleteLegacyTags}`);
info(`Disable Wiki: ${configInstance.disableWiki}`);
info(`Export Changelog Files: ${configInstance.exportChangelogFiles}`);
info(`Wiki Sidebar Changelog Max: ${configInstance.wikiSidebarChangelogMax}`);
info(`Module Paths to Ignore: ${configInstance.modulePathIgnore.join(', ')}`);
info(`Module Change Exclude Patterns: ${configInstance.moduleChangeExcludePatterns.join(', ')}`);
Expand Down
Loading