Skip to content

Commit 0b5debe

Browse files
authored
refactor: simplify report/format helpers (/simplify pass) (#118)
Quality cleanups from a 4-angle review of the composite-action diff: - report-common.mjs: add buildReportContext(context); both report wrappers (format-report, jscpd-report) now call it instead of hand-building the identical owner/repo/sha/cwd/runId/commit object. - jscpd-report.mjs: buildJscpdMessage takes the pre-computed `header` (like generateReport) instead of recomputing getReportHeader internally; wrapper passes it. - format-args.mjs: collapse buildDefaultOptions' 4 near-identical blocks to a base + spread (only the two real overrides remain); simplify checkIsDryRun's granular check via a `dry()` helper; drop the unused `config` field from planFormat's return; make internal-only buildArgs/FormatType module-private. - comment.mjs: run the independent listComments + getAuthenticated calls with Promise.all (getAuthenticated still degrades to undefined on 403). - action.yml: write the push stderr log to $RUNNER_TEMP instead of the repo tree, and drop the duplicate ::error:: annotation on exec failure (per-command one remains). Skipped (documented as follow-ups): unifying the jscpd config resolution that happens in both the shell runner and the JS threshold read (structural; risky without an e2e run), and the github-script import boilerplate (explicit per-step is clearer). biome check + 33 node:test green; wrappers smoke-tested; bash -n + YAML clean. Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
1 parent 25727c7 commit 0b5debe

8 files changed

Lines changed: 70 additions & 78 deletions

File tree

__tests__/jscpd-report.test.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ describe('isOverThreshold', () => {
3434

3535
describe('buildJscpdMessage', () => {
3636
it('strips the default header, adds links and footer', () => {
37-
const md = buildJscpdMessage('# Copy/paste detection report\n\nsome table', duplicates, '/repo/src', '/repo/src', ctx);
37+
const header = getReportHeader('/repo/src');
38+
const md = buildJscpdMessage('# Copy/paste detection report\n\nsome table', duplicates, header, '/repo/src', ctx);
3839
assert.match(md, /## DUPLICATED CODE FOUND - \/repo\/src/);
3940
assert.doesNotMatch(md, /# Copy\/paste detection report/);
4041
assert.match(md, /<summary> JSCPD Details <\/summary>/);

action.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,10 @@ runs:
213213
echo "formatResult=$format_result" >> "$GITHUB_OUTPUT"
214214
215215
# Infrastructure/runner failures fail unconditionally; formatting findings
216-
# remain gated by the failFast step.
216+
# remain gated by the failFast step. The per-command ::error:: above already
217+
# annotated the failure, so just exit here.
217218
if [ "$exec_failed" = "true" ]; then
218-
echo "::error title=DOTNET FORMAT Check::dotnet format failed to execute"
219+
echo "dotnet format failed to execute (see error above)"
219220
exit 1
220221
fi
221222
@@ -311,11 +312,12 @@ runs:
311312
attempt=1
312313
while [ "$attempt" -le 3 ]; do
313314
echo "Pushing changes… (attempt $attempt of 3)"
315+
ERR_LOG="$RUNNER_TEMP/df-push-err.log"
314316
set +e
315-
push_out=$(git push --porcelain origin "$BRANCH:$BRANCH" 2>push_err.log)
317+
push_out=$(git push --porcelain origin "$BRANCH:$BRANCH" 2>"$ERR_LOG")
316318
set -e
317-
push_err=$(cat push_err.log 2>/dev/null || true)
318-
rm -f push_err.log
319+
push_err=$(cat "$ERR_LOG" 2>/dev/null || true)
320+
rm -f "$ERR_LOG"
319321
if printf '%s' "$push_out" | grep -qE '\[rejected\]|\[remote rejected\]'; then
320322
echo "Updates were rejected; fetching and rebasing onto $BRANCH"
321323
git fetch

scripts/format-args.mjs

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import process from 'node:process';
1010
/** Mirrors REPORT_PATH from src/common.ts (computed from cwd at load time). */
1111
export const DEFAULT_REPORT_DIR = `${process.cwd()}/.dotnet-format`;
1212

13-
export const FormatType = {
13+
const FormatType = {
1414
all: 'all',
1515
style: 'style',
1616
analyzers: 'analyzers',
@@ -45,7 +45,7 @@ function resolveEnabled(block, defaultValue) {
4545
* @param {string} type
4646
* @returns {string[]}
4747
*/
48-
export function buildArgs(options, onlyChangedFiles, changedFiles, type) {
48+
function buildArgs(options, onlyChangedFiles, changedFiles, type) {
4949
/** @type {string[]} */
5050
const args = [];
5151
if (options.verifyNoChanges) {
@@ -119,37 +119,22 @@ export function generateFormatCommandArgs(config, workspace, changedFiles = [],
119119
* @returns {Record<string, any>}
120120
*/
121121
export function buildDefaultOptions(inputs) {
122-
const verifyNoChanges = inputs.action === 'check';
122+
// Shared per-block defaults; the blocks differ only in the two overrides below.
123+
const base = {
124+
verifyNoChanges: inputs.action === 'check',
125+
severity: inputs.severityLevel,
126+
verbosity: inputs.logLevel,
127+
noRestore: !!inputs.nugetConfigPath
128+
};
123129
return {
124130
nugetConfigPath: inputs.nugetConfigPath,
125131
projectFileName: inputs.projectFileName,
126132
onlyChangedFiles: inputs.onlyChangedFiles,
127-
options: {
128-
verifyNoChanges,
129-
severity: inputs.severityLevel,
130-
verbosity: inputs.logLevel,
131-
noRestore: !!inputs.nugetConfigPath
132-
},
133-
whitespaceOptions: {
134-
verifyNoChanges,
135-
folder: true,
136-
severity: inputs.severityLevel,
137-
verbosity: inputs.logLevel,
138-
noRestore: !!inputs.nugetConfigPath
139-
},
140-
analyzersOptions: {
141-
verifyNoChanges,
142-
severity: inputs.severityLevel,
143-
verbosity: inputs.logLevel,
144-
noRestore: !!inputs.nugetConfigPath
145-
},
146-
styleOptions: {
147-
verifyNoChanges,
148-
severity: inputs.severityLevel,
149-
verbosity: inputs.logLevel,
150-
// Quirk preserved from the original: style uses dotnetFormatConfigPath here.
151-
noRestore: !!inputs.dotnetFormatConfigPath
152-
}
133+
options: { ...base },
134+
whitespaceOptions: { ...base, folder: true },
135+
analyzersOptions: { ...base },
136+
// Quirk preserved from the original: style keys noRestore off dotnetFormatConfigPath.
137+
styleOptions: { ...base, noRestore: !!inputs.dotnetFormatConfigPath }
153138
};
154139
}
155140

@@ -177,25 +162,22 @@ export function checkIsDryRun(config) {
177162
if (isEnabledBlock(config.options, false)) {
178163
return config.options?.verifyNoChanges ?? false;
179164
}
180-
const wEnabled = isEnabledBlock(config.whitespaceOptions, false);
181-
const aEnabled = isEnabledBlock(config.analyzersOptions, false);
182-
const sEnabled = isEnabledBlock(config.styleOptions, false);
183-
const w = (wEnabled && !!config.whitespaceOptions?.verifyNoChanges) || !wEnabled;
184-
const a = (aEnabled && !!config.analyzersOptions?.verifyNoChanges) || !aEnabled;
185-
const s = (sEnabled && !!config.styleOptions?.verifyNoChanges) || !sEnabled;
186-
return w && a && s;
165+
// A granular block is "dry" when it is disabled, or enabled and verifying no
166+
// changes — i.e. `(enabled && verify) || !enabled` simplifies to `!enabled || verify`.
167+
const dry = block => !isEnabledBlock(block, false) || !!block?.verifyNoChanges;
168+
return dry(config.whitespaceOptions) && dry(config.analyzersOptions) && dry(config.styleOptions);
187169
}
188170

189171
/**
190172
* Convenience: finalize a merged config and emit everything the runner needs.
191173
* @param {Record<string, any>} mergedConfig
192174
* @param {{ workspace: string, changedFiles?: string[], isOnlyChangedFiles?: boolean, reportDir?: string }} opts
193-
* @returns {{ config: Record<string, any>, commands: string[][], isDryRun: boolean }}
175+
* @returns {{ commands: string[][], isDryRun: boolean }}
194176
*/
195177
export function planFormat(mergedConfig, opts) {
196178
const { workspace, changedFiles = [], isOnlyChangedFiles = false, reportDir = DEFAULT_REPORT_DIR } = opts;
197179
const config = finalizeEnabled(mergedConfig);
198180
const commands = generateFormatCommandArgs(config, workspace, changedFiles, { isOnlyChangedFiles, reportDir });
199181
const isDryRun = checkIsDryRun(config);
200-
return { config, commands, isDryRun };
182+
return { commands, isDryRun };
201183
}

scripts/jscpd-report.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ export function isOverThreshold(report, threshold) {
4949

5050
/**
5151
* Build the PR comment / summary message from the jscpd markdown report content
52-
* and the parsed duplicates. Mirrors the message built in postReport.
52+
* and the parsed duplicates. Mirrors the message built in postReport. Takes the
53+
* pre-computed `header` (like `generateReport` in dotnet-report.mjs) so the caller
54+
* does not compute it twice.
5355
* @param {string} markdownReportContent Raw contents of jscpd's markdown report.
5456
* @param {any[]} duplicates
55-
* @param {string} workspace
57+
* @param {string} header
5658
* @param {string} scanPath
5759
* @param {import('./report-common.mjs').ReportContext} ctx
5860
* @returns {string}
5961
*/
60-
export function buildJscpdMessage(markdownReportContent, duplicates, workspace, scanPath, ctx) {
62+
export function buildJscpdMessage(markdownReportContent, duplicates, header, scanPath, ctx) {
6163
const report = markdownReportContent.replace('# Copy/paste detection report', '');
6264
let markdown = '<details>\n';
6365
markdown += ` <summary> JSCPD Details </summary>\n\n`;
@@ -68,7 +70,6 @@ export function buildJscpdMessage(markdownReportContent, duplicates, workspace,
6870
markdown += '\n';
6971
}
7072
markdown += '</details>\n';
71-
const header = getReportHeader(workspace);
7273
return `${header} \n\n${report}\n\n ${markdown}\n\n ${getReportFooter(ctx)}`;
7374
}
7475

scripts/report-common.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@
1212
* @property {string} [commit] Commit to credit in the footer (PR head sha or sha).
1313
*/
1414

15+
/**
16+
* Build the {@link ReportContext} from github-script's `context` (owner/repo/sha +
17+
* the PR-head-or-sha commit to credit). Shared by the dotnet format and jscpd report
18+
* steps so the context shape lives in one place.
19+
* @param {any} context
20+
* @returns {ReportContext}
21+
*/
22+
export function buildReportContext(context) {
23+
return {
24+
owner: context.repo.owner,
25+
repo: context.repo.repo,
26+
sha: context.sha,
27+
cwd: process.cwd(),
28+
runId: context.runId,
29+
commit: context.payload?.pull_request?.head?.sha || context.sha
30+
};
31+
}
32+
1533
/**
1634
* @param {ReportContext} ctx
1735
* @returns {string}

scripts/steps/comment.mjs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@
1010
* @returns {Promise<number | undefined>}
1111
*/
1212
export async function getExistingCommentId(github, context, header) {
13-
const comments = await github.rest.issues.listComments({
14-
owner: context.repo.owner,
15-
repo: context.repo.repo,
16-
issue_number: context.issue.number
17-
});
18-
let userLogin;
19-
try {
20-
userLogin = (await github.rest.users.getAuthenticated()).data?.login;
21-
} catch {
22-
// token without user scope; fall back to matching the Bot user type
23-
userLogin = undefined;
24-
}
13+
// The two calls are independent; run them together. getAuthenticated may 403 on a
14+
// token without user scope — fall back to matching the Bot user type in that case.
15+
const [comments, userLogin] = await Promise.all([
16+
github.rest.issues.listComments({
17+
owner: context.repo.owner,
18+
repo: context.repo.repo,
19+
issue_number: context.issue.number
20+
}),
21+
github.rest.users
22+
.getAuthenticated()
23+
.then(r => r.data?.login)
24+
.catch(() => undefined)
25+
]);
2526
const existing = comments.data?.find(c => {
2627
const isBotUserType = c.user?.type === 'Bot' || c.user?.login === userLogin;
2728
return isBotUserType && c.body?.startsWith(header);

scripts/steps/format-report.mjs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// github-script's { github, context, core }; report dir + inputs arrive via env.
44

55
import { generateReport, getReportFiles, getReportHeader } from '../dotnet-report.mjs';
6+
import { buildReportContext } from '../report-common.mjs';
67
import { upsertComment } from './comment.mjs';
78

89
/**
@@ -21,15 +22,7 @@ export async function run({ github, context, core }) {
2122
}
2223

2324
const header = getReportHeader(workspace);
24-
const reportCtx = {
25-
owner: context.repo.owner,
26-
repo: context.repo.repo,
27-
sha: context.sha,
28-
cwd: process.cwd(),
29-
runId: context.runId,
30-
commit: context.payload?.pull_request?.head?.sha || context.sha
31-
};
32-
const message = generateReport(reportFiles, header, reportCtx);
25+
const message = generateReport(reportFiles, header, buildReportContext(context));
3326
if (!message) {
3427
return;
3528
}

scripts/steps/jscpd-report.mjs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
88
import path from 'node:path';
99
import { buildAnnotations, buildJscpdMessage, getReportHeader, isOverThreshold } from '../jscpd-report.mjs';
1010
import { resolveConfig } from '../read-config.mjs';
11+
import { buildReportContext } from '../report-common.mjs';
1112
import { upsertComment } from './comment.mjs';
1213
import { makeLoader } from './load-config.mjs';
1314

@@ -46,21 +47,14 @@ export async function run({ github, context, core, exec, io }) {
4647
const threshold = cfg.threshold ?? 0;
4748

4849
const cwd = process.cwd();
49-
const reportCtx = {
50-
owner: context.repo.owner,
51-
repo: context.repo.repo,
52-
sha: context.sha,
53-
cwd,
54-
runId: context.runId,
55-
commit: context.payload?.pull_request?.head?.sha || context.sha
56-
};
50+
const reportCtx = buildReportContext(context);
5751
const header = getReportHeader(workspace);
5852

5953
// Build the message from jscpd's markdown report, post it, and overwrite the
6054
// markdown file with the full message (so the uploaded artifact matches).
6155
const mdFile = findByExt(outputDir, '.md');
6256
const mdContent = mdFile ? fs.readFileSync(mdFile, 'utf8') : '';
63-
const message = buildJscpdMessage(mdContent, duplicates, workspace, scanPath, reportCtx);
57+
const message = buildJscpdMessage(mdContent, duplicates, header, scanPath, reportCtx);
6458
if (mdFile) {
6559
fs.writeFileSync(mdFile, message);
6660
}

0 commit comments

Comments
 (0)