Skip to content

Commit 1766830

Browse files
authored
Feature/comment on pr (#95)
* add changelog enforcer * set permissions * test using octokit * fix enforcer * add a comment if changelog fails * try finding comment * log comments * update or add comment * update comment * this should update the comment * tag actor * test enforcer * add latest change via comment * log latestChanges * try adding changes directly * log * try skppig * try without code block * clean up linting and update changelog * update actions
1 parent 1808030 commit 1766830

File tree

11 files changed

+224
-17
lines changed

11 files changed

+224
-17
lines changed

.github/workflows/enforcer.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ permissions:
99
# Give the default GITHUB_TOKEN write permission to commit and push the
1010
# added or changed files to the repository.
1111
contents: write
12-
pull-requests: read
12+
pull-requests: write
1313

1414
jobs:
1515
enforce-changelog:
@@ -26,3 +26,5 @@ jobs:
2626
enable_dependabot: true
2727
dependabot_labels: dependencies
2828
dependabot_section: changed
29+
comment_on_pr: true
30+
show_full_name: true

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,4 +455,22 @@ dependabot_section:
455455
description: The section to put the dependabot changes in. Can be either `security` or `changed`.
456456
default: "security"
457457
required: false
458+
459+
comment_on_pr:
460+
description: |
461+
The bot will leave a comment with a preview of the changes, if the enforce action fails it
462+
will tag the user and let them know the action failed
463+
default: "false"
464+
required: false
465+
466+
show_author_full_name:
467+
description: Show the authors name instead of the authors username.
468+
default: "false"
469+
required: false
470+
471+
name_override:
472+
description: |
473+
If you have a naming that you want to use that is not the github username, then you can
474+
override the github username. Must use a `key=value` pair comma separated.
475+
required: false
458476
```

changelog/pr-comments.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
added:
2+
- message: "Support to add pr comments to preview changes. To enable set `comment_on_pr: true`."
3+
references:
4+
- type: issue
5+
number: 96

enforcer/action.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ inputs:
2121
default: "security"
2222
required: false
2323

24+
show_author_full_name:
25+
description: Show the authors name instead of the authors username.
26+
default: "false"
27+
required: false
28+
29+
name_override:
30+
description: |
31+
If you have a naming that you want to use that is not the github username, then you can
32+
override the github username. Must use a `key=value` pair comma separated.
33+
required: false
34+
35+
comment_on_pr:
36+
description: |
37+
The bot will leave a comment with a preview of the changes, if the enforce action fails it
38+
will tag the user and let them know the action failed
39+
default: "false"
40+
required: false
41+
2442
token:
2543
description: "The secret value from your GITHUB_TOKEN or another token to access the GitHub API. Defaults to the token at `github.token`"
2644
required: true

enforcer/index.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

generate/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/actions/enforcer.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1-
import { debug, getBooleanInput, getInput, setFailed } from "@actions/core";
2-
import { getExecOutput } from "@actions/exec";
1+
import { debug, getBooleanInput, getInput } from "@actions/core";
32
import { context } from "@actions/github";
43
import { exit } from "node:process";
5-
import { generateCommand } from "../lib/generate";
4+
import { type Config, config as baseConfig } from "../lib/config";
65
import { addChangelogDependabot } from "./utils/addChangelogDependabot";
6+
import { compareChangelogs } from "./utils/compareChangelogs";
7+
8+
/**
9+
* @todo Replace with new getKeyValuePairInput inside of github-action-helpers
10+
*
11+
* Format a key value pair to an object.
12+
*
13+
* @param pair the key value pair to turn into an object.
14+
*/
15+
function formatKeyValuePairToObject(pair: string) {
16+
if (!pair) {
17+
return undefined;
18+
}
19+
20+
return pair.split(",").reduce((acc, value) => {
21+
acc[value.split("=")[0]] = value.split("=")[1];
22+
return acc;
23+
}, {} as Record<string, string>);
24+
}
725

826
/**
927
* Run the generate command and check the git diff to see if there are changes
@@ -13,9 +31,16 @@ async function enforceChangelogAction() {
1331
const enableDependabot = getBooleanInput("enable_dependabot", { required: false });
1432
const dependabotLabels = getInput("dependabot_labels").split(",") || [];
1533
const skipLabels = getInput("skip_labels").split(",");
34+
const nameOverrideInput = getInput("name_override", { required: false });
1635
const pullRequest = context.payload.pull_request;
1736
const pullRequestLabels = pullRequest?.labels?.map((label: { name: string }) => label.name) || [];
1837
const set = new Set(pullRequestLabels);
38+
// biome-ignore lint/style/useNamingConvention: Following yaml/toml convention.
39+
const show_author_full_name = getBooleanInput("show_author_full_name", { required: false });
40+
const token = getInput("token");
41+
const customBotName = getInput("custom_bot_name", { required: false });
42+
const nameOverrides = formatKeyValuePairToObject(nameOverrideInput);
43+
const commentOnPr = getBooleanInput("comment_on_pr", { required: false });
1944

2045
if (
2146
enableDependabot && dependabotLabels.some(label => set.has(label))
@@ -28,13 +53,12 @@ async function enforceChangelogAction() {
2853
debug("Skip Enforcing Changelog.");
2954
exit(0);
3055
}
56+
const config: Omit<Config, "repo_url" | "release_url" | "changelog_archive" | "prefers"> = {
57+
...baseConfig,
58+
show_author_full_name,
59+
};
3160

32-
generateCommand("BCL_Bot", context.sha, context.payload.pull_request?.number);
33-
const { stdout } = await getExecOutput("git", ["status", "--porcelain"]);
34-
35-
if (!stdout.match(/CHANGELOG\.md/gi)) {
36-
setFailed("Changelog changes not found.");
37-
}
61+
await compareChangelogs(commentOnPr, token, config, nameOverrides, customBotName);
3862
}
3963

4064
export { enforceChangelogAction };
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { getInput, setFailed } from "@actions/core";
2+
import { context, getOctokit } from "@actions/github";
3+
import { parseChangelog } from "@jelmore1674/changelog";
4+
import { readFileSync } from "node:fs";
5+
import { exit } from "node:process";
6+
import type { Config } from "../../lib/config";
7+
import { generateCommand } from "../../lib/generate";
8+
import { getChangeCount } from "../../utils/getChangeCount";
9+
import { getAuthorName } from "./getAuthorName";
10+
import { getPrNumber } from "./getPrNumber";
11+
12+
const token = getInput("token", { required: false });
13+
14+
async function botCommentOnPr(message: string, prNumber: number, commentId?: number) {
15+
if (commentId) {
16+
await getOctokit(token).rest.issues.updateComment({
17+
...context.repo,
18+
// biome-ignore lint/style/useNamingConvention: Following yaml/toml convention.
19+
comment_id: commentId,
20+
body: message,
21+
});
22+
23+
return;
24+
}
25+
26+
await getOctokit(token).rest.issues.createComment({
27+
// biome-ignore lint/style/useNamingConvention: Following yaml/toml convention.
28+
issue_number: prNumber,
29+
owner: context.repo.owner,
30+
repo: context.repo.repo,
31+
body: message,
32+
});
33+
}
34+
35+
async function compareChangelogs(
36+
commentOnPr: boolean,
37+
token: string,
38+
config: Omit<Config, "repo_url" | "release_url" | "changelog_archive" | "prefers">,
39+
nameOverrides?: Record<string, string>,
40+
customBotName?: string,
41+
) {
42+
const author = await getAuthorName(nameOverrides);
43+
const prNumber = await getPrNumber();
44+
45+
const changelog = readFileSync("CHANGELOG.md", "utf8");
46+
const existingChangelog = getChangeCount(parseChangelog(changelog).versions);
47+
48+
const currentChanges = generateCommand(
49+
author,
50+
context.sha,
51+
prNumber,
52+
undefined,
53+
undefined,
54+
config,
55+
true,
56+
);
57+
58+
const newChangelog = generateCommand(author, context.sha, prNumber);
59+
60+
if (commentOnPr) {
61+
const botNames = ["github-actions[bot]", "build-changelog[bot]"];
62+
63+
if (customBotName) {
64+
botNames.push(customBotName);
65+
}
66+
67+
let exitsingCommentId: number | undefined;
68+
69+
const { data } = await getOctokit(token).rest.issues.listComments({
70+
// biome-ignore lint/style/useNamingConvention: Following yaml/toml convention.
71+
issue_number: prNumber,
72+
owner: context.repo.owner,
73+
repo: context.repo.repo,
74+
});
75+
76+
const foundComment = data.find(i => i?.user?.type === "Bot" && botNames.includes(i.user.login));
77+
if (foundComment) {
78+
exitsingCommentId = foundComment.id;
79+
}
80+
81+
if (existingChangelog === newChangelog.count) {
82+
const failedCommentMessage = `@${context.actor} Don't forget to update your changelog.`;
83+
await botCommentOnPr(failedCommentMessage, prNumber, exitsingCommentId);
84+
setFailed("Changelog changes not found.");
85+
exit(1);
86+
}
87+
88+
await botCommentOnPr(currentChanges.latestChanges, prNumber, exitsingCommentId);
89+
90+
exit(0);
91+
}
92+
93+
if (existingChangelog === newChangelog.count) {
94+
setFailed("Changelog changes not found.");
95+
}
96+
}
97+
98+
export { compareChangelogs };

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ program
1818
.command("generate")
1919
.description("generate the changelog")
2020
.option("--require-changelog", "require that changes have been made to the changelog.")
21-
.action(() => generateCommand("bcl-bot", sha, 1));
21+
.action(() => {
22+
generateCommand("bcl-bot", sha, 1, undefined, undefined, undefined, true);
23+
});
2224

2325
program
2426
.command("notes [version]")

src/lib/generate.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { debug } from "@actions/core";
22
import {
3+
getReleaseNotes,
34
type KeepAChangelogKeywords,
45
parseChangelog,
56
type Reference as ReferenceLink,
@@ -10,6 +11,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1011
import path from "node:path";
1112
import { ParsedChanges, Reference } from "../types";
1213
import { cleanUpChangelog } from "../utils/cleanUpChangelog";
14+
import { getChangeCount } from "../utils/getChangeCount";
1315
import { getParser } from "../utils/getParser";
1416
import { isTomlOrYamlFile } from "../utils/isTomlOrYamlFile";
1517
import { log } from "../utils/log";
@@ -226,6 +228,7 @@ function generateCommand(
226228
releaseVersion?: string,
227229
changelogOptions?: ChangelogOptions,
228230
actionConfig = config as Omit<Config, "repo_url" | "release_url" | "prefers">,
231+
skip_changelog = false,
229232
) {
230233
log("generate command parameters", { author, prNumber, releaseVersion, changelogOptions });
231234

@@ -236,7 +239,7 @@ function generateCommand(
236239
let changelogVersions: Version<Partial<Record<KeepAChangelogKeywords, string[]>>>[] = [];
237240
let changelogLinks: ReferenceLink[] = [];
238241

239-
if (existsSync(changelogPath)) {
242+
if (!skip_changelog && existsSync(changelogPath)) {
240243
const changelogFile = readFileSync(changelogPath, { encoding: "utf8" });
241244
const changelog = parseChangelog(changelogFile, releaseVersion);
242245
changelogVersions = changelog.versions;
@@ -430,15 +433,25 @@ function generateCommand(
430433
changelogOptions,
431434
);
432435

436+
const latestChanges = getReleaseNotes(renderedChangelog).replace("# What's Changed\n\n", "");
437+
433438
debug(renderedChangelog);
434439

435-
writeFileSync(changelogPath, renderedChangelog, { encoding: "utf8" });
440+
if (!skip_changelog) {
441+
writeFileSync(changelogPath, renderedChangelog, { encoding: "utf8" });
442+
443+
cleanUpChangelog(actionConfig.dir);
444+
}
436445

437446
log("CHANGELOG.md finished writing.");
438447

439-
cleanUpChangelog(actionConfig.dir);
448+
const count = getChangeCount(sortedVersions);
440449

441450
rl.close();
451+
return {
452+
count,
453+
latestChanges,
454+
};
442455
}
443456

444457
export { generateCommand, parseChanges };

0 commit comments

Comments
 (0)