diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b6cf24..e5585b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} working-directory: __tests__ terraform-plan-file: ../test-plan.tfplan + comment-title: Terraform Plan in different dir test-action-without-tf-wrapper: # make sure the action works without the terraform wrapper name: Test without TF wrapper @@ -66,6 +67,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} working-directory: __tests__ terraform-plan-file: test-plan.tfplan + comment-title: Terraform Plan without wrapper test-action-at-root: # make sure the action works with terraform dir as root dir name: Test from Root @@ -83,4 +85,5 @@ jobs: - uses: ./ with: github-token: ${{ secrets.GITHUB_TOKEN }} - terraform-plan-file: test-plan.tfplan \ No newline at end of file + terraform-plan-file: test-plan.tfplan + comment-title: Terraform Plan from root \ No newline at end of file diff --git a/README.md b/README.md index 9d0bb95..560d9ef 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ jobs: * `github-token` - (**required**) pass in the GitHub token to make comments on the PR * `working-directory` - (_optional_) the directory of the terraform configuration files (defaults to `.`) * `terraform-plan-file` - (**required**) Filename of the terraform plan (relative to `working-directory`) +* `comment-title` - (_optional_) Title for the comment this action will make on your pull request (defaults to `Terraform Plan`) + +**note**: the `comment-title` is used to determine which PR comment to update. +For instance if you have two of these actions in one PR with the same `comment-title` then they will both try to update the same comment. ## Output This action will create a comment on your PR like: @@ -53,7 +57,9 @@ GitHub Actions will run the entry point from the action.yml. In our case, that happens to be /dist/index.js. Actions run from GitHub repos. -We don't want to check in node_modules. Hence, we package the app using `yarn run pack`. +We don't want to check in node_modules. +Hence, we package the app using `yarn run pack`. +Make sure you run `yarn run pack` before committing/pushing. ### Modifying Source Code Just run `yarn install` locally. diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 92fc427..d39f0c5 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -8,6 +8,18 @@ const pr: PullRequest = { body: '', html_url: '' } +const basicTestPlan: TerraformPlan = { + resource_changes: [ + { + address: 'local_file.fake_file', + type: 'local_file', + name: 'fake_file', + change: { + actions: [Action.delete, Action.create] + } + } + ] +} process.env['GITHUB_REPOSITORY'] = 'https://github.com/byu-oit/github-action-tf-plan-comment' test('Test Basic Plan Summary', async () => { @@ -17,20 +29,13 @@ test('Test Basic Plan Summary', async () => { html_url: 'https://test/workflow/url' }) - const commenter = new PlanCommenter(github.getOctokit('fake'), 1234, pr) - const plan: TerraformPlan = { - resource_changes: [ - { - address: 'local_file.fake_file', - type: 'local_file', - name: 'fake_file', - change: { - actions: [Action.delete, Action.create] - } - } - ] - } - const summary = await commenter.planSummaryBody(plan) + const commenter = new PlanCommenter({ + octokit: github.getOctokit('fake'), + runId: 1234, + pr + }) + + const summary = await commenter.planSummaryBody(basicTestPlan) const expected = `## Terraform Plan: will **replace (delete then create)** 1 resource: * local_file - fake_file @@ -42,10 +47,39 @@ will **replace (delete then create)** 1 resource: }) test('Test Empty Plan Summary', async () => { - const commenter = new PlanCommenter(github.getOctokit('fake'), 1234, pr) + const commenter = new PlanCommenter({ + octokit: github.getOctokit('fake'), + runId: 1234, + pr + }) const emptyPlan: TerraformPlan = {} const summary = await commenter.planSummaryBody(emptyPlan) const expected = `## Terraform Plan: No changes detected` expect(summary).toEqual(expected) }) + +test('Test Basic Plan with custom title', async () => { + const scope = nock('https://api.github.com') + .get(/.*/) + .reply(200, { + html_url: 'https://test/workflow/url' + }) + + const commenter = new PlanCommenter({ + octokit: github.getOctokit('fake'), + runId: 1234, + pr, + commentTitle: 'Test Plan Title' + }) + + const summary = await commenter.planSummaryBody(basicTestPlan) + const expected = `## Test Plan Title: +will **replace (delete then create)** 1 resource: + * local_file - fake_file + +[see details](https://test/workflow/url)` + expect(summary).toEqual(expected) + + scope.done() +}) diff --git a/action.yml b/action.yml index 4cde159..3aa5090 100644 --- a/action.yml +++ b/action.yml @@ -12,6 +12,10 @@ inputs: required: false description: Directory of the terraform configuration default: . + comment-title: + required: false + description: Title of the comment + default: Terraform Plan runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index f34edff..084fad9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1740,13 +1740,12 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.PlanCommenter = exports.noChangesComment = exports.commentPrefix = void 0; +exports.PlanCommenter = exports.noChangesComment = void 0; const core = __importStar(__webpack_require__(470)); const exec = __importStar(__webpack_require__(986)); const github = __importStar(__webpack_require__(469)); const types_1 = __webpack_require__(251); -exports.commentPrefix = '## Terraform Plan:'; -exports.noChangesComment = '## Terraform Plan:\nNo changes detected'; +exports.noChangesComment = 'No changes detected'; async function run() { try { core.debug('got inside the action'); @@ -1758,6 +1757,7 @@ async function run() { core.debug('got pull request'); const planFileName = core.getInput('terraform-plan-file'); const workingDir = core.getInput('working-directory'); + const commentTitle = core.getInput('comment-title'); const json = await jsonFromPlan(workingDir, planFileName); const terraformPlan = JSON.parse(json); core.debug('successfully parsed json'); @@ -1767,7 +1767,12 @@ async function run() { core.setFailed('No GITHUB_RUN_ID found'); return; } - const commenter = new PlanCommenter(github.getOctokit(token), runId, pr); + const commenter = new PlanCommenter({ + octokit: github.getOctokit(token), + runId, + pr, + commentTitle + }); await commenter.commentWithPlanSummary(terraformPlan); } catch (error) { @@ -1804,10 +1809,12 @@ async function jsonFromPlan(workingDir, planFileName) { return json[0]; } class PlanCommenter { - constructor(octokit, runId, pr) { - this.octokit = octokit; - this.runId = runId; - this.pr = pr; + constructor(options) { + this.octokit = options.octokit; + this.runId = options.runId; + this.pr = options.pr; + this.commentPrefix = + options.commentTitle === undefined ? '## Terraform Plan:' : `## ${options.commentTitle}:`; } async commentWithPlanSummary(terraformPlan) { const body = await this.planSummaryBody(terraformPlan); @@ -1818,7 +1825,8 @@ class PlanCommenter { }); let previousCommentId = null; for (const comment of comments.data) { - if (comment.user.login === 'github-actions[bot]' && comment.body.startsWith(exports.commentPrefix)) { + if (comment.user.login === 'github-actions[bot]' && + comment.body.startsWith(this.commentPrefix)) { previousCommentId = comment.id; } } @@ -1850,7 +1858,7 @@ class PlanCommenter { const toReplace = []; const toUpdate = []; if (!terraformPlan.resource_changes) { - return exports.noChangesComment; + return `${this.commentPrefix}\n${exports.noChangesComment}`; } for (const resourceChange of terraformPlan.resource_changes) { const actions = resourceChange.change.actions; @@ -1878,7 +1886,7 @@ class PlanCommenter { core.debug(`toUpdate: ${toUpdate}`); core.debug(`toReplace: ${toReplace}`); core.debug(`toDelete: ${toDelete}`); - let body = `${exports.commentPrefix}\n`; + let body = `${this.commentPrefix}\n`; body += PlanCommenter.resourcesToChangeSection('create', toCreate); body += PlanCommenter.resourcesToChangeSection('update', toUpdate); body += PlanCommenter.resourcesToChangeSection('**delete**', toDelete); diff --git a/src/main.ts b/src/main.ts index a1813fe..8664474 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,8 +5,7 @@ import {GitHub} from '@actions/github/lib/utils' import {Action, PullRequest, TerraformPlan} from './types' import {ExecOptions} from '@actions/exec' -export const commentPrefix = '## Terraform Plan:' -export const noChangesComment = '## Terraform Plan:\nNo changes detected' +export const noChangesComment = 'No changes detected' async function run(): Promise { try { @@ -21,6 +20,7 @@ async function run(): Promise { const planFileName = core.getInput('terraform-plan-file') const workingDir = core.getInput('working-directory') + const commentTitle = core.getInput('comment-title') const json = await jsonFromPlan(workingDir, planFileName) const terraformPlan: TerraformPlan = JSON.parse(json) @@ -33,7 +33,12 @@ async function run(): Promise { return } - const commenter = new PlanCommenter(github.getOctokit(token), runId, pr) + const commenter = new PlanCommenter({ + octokit: github.getOctokit(token), + runId, + pr, + commentTitle + }) await commenter.commentWithPlanSummary(terraformPlan) } catch (error) { core.setFailed(error.message) @@ -73,15 +78,25 @@ async function jsonFromPlan(workingDir: string, planFileName: string): Promise runId: number pr: PullRequest + commentTitle?: string | undefined +} - constructor(octokit: InstanceType, runId: number, pr: PullRequest) { - this.octokit = octokit - this.runId = runId - this.pr = pr +export class PlanCommenter { + octokit: InstanceType + runId: number + pr: PullRequest + commentPrefix: string + + constructor(options: PlanCommenterOptions) { + this.octokit = options.octokit + this.runId = options.runId + this.pr = options.pr + this.commentPrefix = + options.commentTitle === undefined ? '## Terraform Plan:' : `## ${options.commentTitle}:` } async commentWithPlanSummary(terraformPlan: TerraformPlan): Promise { @@ -93,7 +108,10 @@ export class PlanCommenter { }) let previousCommentId: number | null = null for (const comment of comments.data) { - if (comment.user.login === 'github-actions[bot]' && comment.body.startsWith(commentPrefix)) { + if ( + comment.user.login === 'github-actions[bot]' && + comment.body.startsWith(this.commentPrefix) + ) { previousCommentId = comment.id } } @@ -125,7 +143,7 @@ export class PlanCommenter { const toReplace = [] const toUpdate = [] if (!terraformPlan.resource_changes) { - return noChangesComment + return `${this.commentPrefix}\n${noChangesComment}` } for (const resourceChange of terraformPlan.resource_changes) { @@ -154,7 +172,7 @@ export class PlanCommenter { core.debug(`toReplace: ${toReplace}`) core.debug(`toDelete: ${toDelete}`) - let body = `${commentPrefix}\n` + let body = `${this.commentPrefix}\n` body += PlanCommenter.resourcesToChangeSection('create', toCreate) body += PlanCommenter.resourcesToChangeSection('update', toUpdate) body += PlanCommenter.resourcesToChangeSection('**delete**', toDelete)