Skip to content

Commit 3ec1340

Browse files
authored
feat: add GHES compatibility, update terraform-docs to v0.20.0, and improve utility functions (#208)
* build(deps): update textlint to version 14.7.1 and add terraform-docs-version script * chore: update terraformDocsVersion to v0.20.0 in config and tests * refactor: remove commented-out code in removeDirectoryContents function Fixes: https://sonarcloud.io/project/issues?issueStatuses=OPEN%2CCONFIRMED&id=terraform-module-releaser&open=AZOqFkSEBpiYhJtwX4f1 * chore: update octokit dependencies to latest versions * chore: remove empty CHANGELOG.md file * chore: update @types/node to version 22.15.29 * feat: enhance changelog generation with GitHub Models API and update workflows * feat: add support for GitHub Enterprise Server - Add proper handling for GHES custom API endpoints via environment variables - Enhance initializeContext to include API URL detection for GHES environments - Improve wiki checkout process for GHES custom domain configurations - Update formatModuleSource to handle SSH and HTTPS conversions for GHES URLs - Fix automated tag cleanup logic to work with GHES repositories - Add changelog sections to PR comments with GHES compatibility - Update tests to support GHES functionality and edge cases - Add comprehensive documentation for GHES configuration and usage - Small tweaks to improve branding clarity on mobile
1 parent 5b36a9c commit 3ec1340

24 files changed

+1095
-1336
lines changed

.github/scripts/changelog.js

Lines changed: 255 additions & 93 deletions
Large diffs are not rendered by default.

.github/workflows/release-start.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ jobs:
6262
uses: actions/github-script@v7
6363
id: changelog
6464
env:
65-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66-
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
65+
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_READ_AND_MODELS }}
6766
with:
6867
result-encoding: json
6968
script: |

CHANGELOG.md

Whitespace-only changes.

README.md

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ maintains independence while living in the same repository, with proper isolatio
2323
Additionally, the action generates a beautifully crafted wiki for each module, complete with readme information, usage
2424
examples, Terraform-docs details, and a full changelog.
2525

26+
Compatible with both GitHub.com and GitHub Enterprise Server (GHES) – works seamlessly in cloud and on-premises
27+
environments.
28+
2629
## 🚀 Features
2730

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

7174
## Getting Started
7275

73-
### Step 1: Ensure GitHub Wiki is Enabled
76+
### Step 1: Enable and Initialize GitHub Wiki
77+
78+
Before using this action, you'll need to enable the wiki feature for your repository:
7479

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

77-
1. Go to your repository's homepage.
78-
1. Navigate to the **Settings** tab.
79-
1. Under the **Features** section, ensure the **Wikis** option is checked to enable the GitHub Wiki.
80-
1. Navigate to the **Wiki** tab on your repository.
81-
1. Click the **Create the first page** button and add a basic title like **Home** to initialize the wiki with an initial
82-
commit.
83-
1. Save the changes to ensure your wiki is not empty when the GitHub Action updates it with module information.
88+
> This initialization step is necessary because GitHub doesn't provide an API to programmatically enable or initialize
89+
> the wiki.
8490
8591
### Step 2: Configure the Action
8692

@@ -116,6 +122,22 @@ reasonably configured.
116122
117123
If you need to customize additional parameters, please refer to [Input Parameters](#input-parameters) section below.
118124
125+
## GitHub Enterprise Server (GHES) Support
126+
127+
This action is fully compatible with GitHub Enterprise Server deployments:
128+
129+
- **Automatic Detection**: The action automatically detects when running on GHES and adjusts API endpoints accordingly
130+
- **Wiki Generation**: Full wiki support works on GHES instances with wiki features enabled
131+
- **Release Management**: Creates releases and tags using your GHES instance's API
132+
- **No Additional Configuration**: Works out-of-the-box on GHES without requiring special configuration
133+
- **SSH Source Format**: Use the use-ssh-source-format parameter for GHES environments that prefer SSH-based Git URLs
134+
135+
### GHES Requirements
136+
137+
- GitHub Enterprise Server version that supports GitHub Actions
138+
- Wiki feature enabled on your GHES instance (contact your administrator if wikis are disabled)
139+
- Appropriate permissions for the GitHub Actions runner to access repository features
140+
119141
## Permissions
120142
121143
Before executing the GitHub Actions workflow, ensure that you have the necessary permissions set for accessing pull
@@ -360,7 +382,7 @@ by Piotr Krukowski.
360382
- **100% GitHub-based**: This action has no external dependencies, eliminating the need for additional authentication
361383
and complexity. Unlike earlier variations that stored built module assets in external services like Amazon S3, this
362384
action keeps everything within GitHub, providing a self-contained and streamlined solution for managing Terraform
363-
modules.
385+
modules. Works seamlessly with both GitHub.com and GitHub Enterprise Server environments.
364386
- **Pull Request-based workflow**: This action runs on the pull_request event, using pull request comments to track
365387
permanent releases tied to commits. This method ensures persistence, unlike Action Artifacts, which expire. As a
366388
result, the module does not support non-PR workflows, such as direct pushes to the default branch.

__mocks__/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const defaultConfig: Config = {
1616
minorKeywords: ['feat', 'feature'],
1717
patchKeywords: ['fix', 'chore'],
1818
defaultFirstTag: 'v1.0.0',
19-
terraformDocsVersion: 'v0.19.0',
19+
terraformDocsVersion: 'v0.20.0',
2020
deleteLegacyTags: false,
2121
disableWiki: false,
2222
wikiSidebarChangelogMax: 10,

__tests__/context.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,34 @@ describe('context', () => {
177177
});
178178
expect(getContext().prBody).toEqual('');
179179
});
180+
181+
it('should use custom GITHUB_API_URL when provided', () => {
182+
const customApiUrl = 'https://github.example.com/api/v3';
183+
vi.stubEnv('GITHUB_API_URL', customApiUrl);
184+
185+
// Clear context to force reinitialization
186+
clearContextForTesting();
187+
188+
const context = getContext();
189+
190+
// Check that the context was created (which means the custom API URL was used)
191+
expect(context).toBeDefined();
192+
expect(context.octokit).toBeDefined();
193+
});
194+
195+
it('should use default GITHUB_API_URL when not provided', () => {
196+
// Ensure GITHUB_API_URL is not set to test the default fallback
197+
vi.stubEnv('GITHUB_API_URL', undefined);
198+
199+
// Clear context to force reinitialization
200+
clearContextForTesting();
201+
202+
const context = getContext();
203+
204+
// Check that the context was created with default API URL
205+
expect(context).toBeDefined();
206+
expect(context.octokit).toBeDefined();
207+
});
180208
});
181209

182210
describe('context proxy', () => {
@@ -185,14 +213,14 @@ describe('context', () => {
185213
const getterRepo = getContext().repo;
186214
expect(proxyRepo).toEqual(getterRepo);
187215
expect(startGroup).toHaveBeenCalledWith('Initializing Context');
188-
expect(info).toHaveBeenCalledTimes(9);
216+
expect(info).toHaveBeenCalledTimes(11);
189217

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

194222
// Second access should not trigger initialization
195-
const prNumber = context.prNumber;
223+
const prNumber = context.prNumber; // Intentionally access a property with no usage
196224
expect(startGroup).not.toHaveBeenCalled();
197225
expect(info).not.toHaveBeenCalled();
198226
});

__tests__/fixtures/_Footer.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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>
1+
<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>

__tests__/pull-request.test.ts

Lines changed: 151 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -444,16 +444,12 @@ describe('pull-request', () => {
444444

445445
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
446446
expect.objectContaining({
447-
body: expect.stringContaining(
448-
'**Note**: The following Terraform modules no longer exist in source; however, corresponding tags/releases exist.',
449-
),
447+
body: expect.stringContaining('**⚠️ The following module no longer exists in source but has tags/releases.'),
450448
}),
451449
);
452450
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
453451
expect.objectContaining({
454-
body: expect.stringContaining(
455-
'Automation tag/release deletion is **enabled** and corresponding tags/releases will be automatically deleted.<br>',
456-
),
452+
body: expect.stringContaining('It will be automatically deleted.'),
457453
}),
458454
);
459455

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

465461
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
466462
expect.objectContaining({
467-
body: expect.stringContaining(
468-
'**Note**: The following Terraform modules no longer exist in source; however, corresponding tags/releases exist.',
469-
),
470-
}),
471-
);
472-
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
473-
expect.objectContaining({
474-
body: expect.stringContaining(
475-
'Automation tag/release deletion is **disabled** — **no** subsequent action will take place.<br>',
476-
),
463+
body: expect.stringContaining('⏸️ Existing tags and releases will be **preserved**'),
477464
}),
478465
);
479466
});
@@ -503,37 +490,180 @@ describe('pull-request', () => {
503490
);
504491
});
505492

506-
it('should include modules to remove when specified', async () => {
493+
it('should include modules to remove when flag enabled', async () => {
494+
const modulesToRemove = ['legacy-module1', 'legacy-module2'];
495+
496+
stubOctokitReturnData('issues.createComment', {
497+
data: { id: 1, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
498+
});
499+
stubOctokitReturnData('issues.listComments', { data: [] });
500+
config.set({ deleteLegacyTags: true });
501+
502+
await addReleasePlanComment([], modulesToRemove, { status: WikiStatus.SUCCESS });
503+
504+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
505+
expect.objectContaining({
506+
body: expect.stringContaining('- `legacy-module1`'),
507+
}),
508+
);
509+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
510+
expect.objectContaining({
511+
body: expect.stringContaining('- `legacy-module2`'),
512+
}),
513+
);
514+
});
515+
516+
it('should not include modules to remove when flag disabled ', async () => {
507517
const modulesToRemove = ['legacy-module1', 'legacy-module2'];
508518

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

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

527+
const createCommentCalls = vi.mocked(context.octokit.rest.issues.createComment).mock.calls;
528+
expect(createCommentCalls.length).toBeGreaterThanOrEqual(1);
529+
530+
// Get the comment body text from the first call
531+
const commentBody = createCommentCalls[0]?.[0]?.body as string;
532+
533+
// Ensure both modules are not included in the body
534+
expect(commentBody).not.toContain('`legacy-module1`');
535+
expect(commentBody).not.toContain('`legacy-module2`');
536+
expect(commentBody).toContain('⏸️ Existing tags and releases will be **preserved**');
537+
});
538+
539+
it('should handle cleanup when delete-legacy-tags is enabled but no modules to remove', async () => {
540+
const newCommentId = 12345;
541+
config.set({ deleteLegacyTags: true });
542+
stubOctokitReturnData('issues.createComment', {
543+
data: { id: newCommentId, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
544+
});
545+
stubOctokitReturnData('issues.listComments', { data: [] });
546+
547+
await addReleasePlanComment(terraformChangedModules, [], { status: WikiStatus.SUCCESS });
548+
516549
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
517550
expect.objectContaining({
518-
body: expect.stringContaining('`legacy-module1`, `legacy-module2`'),
551+
body: expect.stringContaining(
552+
'✅ All tags and releases are synchronized with the codebase. No cleanup required.',
553+
),
519554
}),
520555
);
521556
});
522557

558+
it('should handle multiple modules to remove with plural warning message', async () => {
559+
const newCommentId = 12345;
560+
const terraformModuleNamesToRemove = ['aws/module1', 'aws/module2', 'gcp/module3'];
561+
config.set({ deleteLegacyTags: true });
562+
stubOctokitReturnData('issues.createComment', {
563+
data: { id: newCommentId, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
564+
});
565+
stubOctokitReturnData('issues.listComments', { data: [] });
566+
567+
await addReleasePlanComment(terraformChangedModules, terraformModuleNamesToRemove, {
568+
status: WikiStatus.SUCCESS,
569+
});
570+
571+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
572+
expect.objectContaining({
573+
body: expect.stringContaining('**⚠️ The following modules no longer exist in source but have tags/releases.'),
574+
}),
575+
);
576+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
577+
expect.objectContaining({
578+
body: expect.stringContaining('They will be automatically deleted.'),
579+
}),
580+
);
581+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
582+
expect.objectContaining({
583+
body: expect.stringContaining('- `aws/module1`'),
584+
}),
585+
);
586+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
587+
expect.objectContaining({
588+
body: expect.stringContaining('- `aws/module2`'),
589+
}),
590+
);
591+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
592+
expect.objectContaining({
593+
body: expect.stringContaining('- `gcp/module3`'),
594+
}),
595+
);
596+
});
597+
598+
it('should handle wiki failure status with error message', async () => {
599+
const newCommentId = 12345;
600+
const errorMessage = 'Repository does not have wiki enabled';
601+
stubOctokitReturnData('issues.createComment', {
602+
data: { id: newCommentId, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
603+
});
604+
stubOctokitReturnData('issues.listComments', { data: [] });
605+
606+
await addReleasePlanComment([], [], {
607+
status: WikiStatus.FAILURE,
608+
errorMessage,
609+
});
610+
611+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
612+
expect.objectContaining({
613+
body: expect.stringContaining('**⚠️ Failed to checkout wiki:**'),
614+
}),
615+
);
616+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
617+
expect.objectContaining({
618+
body: expect.stringContaining('```'),
619+
}),
620+
);
621+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
622+
expect.objectContaining({
623+
body: expect.stringContaining(errorMessage),
624+
}),
625+
);
626+
expect(context.octokit.rest.issues.createComment).toHaveBeenCalledWith(
627+
expect.objectContaining({
628+
body: expect.stringContaining('Please consult the [README.md]'),
629+
}),
630+
);
631+
});
632+
633+
it('should exclude branding when disabled', async () => {
634+
const newCommentId = 12345;
635+
config.set({ disableBranding: true });
636+
stubOctokitReturnData('issues.createComment', {
637+
data: { id: newCommentId, html_url: 'https://github.com/org/repo/pull/1#issuecomment-1' },
638+
});
639+
stubOctokitReturnData('issues.listComments', { data: [] });
640+
641+
await addReleasePlanComment(terraformChangedModules, [], { status: WikiStatus.SUCCESS });
642+
643+
const createCommentCalls = vi.mocked(context.octokit.rest.issues.createComment).mock.calls;
644+
expect(createCommentCalls.length).toBeGreaterThanOrEqual(1);
645+
646+
// Get the comment body text from the first call
647+
const commentBody = createCommentCalls[0]?.[0]?.body as string;
648+
649+
// Ensure branding is not included
650+
expect(commentBody).not.toContain(BRANDING_COMMENT);
651+
});
652+
523653
it('should handle different wiki statuses', async () => {
524654
const cases = [
525655
{
526656
status: WikiStatus.SUCCESS,
527-
expectedContent: '✅ Wiki Check',
657+
expectedContent: '✅ Enabled',
528658
},
529659
{
530660
status: WikiStatus.FAILURE,
531661
errorMessage: 'Failed to clone',
532-
expectedContent: '⚠️ Wiki Check: Failed to checkout wiki.',
662+
expectedContent: '**⚠️ Failed to checkout wiki:**',
533663
},
534664
{
535665
status: WikiStatus.DISABLED,
536-
expectedContent: '🚫 Wiki Check: Generation is disabled',
666+
expectedContent: '🚫 Wiki generation **disabled** via `disable-wiki` flag.',
537667
},
538668
];
539669

__tests__/terraform-docs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ vi.mock('node:util', () => ({
4040
}));
4141

4242
describe('terraform-docs', async () => {
43-
const terraformDocsVersion = 'v0.19.0';
43+
const terraformDocsVersion = 'v0.20.0';
4444
const mockExecFileSync = vi.mocked(execFileSync);
4545
const mockWhichSync = vi.mocked(which.sync);
4646
const fsExistsSyncMock = vi.mocked(existsSync);

0 commit comments

Comments
 (0)