safegres 1.23.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/config/schema.js +7 -1
- package/config/types.d.ts +17 -2
- package/esm/config/schema.js +7 -1
- package/esm/config/types.d.ts +17 -2
- package/esm/report/github.d.ts +16 -3
- package/esm/report/github.js +54 -9
- package/package.json +8 -8
- package/report/github.d.ts +16 -3
- package/report/github.js +55 -10
- package/schema/safegres.schema.json +11 -1
package/README.md
CHANGED
|
@@ -435,13 +435,20 @@ Or the first-party action, which installs the CLI, runs that config, and turns t
|
|
|
435
435
|
job summary, annotations and a sticky PR comment:
|
|
436
436
|
|
|
437
437
|
```yaml
|
|
438
|
-
- uses: constructive-io/constructive/packages/safegres@
|
|
438
|
+
- uses: constructive-io/constructive/packages/safegres@safegres-action-v1
|
|
439
439
|
with:
|
|
440
|
-
out: safegres-reports
|
|
441
440
|
comment: true # sticky PR comment (needs pull-requests: write)
|
|
442
441
|
upload-sarif: true # code scanning (needs security-events: write)
|
|
443
442
|
```
|
|
444
443
|
|
|
444
|
+
`safegres-action-v1` is a moving tag on the action's latest v1; `@main` tracks the monorepo tip.
|
|
445
|
+
The reports are uploaded as the `safegres-reports` artifact by default — that artifact *is* the
|
|
446
|
+
next run's baseline, so a job that suppresses it (`upload-reports: false`) can never show a delta.
|
|
447
|
+
|
|
448
|
+
Auditing a pgpm workspace (`source.pgpm`) adds `version: local`, which runs the repo's own safegres
|
|
449
|
+
rather than installing one: that path deploys through `pgsql-test`, an optional peer only the
|
|
450
|
+
workspace has.
|
|
451
|
+
|
|
445
452
|
On a pull request it also measures the delta for you: it picks the base branch's most recent run
|
|
446
453
|
that *is* a sane comparison — succeeded, younger than 48 hours, and audited a commit in this
|
|
447
454
|
branch's merge-base history — downloads its report, and names the run it chose in the summary and
|
package/config/schema.js
CHANGED
|
@@ -131,7 +131,13 @@ const FAIL_ON = {
|
|
|
131
131
|
};
|
|
132
132
|
const GITHUB_COMMENT = {
|
|
133
133
|
sticky: bool('Reuse one comment per PR instead of appending. Default true.'),
|
|
134
|
-
sections: list('Sections to include. Default scores, delta, new-findings.
|
|
134
|
+
sections: list('Sections to include. Default scores, delta, new-findings. `report` adds the '
|
|
135
|
+
+ 'markdown report (`findings` is its old name).', str('Section', ['scores', 'delta', 'new-findings', 'report', 'findings', 'planes'])),
|
|
136
|
+
detail: str('How much of the report the report section carries. Default summary.', [
|
|
137
|
+
'summary',
|
|
138
|
+
'normal',
|
|
139
|
+
'verbose'
|
|
140
|
+
])
|
|
135
141
|
};
|
|
136
142
|
const GITHUB = {
|
|
137
143
|
summary: list('Scores in the job summary, in order (security, perf, planes:<glob>)', str('Score')),
|
package/config/types.d.ts
CHANGED
|
@@ -479,8 +479,23 @@ export interface GithubReportConfig {
|
|
|
479
479
|
export interface GithubCommentConfig {
|
|
480
480
|
/** Reuse one comment per PR instead of appending. Default `true`. */
|
|
481
481
|
sticky?: boolean;
|
|
482
|
-
/**
|
|
483
|
-
|
|
482
|
+
/**
|
|
483
|
+
* Sections to include, in this order: `scores` (badges + exposure), `planes`,
|
|
484
|
+
* `delta`, `new-findings` (the perf ratchet verdict and what it caught), and
|
|
485
|
+
* `report` — the same markdown report the job summary carries, at `detail`.
|
|
486
|
+
* `findings` is the old name for `report`. Default
|
|
487
|
+
* `['scores', 'delta', 'new-findings']`; add `report` to review the audit
|
|
488
|
+
* without leaving the PR.
|
|
489
|
+
*/
|
|
490
|
+
sections?: Array<'scores' | 'delta' | 'new-findings' | 'report' | 'findings' | 'planes'>;
|
|
491
|
+
/**
|
|
492
|
+
* How much of the report the `report` section carries. Default `summary` —
|
|
493
|
+
* scores, exposure, counts, the delta and the ratchet verdict, with no
|
|
494
|
+
* per-finding tables, because a comment is read on the way past and GitHub
|
|
495
|
+
* rejects a body over 64 KB. `normal`/`verbose` add the finding tables and
|
|
496
|
+
* degrade back to `summary` when the body would not fit.
|
|
497
|
+
*/
|
|
498
|
+
detail?: 'summary' | 'normal' | 'verbose';
|
|
484
499
|
}
|
|
485
500
|
/**
|
|
486
501
|
* The full safegres configuration, loadable from `safegres.config.{ts,js,mjs,cjs}`,
|
package/esm/config/schema.js
CHANGED
|
@@ -127,7 +127,13 @@ const FAIL_ON = {
|
|
|
127
127
|
};
|
|
128
128
|
const GITHUB_COMMENT = {
|
|
129
129
|
sticky: bool('Reuse one comment per PR instead of appending. Default true.'),
|
|
130
|
-
sections: list('Sections to include. Default scores, delta, new-findings.
|
|
130
|
+
sections: list('Sections to include. Default scores, delta, new-findings. `report` adds the '
|
|
131
|
+
+ 'markdown report (`findings` is its old name).', str('Section', ['scores', 'delta', 'new-findings', 'report', 'findings', 'planes'])),
|
|
132
|
+
detail: str('How much of the report the report section carries. Default summary.', [
|
|
133
|
+
'summary',
|
|
134
|
+
'normal',
|
|
135
|
+
'verbose'
|
|
136
|
+
])
|
|
131
137
|
};
|
|
132
138
|
const GITHUB = {
|
|
133
139
|
summary: list('Scores in the job summary, in order (security, perf, planes:<glob>)', str('Score')),
|
package/esm/config/types.d.ts
CHANGED
|
@@ -479,8 +479,23 @@ export interface GithubReportConfig {
|
|
|
479
479
|
export interface GithubCommentConfig {
|
|
480
480
|
/** Reuse one comment per PR instead of appending. Default `true`. */
|
|
481
481
|
sticky?: boolean;
|
|
482
|
-
/**
|
|
483
|
-
|
|
482
|
+
/**
|
|
483
|
+
* Sections to include, in this order: `scores` (badges + exposure), `planes`,
|
|
484
|
+
* `delta`, `new-findings` (the perf ratchet verdict and what it caught), and
|
|
485
|
+
* `report` — the same markdown report the job summary carries, at `detail`.
|
|
486
|
+
* `findings` is the old name for `report`. Default
|
|
487
|
+
* `['scores', 'delta', 'new-findings']`; add `report` to review the audit
|
|
488
|
+
* without leaving the PR.
|
|
489
|
+
*/
|
|
490
|
+
sections?: Array<'scores' | 'delta' | 'new-findings' | 'report' | 'findings' | 'planes'>;
|
|
491
|
+
/**
|
|
492
|
+
* How much of the report the `report` section carries. Default `summary` —
|
|
493
|
+
* scores, exposure, counts, the delta and the ratchet verdict, with no
|
|
494
|
+
* per-finding tables, because a comment is read on the way past and GitHub
|
|
495
|
+
* rejects a body over 64 KB. `normal`/`verbose` add the finding tables and
|
|
496
|
+
* degrade back to `summary` when the body would not fit.
|
|
497
|
+
*/
|
|
498
|
+
detail?: 'summary' | 'normal' | 'verbose';
|
|
484
499
|
}
|
|
485
500
|
/**
|
|
486
501
|
* The full safegres configuration, loadable from `safegres.config.{ts,js,mjs,cjs}`,
|
package/esm/report/github.d.ts
CHANGED
|
@@ -34,9 +34,22 @@ export declare function gradeBadge(label: string, grade: Grade, badges: boolean)
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function renderGithubSummary(report: Report, options?: GithubRenderOptions): string;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* GitHub rejects an issue comment body over 65536 characters, and a comment
|
|
38
|
+
* that fails to post is worse than a short one — so a comment carrying the
|
|
39
|
+
* report degrades (verbose → normal → summary → a pointer at the job summary)
|
|
40
|
+
* rather than being truncated at a byte boundary by the API.
|
|
41
|
+
*/
|
|
42
|
+
export declare const COMMENT_MAX_CHARS = 65536;
|
|
43
|
+
/**
|
|
44
|
+
* The sticky PR comment: shorter than the summary by default. A comment is read
|
|
45
|
+
* on the way past, so out of the box it carries the scores, the movement, and
|
|
46
|
+
* what is new, with the full finding tables in the job summary and the SARIF
|
|
47
|
+
* alerts.
|
|
48
|
+
*
|
|
49
|
+
* `comment.sections` chooses what it says. `report` puts the whole markdown
|
|
50
|
+
* report in the comment at `comment.detail` (default `summary`: scores,
|
|
51
|
+
* exposure, counts, the delta and the ratchet verdict — no per-finding tables),
|
|
52
|
+
* which is the same render the job summary uses, so the two cannot disagree.
|
|
40
53
|
*/
|
|
41
54
|
export declare function renderGithubComment(report: Report, options?: GithubRenderOptions): string;
|
|
42
55
|
/**
|
package/esm/report/github.js
CHANGED
|
@@ -72,22 +72,53 @@ export function renderGithubSummary(report, options = {}) {
|
|
|
72
72
|
return out.join('\n');
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* GitHub rejects an issue comment body over 65536 characters, and a comment
|
|
76
|
+
* that fails to post is worse than a short one — so a comment carrying the
|
|
77
|
+
* report degrades (verbose → normal → summary → a pointer at the job summary)
|
|
78
|
+
* rather than being truncated at a byte boundary by the API.
|
|
79
|
+
*/
|
|
80
|
+
export const COMMENT_MAX_CHARS = 65536;
|
|
81
|
+
/**
|
|
82
|
+
* The sticky PR comment: shorter than the summary by default. A comment is read
|
|
83
|
+
* on the way past, so out of the box it carries the scores, the movement, and
|
|
84
|
+
* what is new, with the full finding tables in the job summary and the SARIF
|
|
85
|
+
* alerts.
|
|
86
|
+
*
|
|
87
|
+
* `comment.sections` chooses what it says. `report` puts the whole markdown
|
|
88
|
+
* report in the comment at `comment.detail` (default `summary`: scores,
|
|
89
|
+
* exposure, counts, the delta and the ratchet verdict — no per-finding tables),
|
|
90
|
+
* which is the same render the job summary uses, so the two cannot disagree.
|
|
78
91
|
*/
|
|
79
92
|
export function renderGithubComment(report, options = {}) {
|
|
93
|
+
const wanted = options.config?.comment?.detail ?? 'summary';
|
|
94
|
+
const chain = wanted === 'verbose' ? ['verbose', 'normal', 'summary'] : wanted === 'normal' ? ['normal', 'summary'] : ['summary'];
|
|
95
|
+
for (const detail of chain) {
|
|
96
|
+
const body = commentBody(report, options, detail);
|
|
97
|
+
if (body.length <= COMMENT_MAX_CHARS)
|
|
98
|
+
return body;
|
|
99
|
+
}
|
|
100
|
+
return commentBody(report, options, null);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One comment body. `detail` is the report section's detail, or null to drop
|
|
104
|
+
* the report and say where it went — which is how the size cap is honored.
|
|
105
|
+
*/
|
|
106
|
+
function commentBody(report, options, detail) {
|
|
80
107
|
const config = options.config ?? {};
|
|
81
108
|
const badges = config.badges !== false;
|
|
82
109
|
const sections = new Set(config.comment?.sections ?? ['scores', 'delta', 'new-findings']);
|
|
83
110
|
const selectors = config.summary ?? ['security', 'perf'];
|
|
84
111
|
const view = selectView(report, { planes: planePatterns(selectors), ...options.view });
|
|
112
|
+
// `findings` is the old name for `report`; both mean "the markdown report".
|
|
113
|
+
const wantsReport = sections.has('report') || sections.has('findings');
|
|
114
|
+
const withReport = wantsReport && detail != null;
|
|
85
115
|
const out = [COMMENT_MARKER, '## safegres', ''];
|
|
86
116
|
if (sections.has('scores')) {
|
|
87
117
|
out.push(selectedScores(view.scores, selectors)
|
|
88
118
|
.map((s) => scoreBadge(s.label, s.score, badges))
|
|
89
119
|
.join(' '), '');
|
|
90
|
-
|
|
120
|
+
// Everything below is in the report too, so it is said once.
|
|
121
|
+
if (report.exposure && !withReport) {
|
|
91
122
|
out.push(report.exposure.known
|
|
92
123
|
? `Exposure (${report.exposure.source}): **${report.exposure.exposedTables}/`
|
|
93
124
|
+ `${report.exposure.totalTables}** tables reachable.`
|
|
@@ -100,7 +131,9 @@ export function renderGithubComment(report, options = {}) {
|
|
|
100
131
|
? `| \`${p.name}\` | ${p.kind} | — | not graded |`
|
|
101
132
|
: `| \`${p.name}\` | ${p.kind} | ${p.exposedTables} | **${p.score.grade}** (${p.score.value}) |`), '');
|
|
102
133
|
}
|
|
103
|
-
|
|
134
|
+
// The report renders the comparison (or the reason there is none) itself.
|
|
135
|
+
const wantsDelta = sections.has('delta') && !withReport;
|
|
136
|
+
if (wantsDelta && report.comparison) {
|
|
104
137
|
const cmp = report.comparison;
|
|
105
138
|
const since = describeBaseline(cmp.previous);
|
|
106
139
|
if (cmp.unchanged) {
|
|
@@ -121,18 +154,30 @@ export function renderGithubComment(report, options = {}) {
|
|
|
121
154
|
out.push('');
|
|
122
155
|
}
|
|
123
156
|
}
|
|
124
|
-
else if (
|
|
157
|
+
else if (wantsDelta && report.comparisonSkipped) {
|
|
125
158
|
out.push(`_No delta baseline: ${report.comparisonSkipped}. `
|
|
126
159
|
+ 'The absolute score and the perf baseline still gate this run._', '');
|
|
127
160
|
}
|
|
128
161
|
if (sections.has('new-findings')) {
|
|
129
|
-
const
|
|
162
|
+
const diff = report.perf?.diff;
|
|
163
|
+
// The ratchet's verdict, even when it is "nothing new": a comment that only
|
|
164
|
+
// speaks up on new debt cannot be distinguished from one that never ran.
|
|
165
|
+
// The report carries this line itself, so it is not repeated under it.
|
|
166
|
+
if (diff && !withReport) {
|
|
167
|
+
out.push(`Perf baseline: **${diff.added.length} new**, ${diff.accepted.length} accepted, `
|
|
168
|
+
+ `${diff.removed.length} resolved.`, '');
|
|
169
|
+
}
|
|
170
|
+
const added = diff?.added ?? [];
|
|
130
171
|
if (added.length > 0) {
|
|
131
172
|
out.push(`**${added.length} new performance finding${added.length === 1 ? '' : 's'} since the baseline**`, '', ...added.slice(0, 10).map((f) => `- \`${f.code}\` ${location(f)} — ${f.message}`), '');
|
|
132
173
|
}
|
|
133
174
|
}
|
|
134
|
-
if (
|
|
135
|
-
out.push(renderMarkdown(report, { title: '
|
|
175
|
+
if (withReport) {
|
|
176
|
+
out.push(renderMarkdown(report, { title: 'Report', view: { detail, ...options.view } }), '');
|
|
177
|
+
}
|
|
178
|
+
else if (wantsReport) {
|
|
179
|
+
out.push('_The report is too large for a PR comment — it is in the job summary and the '
|
|
180
|
+
+ 'reports artifact._', '');
|
|
136
181
|
}
|
|
137
182
|
return out.join('\n');
|
|
138
183
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safegres",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Postgres security and performance auditor: RLS, grants and effective-access analysis plus index and policy-cost checks, scored on two independent axes. One connection, no framework.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -57,16 +57,16 @@
|
|
|
57
57
|
"safegres"
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@inquirerer/utils": "^3.3.
|
|
61
|
-
"@pgpmjs/logger": "^2.25.
|
|
60
|
+
"@inquirerer/utils": "^3.3.13",
|
|
61
|
+
"@pgpmjs/logger": "^2.25.1",
|
|
62
62
|
"@pgsql/lint": "^18.2.5",
|
|
63
63
|
"@pgsql/traverse": "^18.7.7",
|
|
64
64
|
"@pgsql/types": "^18.0.0",
|
|
65
65
|
"confstash": "^0.1.2",
|
|
66
|
-
"inquirerer": "^4.9.
|
|
66
|
+
"inquirerer": "^4.9.4",
|
|
67
67
|
"libpg-query": "^18.1.4",
|
|
68
68
|
"pg": "^8.21.0",
|
|
69
|
-
"pg-env": "^1.31.
|
|
69
|
+
"pg-env": "^1.31.1",
|
|
70
70
|
"pgsql-deparser": "^18.3.6",
|
|
71
71
|
"pgsql-parser": "^18.2.6",
|
|
72
72
|
"yanse": "^0.2.2"
|
|
@@ -82,8 +82,8 @@
|
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@types/node": "^22.0.0",
|
|
84
84
|
"@types/pg": "^8.20.4",
|
|
85
|
-
"makage": "^0.
|
|
86
|
-
"pgsql-test": "^5.13.
|
|
85
|
+
"makage": "^0.8.0",
|
|
86
|
+
"pgsql-test": "^5.13.3"
|
|
87
87
|
},
|
|
88
|
-
"gitHead": "
|
|
88
|
+
"gitHead": "5e0bfead2856d1a062b495d343a43e543343f9b8"
|
|
89
89
|
}
|
package/report/github.d.ts
CHANGED
|
@@ -34,9 +34,22 @@ export declare function gradeBadge(label: string, grade: Grade, badges: boolean)
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function renderGithubSummary(report: Report, options?: GithubRenderOptions): string;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* GitHub rejects an issue comment body over 65536 characters, and a comment
|
|
38
|
+
* that fails to post is worse than a short one — so a comment carrying the
|
|
39
|
+
* report degrades (verbose → normal → summary → a pointer at the job summary)
|
|
40
|
+
* rather than being truncated at a byte boundary by the API.
|
|
41
|
+
*/
|
|
42
|
+
export declare const COMMENT_MAX_CHARS = 65536;
|
|
43
|
+
/**
|
|
44
|
+
* The sticky PR comment: shorter than the summary by default. A comment is read
|
|
45
|
+
* on the way past, so out of the box it carries the scores, the movement, and
|
|
46
|
+
* what is new, with the full finding tables in the job summary and the SARIF
|
|
47
|
+
* alerts.
|
|
48
|
+
*
|
|
49
|
+
* `comment.sections` chooses what it says. `report` puts the whole markdown
|
|
50
|
+
* report in the comment at `comment.detail` (default `summary`: scores,
|
|
51
|
+
* exposure, counts, the delta and the ratchet verdict — no per-finding tables),
|
|
52
|
+
* which is the same render the job summary uses, so the two cannot disagree.
|
|
40
53
|
*/
|
|
41
54
|
export declare function renderGithubComment(report: Report, options?: GithubRenderOptions): string;
|
|
42
55
|
/**
|
package/report/github.js
CHANGED
|
@@ -45,7 +45,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
45
45
|
};
|
|
46
46
|
})();
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.COMMENT_MARKER = void 0;
|
|
48
|
+
exports.COMMENT_MAX_CHARS = exports.COMMENT_MARKER = void 0;
|
|
49
49
|
exports.scoreBadge = scoreBadge;
|
|
50
50
|
exports.gradeBadge = gradeBadge;
|
|
51
51
|
exports.renderGithubSummary = renderGithubSummary;
|
|
@@ -115,22 +115,53 @@ function renderGithubSummary(report, options = {}) {
|
|
|
115
115
|
return out.join('\n');
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
118
|
+
* GitHub rejects an issue comment body over 65536 characters, and a comment
|
|
119
|
+
* that fails to post is worse than a short one — so a comment carrying the
|
|
120
|
+
* report degrades (verbose → normal → summary → a pointer at the job summary)
|
|
121
|
+
* rather than being truncated at a byte boundary by the API.
|
|
122
|
+
*/
|
|
123
|
+
exports.COMMENT_MAX_CHARS = 65536;
|
|
124
|
+
/**
|
|
125
|
+
* The sticky PR comment: shorter than the summary by default. A comment is read
|
|
126
|
+
* on the way past, so out of the box it carries the scores, the movement, and
|
|
127
|
+
* what is new, with the full finding tables in the job summary and the SARIF
|
|
128
|
+
* alerts.
|
|
129
|
+
*
|
|
130
|
+
* `comment.sections` chooses what it says. `report` puts the whole markdown
|
|
131
|
+
* report in the comment at `comment.detail` (default `summary`: scores,
|
|
132
|
+
* exposure, counts, the delta and the ratchet verdict — no per-finding tables),
|
|
133
|
+
* which is the same render the job summary uses, so the two cannot disagree.
|
|
121
134
|
*/
|
|
122
135
|
function renderGithubComment(report, options = {}) {
|
|
136
|
+
const wanted = options.config?.comment?.detail ?? 'summary';
|
|
137
|
+
const chain = wanted === 'verbose' ? ['verbose', 'normal', 'summary'] : wanted === 'normal' ? ['normal', 'summary'] : ['summary'];
|
|
138
|
+
for (const detail of chain) {
|
|
139
|
+
const body = commentBody(report, options, detail);
|
|
140
|
+
if (body.length <= exports.COMMENT_MAX_CHARS)
|
|
141
|
+
return body;
|
|
142
|
+
}
|
|
143
|
+
return commentBody(report, options, null);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* One comment body. `detail` is the report section's detail, or null to drop
|
|
147
|
+
* the report and say where it went — which is how the size cap is honored.
|
|
148
|
+
*/
|
|
149
|
+
function commentBody(report, options, detail) {
|
|
123
150
|
const config = options.config ?? {};
|
|
124
151
|
const badges = config.badges !== false;
|
|
125
152
|
const sections = new Set(config.comment?.sections ?? ['scores', 'delta', 'new-findings']);
|
|
126
153
|
const selectors = config.summary ?? ['security', 'perf'];
|
|
127
154
|
const view = (0, view_1.selectView)(report, { planes: planePatterns(selectors), ...options.view });
|
|
155
|
+
// `findings` is the old name for `report`; both mean "the markdown report".
|
|
156
|
+
const wantsReport = sections.has('report') || sections.has('findings');
|
|
157
|
+
const withReport = wantsReport && detail != null;
|
|
128
158
|
const out = [exports.COMMENT_MARKER, '## safegres', ''];
|
|
129
159
|
if (sections.has('scores')) {
|
|
130
160
|
out.push(selectedScores(view.scores, selectors)
|
|
131
161
|
.map((s) => scoreBadge(s.label, s.score, badges))
|
|
132
162
|
.join(' '), '');
|
|
133
|
-
|
|
163
|
+
// Everything below is in the report too, so it is said once.
|
|
164
|
+
if (report.exposure && !withReport) {
|
|
134
165
|
out.push(report.exposure.known
|
|
135
166
|
? `Exposure (${report.exposure.source}): **${report.exposure.exposedTables}/`
|
|
136
167
|
+ `${report.exposure.totalTables}** tables reachable.`
|
|
@@ -143,7 +174,9 @@ function renderGithubComment(report, options = {}) {
|
|
|
143
174
|
? `| \`${p.name}\` | ${p.kind} | — | not graded |`
|
|
144
175
|
: `| \`${p.name}\` | ${p.kind} | ${p.exposedTables} | **${p.score.grade}** (${p.score.value}) |`), '');
|
|
145
176
|
}
|
|
146
|
-
|
|
177
|
+
// The report renders the comparison (or the reason there is none) itself.
|
|
178
|
+
const wantsDelta = sections.has('delta') && !withReport;
|
|
179
|
+
if (wantsDelta && report.comparison) {
|
|
147
180
|
const cmp = report.comparison;
|
|
148
181
|
const since = (0, compare_1.describeBaseline)(cmp.previous);
|
|
149
182
|
if (cmp.unchanged) {
|
|
@@ -164,18 +197,30 @@ function renderGithubComment(report, options = {}) {
|
|
|
164
197
|
out.push('');
|
|
165
198
|
}
|
|
166
199
|
}
|
|
167
|
-
else if (
|
|
200
|
+
else if (wantsDelta && report.comparisonSkipped) {
|
|
168
201
|
out.push(`_No delta baseline: ${report.comparisonSkipped}. `
|
|
169
202
|
+ 'The absolute score and the perf baseline still gate this run._', '');
|
|
170
203
|
}
|
|
171
204
|
if (sections.has('new-findings')) {
|
|
172
|
-
const
|
|
205
|
+
const diff = report.perf?.diff;
|
|
206
|
+
// The ratchet's verdict, even when it is "nothing new": a comment that only
|
|
207
|
+
// speaks up on new debt cannot be distinguished from one that never ran.
|
|
208
|
+
// The report carries this line itself, so it is not repeated under it.
|
|
209
|
+
if (diff && !withReport) {
|
|
210
|
+
out.push(`Perf baseline: **${diff.added.length} new**, ${diff.accepted.length} accepted, `
|
|
211
|
+
+ `${diff.removed.length} resolved.`, '');
|
|
212
|
+
}
|
|
213
|
+
const added = diff?.added ?? [];
|
|
173
214
|
if (added.length > 0) {
|
|
174
215
|
out.push(`**${added.length} new performance finding${added.length === 1 ? '' : 's'} since the baseline**`, '', ...added.slice(0, 10).map((f) => `- \`${f.code}\` ${location(f)} — ${f.message}`), '');
|
|
175
216
|
}
|
|
176
217
|
}
|
|
177
|
-
if (
|
|
178
|
-
out.push((0, markdown_1.renderMarkdown)(report, { title: '
|
|
218
|
+
if (withReport) {
|
|
219
|
+
out.push((0, markdown_1.renderMarkdown)(report, { title: 'Report', view: { detail, ...options.view } }), '');
|
|
220
|
+
}
|
|
221
|
+
else if (wantsReport) {
|
|
222
|
+
out.push('_The report is too large for a PR comment — it is in the job summary and the '
|
|
223
|
+
+ 'reports artifact._', '');
|
|
179
224
|
}
|
|
180
225
|
return out.join('\n');
|
|
181
226
|
}
|
|
@@ -1044,12 +1044,22 @@
|
|
|
1044
1044
|
"scores",
|
|
1045
1045
|
"delta",
|
|
1046
1046
|
"new-findings",
|
|
1047
|
+
"report",
|
|
1047
1048
|
"findings",
|
|
1048
1049
|
"planes"
|
|
1049
1050
|
],
|
|
1050
1051
|
"description": "Section"
|
|
1051
1052
|
},
|
|
1052
|
-
"description": "Sections to include. Default scores, delta, new-findings."
|
|
1053
|
+
"description": "Sections to include. Default scores, delta, new-findings. `report` adds the markdown report (`findings` is its old name)."
|
|
1054
|
+
},
|
|
1055
|
+
"detail": {
|
|
1056
|
+
"type": "string",
|
|
1057
|
+
"enum": [
|
|
1058
|
+
"summary",
|
|
1059
|
+
"normal",
|
|
1060
|
+
"verbose"
|
|
1061
|
+
],
|
|
1062
|
+
"description": "How much of the report the report section carries. Default summary."
|
|
1053
1063
|
}
|
|
1054
1064
|
},
|
|
1055
1065
|
"description": "Sticky PR comment"
|