Skip to content

Commit

Permalink
✨feature: option to update existing comment #44 (#49)
Browse files Browse the repository at this point in the history
* ✨feature: option to update existing comment #44

* ✨feature: add comment footer

* 📄 docs: update

* 🐞 fix: add job name for workflow
  • Loading branch information
maxisam committed Oct 10, 2023
1 parent 7ca60ca commit abb1b9b
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 89 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test-dotnet-format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:

jobs:
dotnet-format:
name: "🧪 Test dotnet format"
runs-on: ubuntu-latest
steps:
- name: checkout
Expand All @@ -29,6 +30,7 @@ jobs:
run: echo "${{ steps.dotnet-format.outputs.output }}"

dotnet-format-config:
name: "🧪 Test dotnet-format with config"
runs-on: ubuntu-latest
steps:
- name: checkout
Expand Down
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,21 @@ Yet another dotnet format. It combines dotnet format with jscpd to provide a sin
- Generate reports as comment on PR
- Generate reports as workflow summary
- (optional) commit changes
- (optional) update existing PR comment

## Demo

- generate report as comment https://github.com/maxisam/dotnet-format-plus/pull/48
- Workflow summary
<img width="1076" alt="image" src="https://github.com/maxisam/dotnet-format-plus/assets/456807/d1c3e659-b9f3-4969-a752-054739b7920b">

- Annotation

<img width="567" alt="image" src="https://github.com/maxisam/dotnet-format-plus/assets/456807/87de99ae-a860-46f3-9987-d692df0aaf37">
- generate report as comment

<img width="712" alt="image" src="https://github.com/maxisam/dotnet-format-plus/assets/456807/085a4e5f-61e0-4561-a00a-bf5e26c8a2da">

- Workflow summary

<img width="1108" alt="image" src="https://github.com/maxisam/dotnet-format-plus/assets/456807/1ae6b0c3-fd22-4ecd-9330-78ccf18aa9ef">

- Annotation

<img width="567" alt="image" src="https://github.com/maxisam/dotnet-format-plus/assets/456807/87de99ae-a860-46f3-9987-d692df0aaf37">

## Usage

Expand All @@ -46,3 +51,5 @@ This project is based on / inspired by lots of other projects, including but not
- https://github.com/kucherenko/jscpd

- https://github.com/getunlatch/jscpd-github-action

- https://github.com/bibipkins/dotnet-test-reporter
8 changes: 8 additions & 0 deletions __tests__/dotnet/ConfigConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@
Console.WriteLine("Hello, World!");
var test = "test";
string test2 = null;
console.log(test2);
console.log(test2);
console.log(test2);
console.log(test2);
console.log(test2);
console.log(test2);
console.log(test2);
console.log(test2);
console.log(test2);
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ inputs:
required: false
default: "false"

postNewComment:
description: "Post a new comment on the pull request or update an existing one with the results"
required: false
default: "false"

outputs:
hasChanges:
description: "A value indicating if any files were formatted. If `action` is `check` we will inspect the exit code of `dotnet format` to be <> 0. If `action` is `fix` we see if there are changes to files with `git status -s`."
Expand Down
148 changes: 104 additions & 44 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export function getInputs(): IInputs {
dotnetFormatConfigPath: core.getInput(INPUTS.dotnetFormatConfigPath),
jscpdConfigPath: core.getInput(INPUTS.jscpdConfigPath),
jscpdCheck: core.getInput(INPUTS.jscpdCheck) === 'true',
jscpdCheckAsError: core.getInput(INPUTS.jscpdCheckAsError) === 'true'
jscpdCheckAsError: core.getInput(INPUTS.jscpdCheckAsError) === 'true',
postNewComment: core.getInput(INPUTS.postNewComment) === 'true'
};
core.debug(`Inputs: ${inspect(inputs)}`);
return inputs;
Expand Down Expand Up @@ -77,3 +78,10 @@ export function formatOnlyChangedFiles(onlyChangedFiles: boolean): boolean {
onlyChangedFiles && core.warning('Formatting only changed files is available on the issue_comment and pull_request events only');
return false;
}

export function getReportFooter(): string {
const commit = context.payload?.pull_request?.head?.sha || context.sha;
const commitLink = `[${commit.substring(0, 7)}](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${commit})`;
const workflowLink = `[Workflow](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
return commit ? `<br/>_✏️ updated for commit ${commitLink} by ${workflowLink}_ \n\n` : '';
}
8 changes: 4 additions & 4 deletions src/dotnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { context } from '@actions/github';
import * as fs from 'fs';
import path from 'path';
import { inspect } from 'util';
import { REPORT_PATH, formatOnlyChangedFiles } from './common';
import { REPORT_PATH, formatOnlyChangedFiles, getReportFooter } from './common';
import { execute } from './execute';
import { FormatResult, FormatType, IDotnetFormatArgs, IDotnetFormatConfig, ReportItem } from './modals';

Expand Down Expand Up @@ -91,11 +91,11 @@ export function getReportFiles(): string[] {
// check if file size is greater than 2 bytes to avoid empty report
return reportPaths.filter(p => fs.existsSync(p) && fs.statSync(p).size > 2);
}
function reportHeader(workspace: string): string {
export function getReportHeader(workspace: string): string {
return `## ✅ DOT NET FORMAT - ${workspace}`;
}

export function generateReport(reports: string[], workspace: string): string {
export function generateReport(reports: string[], header: string): string {
let markdownReport = '';
for (const report of reports) {
// get file name from report path without extension
Expand All @@ -106,7 +106,7 @@ export function generateReport(reports: string[], workspace: string): string {
if (!markdownReport) {
return '';
}
return `${reportHeader(workspace)}\n\n ${markdownReport}`;
return `${header}\n\n ${markdownReport}\n\n ${getReportFooter()}`;
}

export async function nugetRestore(nugetConfigPath: string, workspace: string): Promise<boolean> {
Expand Down
29 changes: 22 additions & 7 deletions src/duplicated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Octokit } from '@octokit/rest';
import * as fs from 'fs';
import { detectClones } from 'jscpd';
import { inspect } from 'util';
import { getReportFooter } from './common';
import { execute } from './execute';
import * as git from './git';
import { IJsonReport } from './modals';
Expand All @@ -18,6 +19,7 @@ export async function duplicatedCheck(
workspace: string,
jscpdConfigPath: string,
jscpdCheckAsError: boolean,
postNewComment: boolean,
githubClient: InstanceType<typeof Octokit>
): Promise<void> {
const cwd = process.cwd();
Expand All @@ -28,7 +30,7 @@ export async function duplicatedCheck(
const reportFiles = getReportFiles(cwd);
const markdownReport = reportFiles.find(file => file.endsWith('.md')) as string;
const jsonReport = reportFiles.find(file => file.endsWith('.json')) as string;
const message = await postReport(githubClient, markdownReport, clones, workspace);
const message = await postReport(githubClient, markdownReport, clones, workspace, postNewComment);
fs.writeFileSync(markdownReport, message);
await git.UploadReportToArtifacts([markdownReport, jsonReport], REPORT_ARTIFACT_NAME);
const isOverThreshold = checkThreshold(jsonReport, options.threshold || 0);
Expand Down Expand Up @@ -88,12 +90,20 @@ function showAnnotation(clones: IClone[], cwd: string, isError: boolean): void {
}
}

function reportHeader(workspace: string): string {
function getReportHeader(workspace: string): string {
return `## ❌ DUPLICATED CODE FOUND - ${workspace}`;
}

async function postReport(githubClient: InstanceType<typeof Octokit>, markdownReport: string, clones: IClone[], workspace: string): Promise<string> {
const report = fs.readFileSync(markdownReport, 'utf8');
async function postReport(
githubClient: InstanceType<typeof Octokit>,
markdownReport: string,
clones: IClone[],
workspace: string,
postNewComment: boolean
): Promise<string> {
let report = fs.readFileSync(markdownReport, 'utf8');
// remove existing header
report = report.replace('# Copy/paste detection report', '');
const cwd = process.cwd();
let markdown = '<details>\n';
markdown += ` <summary> JSCPD Details </summary>\n\n`;
Expand All @@ -104,11 +114,16 @@ async function postReport(githubClient: InstanceType<typeof Octokit>, markdownRe
markdown += '\n';
}
markdown += '</details>\n';
let message = `${reportHeader(workspace)} \n\n${report}\n\n ${markdown}`;
const header = getReportHeader(workspace);
const message = `${header} \n\n${report}\n\n ${markdown}\n\n ${getReportFooter()}`;
await git.setSummary(message);
message += `\n\n[Workflow Runner](${git.getActionRunLink()})`;
if (context.eventName === 'pull_request') {
await git.comment(githubClient, message);
const existingCommentId = await git.getExistingCommentId(githubClient, header);
if (!postNewComment && existingCommentId) {
await git.updateComment(githubClient, existingCommentId, message);
} else {
await git.comment(githubClient, message);
}
}
return message;
}
Expand Down
20 changes: 15 additions & 5 deletions src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function format(inputs: IInputs, githubClient: InstanceType<typeof
const reportFiles = dotnet.getReportFiles();
await git.UploadReportToArtifacts(reportFiles, REPORT_ARTIFACT_NAME);
const isDryRun = checkIsDryRun(configOptions);
const isReportPosted = await postReport(reportFiles, githubClient, inputs.workspace);
const isReportPosted = await postReport(reportFiles, githubClient, inputs.workspace, inputs.postNewComment);
const isReportRemoved = isReportPosted && (await Common.RemoveReportFiles());
const isChanged = isReportRemoved && (await git.checkIsFileChanged());
setOutput(isDryRun, isChanged);
Expand All @@ -44,13 +44,23 @@ export async function format(inputs: IInputs, githubClient: InstanceType<typeof
return finalFormatResult;
}

async function postReport(reportFiles: string[], githubClient: InstanceType<typeof Octokit>, workspace: string): Promise<boolean> {
async function postReport(
reportFiles: string[],
githubClient: InstanceType<typeof Octokit>,
workspace: string,
postNewComment: boolean
): Promise<boolean> {
if (reportFiles.length) {
let message = dotnet.generateReport(reportFiles, workspace);
const header = dotnet.getReportHeader(workspace);
const message = dotnet.generateReport(reportFiles, header);
await git.setSummary(message);
message += `\n\n[Workflow Runner](${git.getActionRunLink()})`;
if (context.eventName === 'pull_request') {
return await git.comment(githubClient, message);
const existingCommentId = await git.getExistingCommentId(githubClient, header);
if (!postNewComment && existingCommentId) {
return await git.updateComment(githubClient, existingCommentId, message);
} else {
return await git.comment(githubClient, message);
}
}
}
return true;
Expand Down
71 changes: 53 additions & 18 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,6 @@ export async function getPullRequestFiles(githubClient: InstanceType<typeof Octo
.map(file => file.filename);
}

export async function comment(githubClient: InstanceType<typeof Octokit>, message: string): Promise<boolean> {
const { owner, repo, number } = context.issue;
if (!number) {
throw new Error('Unable to get pull request number from action event');
}
core.info(`Commenting on PR #${number}`);
const resp = await githubClient.rest.issues.createComment({
owner,
repo,
issue_number: number,
body: message
});
resp.status === 201 ? core.info('Commented on PR') : core.error(`Failed to comment on PR. Response: ${resp}`);
return resp.status === 201;
}

export async function init(workspace: string, username: string, email: string): Promise<boolean> {
try {
core.info('Configuring git…');
Expand Down Expand Up @@ -158,6 +142,57 @@ export async function setSummary(text: string): Promise<void> {
await core.summary.addRaw(text).write();
}

export function getActionRunLink(): string {
return `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
async function tryGetUserLogin(octokit: Octokit): Promise<string | undefined> {
try {
const username = await octokit.rest.users.getAuthenticated();
return username.data?.login;
} catch {
core.warning('⚠️ Failed to get username without user scope, will check comment with user type instead');
// when token doesn't have user scope
return undefined;
}
}

export async function getExistingCommentId(octokit: Octokit, header: string): Promise<number | undefined> {
const comments = await octokit.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const userLogin = await tryGetUserLogin(octokit);
const existingComment = comments.data?.find(c => {
const isBotUserType = c.user?.type === 'Bot' || c.user?.login === userLogin;
const startsWithHeader = c.body?.startsWith(header);
return isBotUserType && startsWithHeader;
});
return existingComment?.id;
}

export async function comment(githubClient: InstanceType<typeof Octokit>, message: string): Promise<boolean> {
const { owner, repo, number } = context.issue;
if (!number) {
throw new Error('Unable to get pull request number from action event');
}
core.info(`Commenting on PR #${number}`);
const resp = await githubClient.rest.issues.createComment({
owner,
repo,
issue_number: number,
body: message
});
resp.status === 201 ? core.info('Commented on PR') : core.error(`Failed to comment on PR. Response: ${resp}`);
return resp.status === 201;
}

export async function updateComment(githubClient: InstanceType<typeof Octokit>, commentId: number, body: string): Promise<boolean> {
const { owner, repo, number } = context.issue;
core.info(`♻️ Updating comment #${commentId} on PR #${number}`);
const resp = await githubClient.rest.issues.updateComment({
owner,
repo,
comment_id: commentId,
body
});
resp.status === 200 ? core.info('Comment updated') : core.error(`Failed to update comment. Response: ${resp}`);
return resp.status === 200;
}
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ async function run(): Promise<boolean> {
const finalFormatResult = await format(inputs, githubClient);
inputs.problemMatcherEnabled && removeProblemMatcher();
if (inputs.jscpdCheck) {
await duplicatedCheck(inputs.workspace, inputs.jscpdConfigPath, inputs.jscpdCheckAsError, githubClient);
await duplicatedCheck(inputs.workspace, inputs.jscpdConfigPath, inputs.jscpdCheckAsError, inputs.postNewComment, githubClient);
}
if (!finalFormatResult && inputs.failFast) {
core.setFailed(`Action failed with format issue`);
Expand Down
4 changes: 3 additions & 1 deletion src/modals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export enum INPUTS {
jscpdConfigPath = 'jscpdConfigPath',
jscpdCheckAsError = 'jscpdCheckAsError',
problemMatcherEnabled = 'problemMatcherEnabled',
skipCommit = 'skipCommit'
skipCommit = 'skipCommit',
postNewComment = 'postNewComment'
}

export interface IInputs {
Expand All @@ -40,6 +41,7 @@ export interface IInputs {
jscpdCheck: boolean;
jscpdConfigPath: string;
jscpdCheckAsError: boolean;
postNewComment: boolean;
}

export type FixLevelType = 'error' | 'info' | 'warn';
Expand Down

0 comments on commit abb1b9b

Please sign in to comment.