Skip to content

feat: add GHES compatibility, update terraform-docs to v0.20.0, and improve utility functions #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
348 changes: 255 additions & 93 deletions .github/scripts/changelog.js

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions .github/workflows/release-start.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ jobs:
uses: actions/github-script@v7
id: changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_READ_AND_MODELS }}
with:
result-encoding: json
script: |
Expand Down
Empty file removed CHANGELOG.md
Empty file.
42 changes: 32 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ maintains independence while living in the same repository, with proper isolatio
Additionally, the action generates a beautifully crafted wiki for each module, complete with readme information, usage
examples, Terraform-docs details, and a full changelog.

Compatible with both GitHub.com and GitHub Enterprise Server (GHES) – works seamlessly in cloud and on-premises
environments.

## 🚀 Features

- **Efficient Module Tagging** – Only includes module directory content, dramatically improving Terraform performance.
Expand Down Expand Up @@ -70,17 +73,20 @@ example of how to use this action in a monorepo setup. See real-world usage in a

## Getting Started

### Step 1: Ensure GitHub Wiki is Enabled
### Step 1: Enable and Initialize GitHub Wiki

Before using this action, you'll need to enable the wiki feature for your repository:

Before using this action, make sure that the wiki is enabled and initialized for your repository:
1. Go to your repository's homepage
1. Navigate to the **Settings** tab
1. Under the **Features** section, check the **Wikis** option to enable GitHub Wiki
1. Click on the **Wiki** tab in your repository
1. Click **Create the first page** button
1. Add a simple title (like "Home") and some content
1. Click **Save Page** to initialize the wiki

1. Go to your repository's homepage.
1. Navigate to the **Settings** tab.
1. Under the **Features** section, ensure the **Wikis** option is checked to enable the GitHub Wiki.
1. Navigate to the **Wiki** tab on your repository.
1. Click the **Create the first page** button and add a basic title like **Home** to initialize the wiki with an initial
commit.
1. Save the changes to ensure your wiki is not empty when the GitHub Action updates it with module information.
> This initialization step is necessary because GitHub doesn't provide an API to programmatically enable or initialize
> the wiki.

### Step 2: Configure the Action

Expand Down Expand Up @@ -116,6 +122,22 @@ reasonably configured.

If you need to customize additional parameters, please refer to [Input Parameters](#input-parameters) section below.

## GitHub Enterprise Server (GHES) Support

This action is fully compatible with GitHub Enterprise Server deployments:

- **Automatic Detection**: The action automatically detects when running on GHES and adjusts API endpoints accordingly
- **Wiki Generation**: Full wiki support works on GHES instances with wiki features enabled
- **Release Management**: Creates releases and tags using your GHES instance's API
- **No Additional Configuration**: Works out-of-the-box on GHES without requiring special configuration
- **SSH Source Format**: Use the use-ssh-source-format parameter for GHES environments that prefer SSH-based Git URLs

### GHES Requirements

- GitHub Enterprise Server version that supports GitHub Actions
- Wiki feature enabled on your GHES instance (contact your administrator if wikis are disabled)
- Appropriate permissions for the GitHub Actions runner to access repository features

## Permissions

Before executing the GitHub Actions workflow, ensure that you have the necessary permissions set for accessing pull
Expand Down Expand Up @@ -360,7 +382,7 @@ by Piotr Krukowski.
- **100% GitHub-based**: This action has no external dependencies, eliminating the need for additional authentication
and complexity. Unlike earlier variations that stored built module assets in external services like Amazon S3, this
action keeps everything within GitHub, providing a self-contained and streamlined solution for managing Terraform
modules.
modules. Works seamlessly with both GitHub.com and GitHub Enterprise Server environments.
- **Pull Request-based workflow**: This action runs on the pull_request event, using pull request comments to track
permanent releases tied to commits. This method ensures persistence, unlike Action Artifacts, which expire. As a
result, the module does not support non-PR workflows, such as direct pushes to the default branch.
Expand Down
2 changes: 1 addition & 1 deletion __mocks__/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const defaultConfig: Config = {
minorKeywords: ['feat', 'feature'],
patchKeywords: ['fix', 'chore'],
defaultFirstTag: 'v1.0.0',
terraformDocsVersion: 'v0.19.0',
terraformDocsVersion: 'v0.20.0',
deleteLegacyTags: false,
disableWiki: false,
wikiSidebarChangelogMax: 10,
Expand Down
32 changes: 30 additions & 2 deletions __tests__/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,34 @@ describe('context', () => {
});
expect(getContext().prBody).toEqual('');
});

it('should use custom GITHUB_API_URL when provided', () => {
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)
expect(context).toBeDefined();
expect(context.octokit).toBeDefined();
});

it('should use default GITHUB_API_URL when not provided', () => {
// 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
expect(context).toBeDefined();
expect(context.octokit).toBeDefined();
});
});

describe('context proxy', () => {
Expand All @@ -185,14 +213,14 @@ describe('context', () => {
const getterRepo = getContext().repo;
expect(proxyRepo).toEqual(getterRepo);
expect(startGroup).toHaveBeenCalledWith('Initializing Context');
expect(info).toHaveBeenCalledTimes(9);
expect(info).toHaveBeenCalledTimes(11);

// Reset mock call counts/history via mockClear()
vi.mocked(info).mockClear();
vi.mocked(startGroup).mockClear();

// Second access should not trigger initialization
const prNumber = context.prNumber;
const prNumber = context.prNumber; // Intentionally access a property with no usage
expect(startGroup).not.toHaveBeenCalled();
expect(info).not.toHaveBeenCalled();
});
Expand Down
2 changes: 1 addition & 1 deletion __tests__/fixtures/_Footer.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<h4 align="center">Powered by <img src="https://raw.githubusercontent.com/techpivot/terraform-module-releaser/refs/heads/main/assets/github-mark-12x14.png" height="14" width="12" align="top" /> <a href="https://github.com/techpivot/terraform-module-releaser">techpivot/terraform-module-releaser</a></h4>
<h3 align="center">Powered by:&nbsp;&nbsp;<a href="https://github.com/techpivot/terraform-module-releaser"><img src="https://raw.githubusercontent.com/techpivot/terraform-module-releaser/refs/heads/main/assets/octicons-mark-github.svg" height="14" width="14" align="center" /></a> <a href="https://github.com/techpivot/terraform-module-releaser">techpivot/terraform-module-releaser</a></h3>
172 changes: 151 additions & 21 deletions __tests__/pull-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,16 +444,12 @@ describe('pull-request', () => {

expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining(
'**Note**: The following Terraform modules no longer exist in source; however, corresponding tags/releases exist.',
),
body: expect.stringContaining('**⚠️ The following module no longer exists in source but has tags/releases.'),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining(
'Automation tag/release deletion is **enabled** and corresponding tags/releases will be automatically deleted.<br>',
),
body: expect.stringContaining('It will be automatically deleted.'),
}),
);

Expand All @@ -464,16 +460,7 @@ describe('pull-request', () => {

expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining(
'**Note**: The following Terraform modules no longer exist in source; however, corresponding tags/releases exist.',
),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining(
'Automation tag/release deletion is **disabled** — **no** subsequent action will take place.<br>',
),
body: expect.stringContaining('⏸️ Existing tags and releases will be **preserved**'),
}),
);
});
Expand Down Expand Up @@ -503,37 +490,180 @@ describe('pull-request', () => {
);
});

it('should include modules to remove when specified', async () => {
it('should include modules to remove when flag enabled', async () => {
const modulesToRemove = ['legacy-module1', 'legacy-module2'];

stubOctokitReturnData('issues.createComment', {
data: { id: 1, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
});
stubOctokitReturnData('issues.listComments', { data: [] });
config.set({ deleteLegacyTags: true });

await addReleasePlanComment([], modulesToRemove, { status: WikiStatus.SUCCESS });

expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('- `legacy-module1`'),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('- `legacy-module2`'),
}),
);
});

it('should not include modules to remove when flag disabled ', async () => {
const modulesToRemove = ['legacy-module1', 'legacy-module2'];

stubOctokitReturnData('issues.createComment', {
data: { id: 1, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
});
stubOctokitReturnData('issues.listComments', { data: [] });
config.set({ deleteLegacyTags: false });

await addReleasePlanComment([], modulesToRemove, { status: WikiStatus.SUCCESS });

const createCommentCalls = vi.mocked(context.octokit.rest.issues.createComment).mock.calls;
expect(createCommentCalls.length).toBeGreaterThanOrEqual(1);

// Get the comment body text from the first call
const commentBody = createCommentCalls[0]?.[0]?.body as string;

// Ensure both modules are not included in the body
expect(commentBody).not.toContain('`legacy-module1`');
expect(commentBody).not.toContain('`legacy-module2`');
expect(commentBody).toContain('⏸️ Existing tags and releases will be **preserved**');
});

it('should handle cleanup when delete-legacy-tags is enabled but no modules to remove', async () => {
const newCommentId = 12345;
config.set({ deleteLegacyTags: true });
stubOctokitReturnData('issues.createComment', {
data: { id: newCommentId, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
});
stubOctokitReturnData('issues.listComments', { data: [] });

await addReleasePlanComment(terraformChangedModules, [], { status: WikiStatus.SUCCESS });

expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('`legacy-module1`, `legacy-module2`'),
body: expect.stringContaining(
'✅ All tags and releases are synchronized with the codebase. No cleanup required.',
),
}),
);
});

it('should handle multiple modules to remove with plural warning message', async () => {
const newCommentId = 12345;
const terraformModuleNamesToRemove = ['aws/module1', 'aws/module2', 'gcp/module3'];
config.set({ deleteLegacyTags: true });
stubOctokitReturnData('issues.createComment', {
data: { id: newCommentId, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
});
stubOctokitReturnData('issues.listComments', { data: [] });

await addReleasePlanComment(terraformChangedModules, terraformModuleNamesToRemove, {
status: WikiStatus.SUCCESS,
});

expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('**⚠️ The following modules no longer exist in source but have tags/releases.'),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('They will be automatically deleted.'),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('- `aws/module1`'),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('- `aws/module2`'),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('- `gcp/module3`'),
}),
);
});

it('should handle wiki failure status with error message', async () => {
const newCommentId = 12345;
const errorMessage = 'Repository does not have wiki enabled';
stubOctokitReturnData('issues.createComment', {
data: { id: newCommentId, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
});
stubOctokitReturnData('issues.listComments', { data: [] });

await addReleasePlanComment([], [], {
status: WikiStatus.FAILURE,
errorMessage,
});

expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('**⚠️ Failed to checkout wiki:**'),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('```'),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining(errorMessage),
}),
);
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('Please consult the [README.md]'),
}),
);
});

it('should exclude branding when disabled', async () => {
const newCommentId = 12345;
config.set({ disableBranding: true });
stubOctokitReturnData('issues.createComment', {
data: { id: newCommentId, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
});
stubOctokitReturnData('issues.listComments', { data: [] });

await addReleasePlanComment(terraformChangedModules, [], { status: WikiStatus.SUCCESS });

const createCommentCalls = vi.mocked(context.octokit.rest.issues.createComment).mock.calls;
expect(createCommentCalls.length).toBeGreaterThanOrEqual(1);

// Get the comment body text from the first call
const commentBody = createCommentCalls[0]?.[0]?.body as string;

// Ensure branding is not included
expect(commentBody).not.toContain(BRANDING_COMMENT);
});

it('should handle different wiki statuses', async () => {
const cases = [
{
status: WikiStatus.SUCCESS,
expectedContent: '✅ Wiki Check',
expectedContent: '✅ Enabled',
},
{
status: WikiStatus.FAILURE,
errorMessage: 'Failed to clone',
expectedContent: '⚠️ Wiki Check: Failed to checkout wiki.',
expectedContent: '**⚠️ Failed to checkout wiki:**',
},
{
status: WikiStatus.DISABLED,
expectedContent: '🚫 Wiki Check: Generation is disabled',
expectedContent: '🚫 Wiki generation **disabled** via `disable-wiki` flag.',
},
];

Expand Down
2 changes: 1 addition & 1 deletion __tests__/terraform-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ vi.mock('node:util', () => ({
}));

describe('terraform-docs', async () => {
const terraformDocsVersion = 'v0.19.0';
const terraformDocsVersion = 'v0.20.0';
const mockExecFileSync = vi.mocked(execFileSync);
const mockWhichSync = vi.mocked(which.sync);
const fsExistsSyncMock = vi.mocked(existsSync);
Expand Down
Loading