From 757b2b1bfb9ca266da82aede7f6722c3e7a28169 Mon Sep 17 00:00:00 2001
From: Mark Johnson <739719+virgofx@users.noreply.github.com>
Date: Mon, 25 Nov 2024 20:14:54 +0000
Subject: [PATCH] tests(main): add test coverage for main()
- Refactor main.run() to reduce cognitive complexlity
- Update coverage badge
- Remove one legacy reference to process.env.RUNNER_TEMP and replace
with proper `mkdtempSync` calls.
---
__mocks__/@actions/core.ts | 4 +-
__tests__/context.test.ts | 17 +++
__tests__/main.test.ts | 239 +++++++++++++++++++++++++++++++++++
__tests__/releases.test.ts | 8 +-
__tests__/utils/file.test.ts | 104 +++++++--------
assets/coverage-badge.svg | 2 +-
src/main.ts | 168 ++++++++++++++----------
src/pull-request.ts | 4 +-
src/releases.ts | 8 +-
src/terraform-docs.ts | 12 +-
src/types/index.ts | 6 +
11 files changed, 438 insertions(+), 134 deletions(-)
create mode 100644 __tests__/main.test.ts
diff --git a/__mocks__/@actions/core.ts b/__mocks__/@actions/core.ts
index 3104820..e47f476 100644
--- a/__mocks__/@actions/core.ts
+++ b/__mocks__/@actions/core.ts
@@ -55,9 +55,7 @@ export const error: MockedFunction<(message: string | Error, properties?: Annota
* @param message - The error message or object.
* @throws An error with the specified message.
*/
-export const setFailed: MockedFunction<(message: string | Error) => void> = vi.fn((message: string | Error) => {
- actual.setFailed(message);
-});
+export const setFailed: MockedFunction<(message: string | Error) => void> = vi.fn((message: string | Error) => {});
/**
* Begins a new output group. Output until the next `endGroup` will be foldable in this group.
diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts
index aea0e05..0d61896 100644
--- a/__tests__/context.test.ts
+++ b/__tests__/context.test.ts
@@ -147,6 +147,23 @@ describe('context', () => {
expect(getContext().prTitle).toEqual(prTitle.trim());
});
+ it('should initialize and output only summary of long pull request body', () => {
+ const prBody = 'Test PR with long long title that extends past the 57 character mark';
+ mockReadFileSync.mockImplementation(() => {
+ return JSON.stringify(
+ createPullRequestMock({
+ action: 'test',
+ pull_request: {
+ body: prBody,
+ },
+ }),
+ );
+ });
+
+ getContext();
+ expect(info).toHaveBeenCalledWith(`Pull Request Body: ${prBody.slice(0, 57)}...`);
+ });
+
it('should initialize with null pull request body', () => {
mockReadFileSync.mockImplementation(() => {
return JSON.stringify(
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
new file mode 100644
index 0000000..4261a2a
--- /dev/null
+++ b/__tests__/main.test.ts
@@ -0,0 +1,239 @@
+import { run } from '@/main';
+import { config } from '@/mocks/config';
+import { context } from '@/mocks/context';
+import { addPostReleaseComment, addReleasePlanComment, hasReleaseComment } from '@/pull-request';
+import { createTaggedRelease, deleteLegacyReleases } from '@/releases';
+import { deleteLegacyTags } from '@/tags';
+import { ensureTerraformDocsConfigDoesNotExist, installTerraformDocs } from '@/terraform-docs';
+import { getAllTerraformModules, getTerraformChangedModules, getTerraformModulesToRemove } from '@/terraform-module';
+import type { GitHubRelease, TerraformChangedModule, TerraformModule } from '@/types';
+import { WikiStatus, checkoutWiki, commitAndPushWikiChanges, generateWikiFiles } from '@/wiki';
+import { info, setFailed } from '@actions/core';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+// Mock all required dependencies
+vi.mock('@/pull-request');
+vi.mock('@/releases');
+vi.mock('@/tags');
+vi.mock('@/terraform-docs');
+vi.mock('@/terraform-module');
+vi.mock('@/wiki');
+
+describe('main', () => {
+ // Mock module data
+ const mockRelease: GitHubRelease = {
+ id: 1,
+ title: 'Release v1.0.0',
+ body: 'Release notes',
+ tagName: 'modules/test-module/v1.0.0',
+ };
+
+ const mockChangedModule: TerraformChangedModule = {
+ moduleName: 'test-module',
+ directory: './modules/test-module',
+ tags: ['modules/test-module/v1.0.0'],
+ releases: [mockRelease],
+ latestTag: 'modules/test-module/v1.0.0',
+ latestTagVersion: 'v1.0.0',
+ isChanged: true,
+ commitMessages: ['feat: new feature'],
+ releaseType: 'minor',
+ nextTag: 'modules/test-module/v1.1.0',
+ nextTagVersion: 'v1.1.0',
+ };
+
+ // Add mock for getAllTerraformModules
+ const mockTerraformModule: TerraformModule = {
+ moduleName: 'test-module',
+ directory: './modules/test-module',
+ tags: ['modules/test-module/v1.0.0'],
+ releases: [mockRelease],
+ latestTag: 'modules/test-module/v1.0.0',
+ latestTagVersion: 'v1.0.0',
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ // Reset context and config before each test
+ context.isPrMergeEvent = false;
+ config.disableWiki = false;
+
+ // Reset mocks with default values
+ vi.mocked(hasReleaseComment).mockResolvedValue(false);
+ vi.mocked(getTerraformChangedModules).mockReturnValue([mockChangedModule]);
+ vi.mocked(getAllTerraformModules).mockReturnValue([mockTerraformModule]);
+ vi.mocked(getTerraformModulesToRemove).mockReturnValue([]);
+ });
+
+ it('should exit early if release comment exists', async () => {
+ vi.mocked(hasReleaseComment).mockResolvedValue(true);
+
+ await run();
+
+ expect(info).toHaveBeenCalledWith('Release comment found. Exiting.');
+ });
+
+ it('should handle errors', async () => {
+ vi.mocked(hasReleaseComment).mockRejectedValue(new Error('Test error'));
+
+ await run();
+
+ expect(setFailed).toHaveBeenCalledWith('Test error');
+ });
+
+ it('should handle non-Error type being thrown', async () => {
+ // Mock hasReleaseComment to throw a string instead of an Error
+ vi.mocked(checkoutWiki).mockImplementationOnce(() => {
+ throw 'string error message';
+ });
+
+ // Run the function
+ await run();
+
+ // The setFailed function should not have been called with an error message
+ // since the error wasn't an instance of Error
+ expect(addReleasePlanComment).toHaveBeenCalledTimes(1);
+ expect(setFailed).not.toHaveBeenCalled();
+ });
+
+ it('should call checkoutWiki when wiki is enabled', async () => {
+ vi.mocked(hasReleaseComment).mockResolvedValue(false);
+ vi.mocked(getTerraformChangedModules).mockReturnValue([mockChangedModule]);
+ context.isPrMergeEvent = false;
+ config.disableWiki = false;
+
+ await run();
+
+ expect(vi.mocked(checkoutWiki)).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not call checkoutWiki when wiki is disabled', async () => {
+ vi.mocked(hasReleaseComment).mockResolvedValue(false);
+ vi.mocked(getTerraformChangedModules).mockReturnValue([mockChangedModule]);
+ context.isPrMergeEvent = false;
+ config.disableWiki = true;
+
+ await run();
+
+ expect(vi.mocked(checkoutWiki)).not.toHaveBeenCalled();
+ });
+
+ // Wiki checkout error handling
+ it('should handle wiki checkout errors and add release plan comment', async () => {
+ vi.mocked(hasReleaseComment).mockResolvedValue(false);
+ context.isPrMergeEvent = false;
+ config.disableWiki = false;
+
+ const mockError = new Error('Wiki checkout failed\nAdditional error details');
+ vi.mocked(checkoutWiki).mockImplementationOnce(() => {
+ throw mockError;
+ });
+
+ vi.mocked(getTerraformChangedModules).mockReturnValue([mockChangedModule]);
+ vi.mocked(getTerraformModulesToRemove).mockReturnValue(['old-module']);
+
+ await run();
+
+ expect(addReleasePlanComment).toHaveBeenCalledWith([mockChangedModule], ['old-module'], {
+ status: WikiStatus.FAILURE,
+ errorMessage: 'Wiki checkout failed',
+ });
+ expect(setFailed).toHaveBeenCalledWith('Wiki checkout failed\nAdditional error details');
+ });
+
+ describe('merge event handling', () => {
+ const mockReleaseResponse: GitHubRelease = {
+ id: 2,
+ title: 'Release v1.1.0',
+ body: 'New release notes',
+ tagName: 'modules/test-module/v1.1.0',
+ };
+
+ beforeEach(() => {
+ context.isPrMergeEvent = true;
+
+ vi.mocked(hasReleaseComment).mockResolvedValue(false);
+ vi.mocked(getTerraformChangedModules).mockReturnValue([mockChangedModule]);
+ vi.mocked(createTaggedRelease).mockResolvedValue([
+ {
+ moduleName: mockChangedModule.moduleName,
+ release: mockReleaseResponse,
+ },
+ ]);
+ });
+
+ it('should handle merge event with wiki enabled', async () => {
+ config.disableWiki = false;
+
+ await run();
+
+ expect(createTaggedRelease).toHaveBeenCalledWith([mockChangedModule]);
+ expect(addPostReleaseComment).toHaveBeenCalledWith([
+ {
+ moduleName: mockChangedModule.moduleName,
+ release: mockReleaseResponse,
+ },
+ ]);
+ expect(deleteLegacyReleases).toHaveBeenCalled();
+ expect(deleteLegacyTags).toHaveBeenCalled();
+ expect(installTerraformDocs).toHaveBeenCalledWith(config.terraformDocsVersion);
+ expect(ensureTerraformDocsConfigDoesNotExist).toHaveBeenCalled();
+ expect(checkoutWiki).toHaveBeenCalled();
+ expect(generateWikiFiles).toHaveBeenCalledWith([mockTerraformModule]);
+ expect(commitAndPushWikiChanges).toHaveBeenCalled();
+ });
+
+ it('should handle merge event with wiki disabled', async () => {
+ config.disableWiki = true;
+
+ await run();
+
+ expect(createTaggedRelease).toHaveBeenCalledWith([mockChangedModule]);
+ expect(addPostReleaseComment).toHaveBeenCalledWith([
+ {
+ moduleName: mockChangedModule.moduleName,
+ release: mockReleaseResponse,
+ },
+ ]);
+ expect(deleteLegacyReleases).toHaveBeenCalled();
+ expect(deleteLegacyTags).toHaveBeenCalled();
+ expect(installTerraformDocs).not.toHaveBeenCalled();
+ expect(ensureTerraformDocsConfigDoesNotExist).not.toHaveBeenCalled();
+ expect(checkoutWiki).not.toHaveBeenCalled();
+ expect(generateWikiFiles).not.toHaveBeenCalled();
+ expect(commitAndPushWikiChanges).not.toHaveBeenCalled();
+ expect(info).toHaveBeenCalledWith('Wiki generation is disabled.');
+ });
+
+ it('should handle merge event sequence correctly', async () => {
+ config.disableWiki = false;
+ const expectedTaggedRelease = {
+ moduleName: mockChangedModule.moduleName,
+ release: mockReleaseResponse,
+ };
+
+ vi.mocked(getTerraformChangedModules).mockReturnValue([mockChangedModule]);
+ vi.mocked(createTaggedRelease).mockResolvedValue([expectedTaggedRelease]);
+
+ await run();
+
+ const createTaggedReleaseMock = vi.mocked(createTaggedRelease);
+ const addPostReleaseCommentMock = vi.mocked(addPostReleaseComment);
+ const deleteLegacyReleasesMock = vi.mocked(deleteLegacyReleases);
+ const deleteLegacyTagsMock = vi.mocked(deleteLegacyTags);
+
+ // Verify correct arguments
+ expect(createTaggedReleaseMock).toHaveBeenCalledWith([mockChangedModule]);
+ expect(addPostReleaseCommentMock).toHaveBeenCalledWith([expectedTaggedRelease]);
+
+ // Verify sequence order
+ const createTaggedReleaseCallOrder = createTaggedReleaseMock.mock.invocationCallOrder[0];
+ const deleteLegacyReleasesCallOrder = deleteLegacyReleasesMock.mock.invocationCallOrder[0];
+ const deleteLegacyTagsCallOrder = deleteLegacyTagsMock.mock.invocationCallOrder[0];
+
+ expect(createTaggedReleaseCallOrder).toBeLessThan(deleteLegacyReleasesCallOrder);
+ expect(deleteLegacyReleasesCallOrder).toBeLessThan(deleteLegacyTagsCallOrder);
+ });
+ });
+});
diff --git a/__tests__/releases.test.ts b/__tests__/releases.test.ts
index 70910ba..0f52a0d 100644
--- a/__tests__/releases.test.ts
+++ b/__tests__/releases.test.ts
@@ -1,3 +1,5 @@
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
import { config } from '@/mocks/config';
import { context } from '@/mocks/context';
import { createTaggedRelease, deleteLegacyReleases, getAllReleases } from '@/releases';
@@ -13,7 +15,9 @@ vi.mock('node:child_process', () => ({
vi.mock('node:fs', () => ({
existsSync: vi.fn(),
copyFileSync: vi.fn(),
- mkdirSync: vi.fn(),
+ mkdtempSync: vi.fn().mockImplementation(() => {
+ return join(tmpdir(), (Math.random() + 1).toString(36).substring(7));
+ }),
cpSync: vi.fn(),
readdirSync: vi.fn().mockImplementation(() => []),
}));
@@ -322,6 +326,8 @@ describe('releases', () => {
});
const result = await createTaggedRelease([mockTerraformModule]);
+ console.log(result);
+
expect(result).toHaveLength(1);
expect(result[0].moduleName).toBe('test-module');
expect(result[0].release.title).toBe('test-module/v1.0.1');
diff --git a/__tests__/utils/file.test.ts b/__tests__/utils/file.test.ts
index 5d6e8fb..e731848 100644
--- a/__tests__/utils/file.test.ts
+++ b/__tests__/utils/file.test.ts
@@ -5,27 +5,27 @@ import { copyModuleContents, isTerraformDirectory, removeDirectoryContents, shou
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
describe('utils/file', () => {
- let tempDir: string;
+ let tmpDir: string;
beforeEach(() => {
// Create a temporary directory before each test
- tempDir = mkdtempSync(join(tmpdir(), 'test-dir-'));
+ tmpDir = mkdtempSync(join(tmpdir(), 'test-dir-'));
});
afterEach(() => {
// Remove temporary directory
- rmSync(tempDir, { recursive: true });
+ rmSync(tmpDir, { recursive: true });
});
describe('isTerraformDirectory()', () => {
it('should return true for a directory that has .tf files', () => {
- writeFileSync(join(tempDir, 'main.tf'), '# terraform code');
- expect(isTerraformDirectory(tempDir)).toBe(true);
+ writeFileSync(join(tmpDir, 'main.tf'), '# terraform code');
+ expect(isTerraformDirectory(tmpDir)).toBe(true);
});
it('should return false for a directory that has .tf files', () => {
- writeFileSync(join(tempDir, 'README.md'), '# README');
- expect(isTerraformDirectory(tempDir)).toBe(false);
+ writeFileSync(join(tmpDir, 'README.md'), '# README');
+ expect(isTerraformDirectory(tmpDir)).toBe(false);
});
it('should return false for invalid directory', () => {
@@ -35,33 +35,33 @@ describe('utils/file', () => {
describe('shouldExcludeFile()', () => {
it('should exclude file when pattern matches', () => {
- const baseDirectory = tempDir;
- const filePath = join(tempDir, 'file.txt');
+ const baseDirectory = tmpDir;
+ const filePath = join(tmpDir, 'file.txt');
const excludePatterns = ['*.txt'];
expect(shouldExcludeFile(baseDirectory, filePath, excludePatterns)).toBe(true);
});
it('should not exclude file when pattern does not match', () => {
- const baseDirectory = tempDir;
- const filePath = join(tempDir, 'file.txt');
+ const baseDirectory = tmpDir;
+ const filePath = join(tmpDir, 'file.txt');
const excludePatterns = ['*.js'];
expect(shouldExcludeFile(baseDirectory, filePath, excludePatterns)).toBe(false);
});
it('should handle relative paths correctly', () => {
- const baseDirectory = tempDir;
- const filePath = join(tempDir, 'subdir', 'file.txt');
+ const baseDirectory = tmpDir;
+ const filePath = join(tmpDir, 'subdir', 'file.txt');
const excludePatterns = ['subdir/*.txt'];
expect(shouldExcludeFile(baseDirectory, filePath, excludePatterns)).toBe(true);
});
it('should handle exclusion pattern: *.md', () => {
- const baseDirectory = tempDir;
- const filePath1 = join(tempDir, 'README.md');
- const filePath2 = join(tempDir, 'nested', 'README.md');
+ const baseDirectory = tmpDir;
+ const filePath1 = join(tmpDir, 'README.md');
+ const filePath2 = join(tmpDir, 'nested', 'README.md');
const excludePatterns = ['*.md'];
expect(shouldExcludeFile(baseDirectory, filePath1, excludePatterns)).toBe(true);
@@ -69,9 +69,9 @@ describe('utils/file', () => {
});
it('should handle exclusion pattern: **/*.md', () => {
- const baseDirectory = tempDir;
- const filePath1 = join(tempDir, 'README.md');
- const filePath2 = join(tempDir, 'nested', 'README.md');
+ const baseDirectory = tmpDir;
+ const filePath1 = join(tmpDir, 'README.md');
+ const filePath2 = join(tmpDir, 'nested', 'README.md');
const excludePatterns = ['**/*.md'];
expect(shouldExcludeFile(baseDirectory, filePath1, excludePatterns)).toBe(true);
@@ -79,10 +79,10 @@ describe('utils/file', () => {
});
it('should handle exclusion pattern: tests/**', () => {
- const baseDirectory = tempDir;
- const filePath1 = join(tempDir, 'tests/config.test.ts');
- const filePath2 = join(tempDir, 'tests2/config.test.ts');
- const filePath3 = join(tempDir, 'tests2/tests/config.test.ts');
+ const baseDirectory = tmpDir;
+ const filePath1 = join(tmpDir, 'tests/config.test.ts');
+ const filePath2 = join(tmpDir, 'tests2/config.test.ts');
+ const filePath3 = join(tmpDir, 'tests2/tests/config.test.ts');
const excludePatterns = ['tests/**'];
expect(shouldExcludeFile(baseDirectory, filePath1, excludePatterns)).toBe(true);
@@ -91,10 +91,10 @@ describe('utils/file', () => {
});
it('should handle exclusion pattern: **/tests/**', () => {
- const baseDirectory = tempDir;
- const filePath1 = join(tempDir, 'tests/config.test.ts');
- const filePath2 = join(tempDir, 'tests2/config.test.ts');
- const filePath3 = join(tempDir, 'tests2/tests/config.test.ts');
+ const baseDirectory = tmpDir;
+ const filePath1 = join(tmpDir, 'tests/config.test.ts');
+ const filePath2 = join(tmpDir, 'tests2/config.test.ts');
+ const filePath3 = join(tmpDir, 'tests2/tests/config.test.ts');
const excludePatterns = ['**/tests/**'];
expect(shouldExcludeFile(baseDirectory, filePath1, excludePatterns)).toBe(true);
@@ -106,13 +106,13 @@ describe('utils/file', () => {
describe('copyModuleContents()', () => {
beforeEach(() => {
// Create src and dest directories for every test in this suite
- mkdirSync(join(tempDir, 'src'), { recursive: true });
- mkdirSync(join(tempDir, 'dest'), { recursive: true });
+ mkdirSync(join(tmpDir, 'src'), { recursive: true });
+ mkdirSync(join(tmpDir, 'dest'), { recursive: true });
});
it('should copy directory contents excluding files that match patterns', () => {
- const srcDirectory = join(tempDir, 'src');
- const destDirectory = join(tempDir, 'dest');
+ const srcDirectory = join(tmpDir, 'src');
+ const destDirectory = join(tmpDir, 'dest');
const excludePatterns = ['*.txt'];
// Create files in src directory
@@ -128,8 +128,8 @@ describe('utils/file', () => {
});
it('should handle recursive directory copying', () => {
- const srcDirectory = join(tempDir, 'src');
- const destDirectory = join(tempDir, 'dest');
+ const srcDirectory = join(tmpDir, 'src');
+ const destDirectory = join(tmpDir, 'dest');
const excludePatterns: string[] = [];
// Create source structure
@@ -146,8 +146,8 @@ describe('utils/file', () => {
});
it('should copy files excluding multiple patterns', () => {
- const srcDirectory = join(tempDir, 'src');
- const destDirectory = join(tempDir, 'dest');
+ const srcDirectory = join(tmpDir, 'src');
+ const destDirectory = join(tmpDir, 'dest');
const excludePatterns = ['*.txt', '*.js'];
writeFileSync(join(srcDirectory, 'file.txt'), 'Hello World!');
@@ -162,8 +162,8 @@ describe('utils/file', () => {
});
it('should handle copying from an empty directory', () => {
- const srcDirectory = join(tempDir, 'src');
- const destDirectory = join(tempDir, 'dest');
+ const srcDirectory = join(tmpDir, 'src');
+ const destDirectory = join(tmpDir, 'dest');
const excludePatterns = ['*.txt'];
copyModuleContents(srcDirectory, destDirectory, excludePatterns);
@@ -173,8 +173,8 @@ describe('utils/file', () => {
});
it('should throw an error if the source directory does not exist', () => {
- const nonExistentSrcDirectory = join(tempDir, 'non-existent-src');
- const destDirectory = join(tempDir, 'dest');
+ const nonExistentSrcDirectory = join(tmpDir, 'non-existent-src');
+ const destDirectory = join(tmpDir, 'dest');
const excludePatterns = ['*.txt'];
expect(() => {
@@ -183,8 +183,8 @@ describe('utils/file', () => {
});
it('should copy files that do not match any exclusion patterns', () => {
- const srcDirectory = join(tempDir, 'src');
- const destDirectory = join(tempDir, 'dest');
+ const srcDirectory = join(tmpDir, 'src');
+ const destDirectory = join(tmpDir, 'dest');
const excludePatterns = ['*.js'];
writeFileSync(join(srcDirectory, 'file.txt'), 'Hello World!');
@@ -197,8 +197,8 @@ describe('utils/file', () => {
});
it('should overwrite files in the destination if they have the same name and do not match exclusion patterns', () => {
- const srcDirectory = join(tempDir, 'src');
- const destDirectory = join(tempDir, 'dest');
+ const srcDirectory = join(tmpDir, 'src');
+ const destDirectory = join(tmpDir, 'dest');
const excludePatterns: string[] = [];
writeFileSync(join(srcDirectory, 'file.txt'), 'Hello World from source!');
@@ -213,7 +213,7 @@ describe('utils/file', () => {
describe('removeDirectoryContents()', () => {
it('should remove directory contents except for specified exceptions', () => {
- const directory = join(tempDir, 'dir');
+ const directory = join(tmpDir, 'dir');
const exceptions = ['file.txt'];
mkdirSync(directory);
@@ -227,7 +227,7 @@ describe('utils/file', () => {
});
it('should handle recursive directory removal', () => {
- const directory = join(tempDir, 'dir');
+ const directory = join(tmpDir, 'dir');
const exceptions: string[] = [];
mkdirSync(directory);
@@ -242,7 +242,7 @@ describe('utils/file', () => {
});
it('should handle exceptions correctly', () => {
- const directory = join(tempDir, 'dir');
+ const directory = join(tmpDir, 'dir');
const exceptions = ['file.txt', 'subdir'];
mkdirSync(directory);
@@ -259,7 +259,7 @@ describe('utils/file', () => {
});
it('should handle an empty directory', () => {
- const directory = join(tempDir, 'dir');
+ const directory = join(tmpDir, 'dir');
const exceptions: string[] = [];
mkdirSync(directory); // Create an empty directory
@@ -270,7 +270,7 @@ describe('utils/file', () => {
});
it('should not remove if only exceptions are present', () => {
- const directory = join(tempDir, 'dir');
+ const directory = join(tmpDir, 'dir');
const exceptions = ['file.txt'];
mkdirSync(directory);
@@ -283,7 +283,7 @@ describe('utils/file', () => {
});
it('should handle nested exceptions correctly', () => {
- const directory = join(tempDir, 'dir');
+ const directory = join(tmpDir, 'dir');
const exceptions = ['subdir'];
mkdirSync(directory);
@@ -299,7 +299,7 @@ describe('utils/file', () => {
});
it('should not throw an error if the directory does not exist', () => {
- const nonExistentDirectory = join(tempDir, 'non-existent-dir');
+ const nonExistentDirectory = join(tmpDir, 'non-existent-dir');
const exceptions = ['file.txt'];
expect(() => {
@@ -308,7 +308,7 @@ describe('utils/file', () => {
});
it('should handle exceptions that do not exist in the directory', () => {
- const directory = join(tempDir, 'dir');
+ const directory = join(tmpDir, 'dir');
const exceptions = ['file.txt'];
mkdirSync(directory);
@@ -320,7 +320,7 @@ describe('utils/file', () => {
});
it('should remove directory contents when no exceptions specified', () => {
- const directory = join(tempDir, 'dir');
+ const directory = join(tmpDir, 'dir');
mkdirSync(directory);
writeFileSync(join(directory, 'file.txt'), 'Hello World!');
diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg
index 6bd33ac..5bb55be 100644
--- a/assets/coverage-badge.svg
+++ b/assets/coverage-badge.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
index aecd4d0..6a904f6 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -5,32 +5,115 @@ import { createTaggedRelease, deleteLegacyReleases, getAllReleases } from '@/rel
import { deleteLegacyTags, getAllTags } from '@/tags';
import { ensureTerraformDocsConfigDoesNotExist, installTerraformDocs } from '@/terraform-docs';
import { getAllTerraformModules, getTerraformChangedModules, getTerraformModulesToRemove } from '@/terraform-module';
-import type { Config, Context } from '@/types';
+import type {
+ Config,
+ Context,
+ GitHubRelease,
+ ReleasePlanCommentOptions,
+ TerraformChangedModule,
+ TerraformModule,
+} from '@/types';
import { WikiStatus, checkoutWiki, commitAndPushWikiChanges, generateWikiFiles } from '@/wiki';
import { info, setFailed } from '@actions/core';
/**
- * Ensures both config and context are initialized in the correct order.
- * This function handles the initialization sequence and returns both objects.
+ * Initializes and returns the configuration and context objects.
+ * Config must be initialized before context due to dependency constraints.
+ *
+ * @returns {{ config: Config; context: Context }} Initialized config and context objects.
*/
function initialize(): { config: Config; context: Context } {
- // Force config initialization first
const configInstance = getConfig();
-
- // Then initialize context
const contextInstance = getContext();
-
return { config: configInstance, context: contextInstance };
}
/**
- * The main function for the action.
- * @returns {Promise} Resolves when the action is complete.
+ * Handles wiki-related operations, including checkout, generating release plan comments,
+ * and error handling for failures.
+ *
+ * @param {Config} config - The configuration object containing wiki and Terraform Docs settings.
+ * @param {TerraformChangedModule[]} terraformChangedModules - List of changed Terraform modules.
+ * @param {string[]} terraformModuleNamesToRemove - List of Terraform module names to remove.
+ * @returns {Promise} Resolves when wiki-related operations are completed.
+ */
+async function handleWikiOperations(
+ config: Config,
+ terraformChangedModules: TerraformChangedModule[],
+ terraformModuleNamesToRemove: string[],
+): Promise {
+ let wikiStatus: WikiStatus = WikiStatus.DISABLED;
+ let failure: string | undefined;
+ let error: Error | undefined;
+
+ try {
+ if (!config.disableWiki) {
+ checkoutWiki();
+ wikiStatus = WikiStatus.SUCCESS;
+ }
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message.split('\n')[0] : String(err).split('\n')[0];
+ wikiStatus = WikiStatus.FAILURE;
+ failure = errorMessage;
+ error = err as Error;
+ } finally {
+ const commentOptions: ReleasePlanCommentOptions = {
+ status: wikiStatus,
+ errorMessage: failure,
+ };
+ await addReleasePlanComment(terraformChangedModules, terraformModuleNamesToRemove, commentOptions);
+ }
+
+ if (error) {
+ throw error;
+ }
+}
+
+/**
+ * Handles merge-event-specific operations, including tagging new releases, deleting legacy resources,
+ * and optionally generating Terraform Docs-based wiki documentation.
+ *
+ * @param {Config} config - The configuration object.
+ * @param {TerraformChangedModule[]} terraformChangedModules - List of changed Terraform modules.
+ * @param {string[]} terraformModuleNamesToRemove - List of Terraform module names to remove.
+ * @param {TerraformModule[]} terraformModules - List of all Terraform modules in the repository.
+ * @param {GitHubRelease[]} allReleases - List of all GitHub releases in the repository.
+ * @param {string[]} allTags - List of all tags in the repository.
+ * @returns {Promise} Resolves when merge-event operations are complete.
+ */
+async function handleMergeEvent(
+ config: Config,
+ terraformChangedModules: TerraformChangedModule[],
+ terraformModuleNamesToRemove: string[],
+ terraformModules: TerraformModule[],
+ allReleases: GitHubRelease[],
+ allTags: string[],
+): Promise {
+ const updatedModules = await createTaggedRelease(terraformChangedModules);
+ await addPostReleaseComment(updatedModules);
+
+ await deleteLegacyReleases(terraformModuleNamesToRemove, allReleases);
+ await deleteLegacyTags(terraformModuleNamesToRemove, allTags);
+
+ if (config.disableWiki) {
+ info('Wiki generation is disabled.');
+ } else {
+ installTerraformDocs(config.terraformDocsVersion);
+ ensureTerraformDocsConfigDoesNotExist();
+ checkoutWiki();
+ await generateWikiFiles(terraformModules);
+ commitAndPushWikiChanges();
+ }
+}
+
+/**
+ * Entry point for the GitHub Action. Determines the flow based on whether the event
+ * is a pull request or a merge, and executes the appropriate operations.
+ *
+ * @returns {Promise} Resolves when the action completes successfully or fails.
*/
export async function run(): Promise {
try {
- // Initialize the config and context which will be used throughout the action
- // caching each instance as a singleton with proxy accesors to improve performance.
const { config, context } = initialize();
if (await hasReleaseComment()) {
@@ -38,69 +121,24 @@ export async function run(): Promise {
return;
}
- // Fetch all commits along with associated files in this PR
const commits = await getPullRequestCommits();
-
- // Fetch all tags associated with this PR
const allTags = await getAllTags();
-
- // Fetch all releases associated with this PR
const allReleases = await getAllReleases();
-
- // Get all Terraform modules in this repository including changed metadata
const terraformModules = getAllTerraformModules(commits, allTags, allReleases);
-
- // Create a new array of only changed Terraform modules
const terraformChangedModules = getTerraformChangedModules(terraformModules);
-
- // Get an array of terraform module names to remove based on existing tags
const terraformModuleNamesToRemove = getTerraformModulesToRemove(allTags, terraformModules);
if (!context.isPrMergeEvent) {
- let wikiStatus = WikiStatus.DISABLED;
- let failure: string | undefined;
- let error: Error | undefined;
-
- try {
- if (!config.disableWiki) {
- checkoutWiki();
- wikiStatus = WikiStatus.SUCCESS;
- }
- } catch (err) {
- // Capture error message if the checkout fails
- const errorMessage = (err as Error).message.split('\n')[0] || 'Unknown error during wiki checkout';
- wikiStatus = WikiStatus.FAILURE;
- failure = errorMessage;
- error = err as Error;
- } finally {
- await addReleasePlanComment(terraformChangedModules, terraformModuleNamesToRemove, {
- status: wikiStatus,
- errorMessage: failure,
- });
- }
-
- // If we have an error, let's throw it so that the action fails after we've successfully commented on the PR.
- if (error !== undefined) {
- throw error;
- }
+ await handleWikiOperations(config, terraformChangedModules, terraformModuleNamesToRemove);
} else {
- // Create the tagged release and post a comment to the PR
- const updatedModules = await createTaggedRelease(terraformChangedModules);
- await addPostReleaseComment(updatedModules);
-
- // Delete legacy releases and tags (Ensure we delete releases first)
- await deleteLegacyReleases(terraformModuleNamesToRemove, allReleases);
- await deleteLegacyTags(terraformModuleNamesToRemove, allTags);
-
- if (config.disableWiki) {
- info('Wiki generation is disabled.');
- } else {
- installTerraformDocs(config.terraformDocsVersion);
- ensureTerraformDocsConfigDoesNotExist();
- checkoutWiki();
- await generateWikiFiles(terraformModules);
- commitAndPushWikiChanges();
- }
+ await handleMergeEvent(
+ config,
+ terraformChangedModules,
+ terraformModuleNamesToRemove,
+ terraformModules,
+ allReleases,
+ allTags,
+ );
}
} catch (error) {
if (error instanceof Error) {
diff --git a/src/pull-request.ts b/src/pull-request.ts
index 09c41c7..e3368a4 100644
--- a/src/pull-request.ts
+++ b/src/pull-request.ts
@@ -1,7 +1,7 @@
import { getPullRequestChangelog } from '@/changelog';
import { config } from '@/config';
import { context } from '@/context';
-import type { CommitDetails, GitHubRelease, TerraformChangedModule } from '@/types';
+import type { CommitDetails, GitHubRelease, ReleasePlanCommentOptions, TerraformChangedModule } from '@/types';
import { BRANDING_COMMENT, GITHUB_ACTIONS_BOT_USER_ID, PR_RELEASE_MARKER, PR_SUMMARY_MARKER } from '@/utils/constants';
import { WikiStatus, getWikiLink } from '@/wiki';
import { debug, endGroup, info, startGroup } from '@actions/core';
@@ -205,7 +205,7 @@ export async function getPullRequestCommits(): Promise {
export async function addReleasePlanComment(
terraformChangedModules: TerraformChangedModule[],
terraformModuleNamesToRemove: string[],
- wikiStatus: { status: WikiStatus; errorMessage?: string },
+ wikiStatus: ReleasePlanCommentOptions,
): Promise {
console.time('Elapsed time commenting on pull request');
startGroup('Adding pull request release plan comment');
diff --git a/src/releases.ts b/src/releases.ts
index 198af3b..e8b5c72 100644
--- a/src/releases.ts
+++ b/src/releases.ts
@@ -1,6 +1,7 @@
import { execFileSync } from 'node:child_process';
import type { ExecSyncOptions } from 'node:child_process';
-import { cpSync, mkdirSync } from 'node:fs';
+import { cpSync, mkdtempSync } from 'node:fs';
+import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { getModuleChangelog } from '@/changelog';
import { config } from '@/config';
@@ -115,14 +116,13 @@ export async function createTaggedRelease(
try {
for (const module of terraformChangedModules) {
const { moduleName, directory, releaseType, nextTag, nextTagVersion } = module;
- const tmpDir = join(process.env.RUNNER_TEMP ?? '', 'tmp', moduleName);
info(`Release type: ${releaseType}`);
info(`Next tag version: ${nextTag}`);
// Create a temporary working directory
- mkdirSync(tmpDir, { recursive: true });
- info(`Creating temp directory: ${tmpDir}`);
+ const tmpDir = mkdtempSync(join(tmpdir(), moduleName));
+ info(`Created temp directory: ${tmpDir}`);
// Copy the module's contents to the temporary directory, excluding specified patterns
copyModuleContents(directory, tmpDir, config.moduleAssetExcludePatterns);
diff --git a/src/terraform-docs.ts b/src/terraform-docs.ts
index a4df4aa..d0f1efe 100644
--- a/src/terraform-docs.ts
+++ b/src/terraform-docs.ts
@@ -145,7 +145,7 @@ export function installTerraformDocs(terraformDocsVersion: string): void {
startGroup(`Installing terraform-docs ${terraformDocsVersion}`);
const cwd = process.cwd();
- let tempDir = null;
+ let tmpDir = null;
try {
validateTerraformDocsVersion(terraformDocsVersion);
@@ -156,8 +156,8 @@ export function installTerraformDocs(terraformDocsVersion: string): void {
// Create a temp directory to handle the extraction so this doesn't clobber our
// current working directory.
- tempDir = mkdtempSync(join(tmpdir(), 'terraform-docs-'));
- process.chdir(tempDir);
+ tmpDir = mkdtempSync(join(tmpdir(), 'terraform-docs-'));
+ process.chdir(tmpDir);
if (goPlatform === 'windows') {
const powershellPath = which.sync('powershell');
@@ -206,9 +206,9 @@ export function installTerraformDocs(terraformDocsVersion: string): void {
execFileSync('/usr/local/bin/terraform-docs', ['--version'], { stdio: 'inherit' });
}
} finally {
- if (tempDir !== null) {
- info(`Removing temp dir ${tempDir}`);
- rmSync(tempDir, { recursive: true });
+ if (tmpDir !== null) {
+ info(`Removing temp dir ${tmpDir}`);
+ rmSync(tmpDir, { recursive: true });
}
process.chdir(cwd);
console.timeEnd('Elapsed time installing terraform-docs');
diff --git a/src/types/index.ts b/src/types/index.ts
index eb64c0a..2a3ce1d 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,3 +1,4 @@
+import type { WikiStatus } from '@/wiki';
import type { PaginateInterface } from '@octokit/plugin-paginate-rest';
import type { Api } from '@octokit/plugin-rest-endpoint-methods';
@@ -290,3 +291,8 @@ export interface ExecSyncError extends Error {
*/
error: Error;
}
+
+export interface ReleasePlanCommentOptions {
+ status: WikiStatus;
+ errorMessage?: string;
+}