Skip to content

Commit c25d594

Browse files
committed
Address PR review feedback
Copilot suggestions: - _getBooleanInput returns the default for empty or unrecognized values - _setOutputs only sets change outputs when a report was captured - gate deployment-report capture on reporting being enabled - paginate the sticky-comment lookup so it is found past 100 comments - redact secret-looking arguments from the summary - pin @actions/github to 6.0.0 - remove the unused EMPTY_REPORT constant Reviewer notes: - friendlyType uses startsWith instead of a regex - Dropped icon changed to a cross mark - use string methods (replaceAll, trimEnd, startsWith) where a regex was not needed Add es2019/es2021 string libs, update tests, and rebuild the bundle.
1 parent 9843dd1 commit c25d594

11 files changed

Lines changed: 89 additions & 30 deletions

__tests__/DeploymentSummary.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe('DeploymentSummary tests', () => {
6363
const markdown = buildSummary({ ...baseContext, report });
6464
expect(markdown).toContain('<summary>➕ Created (1)</summary>');
6565
expect(markdown).toContain('<summary>🔄 Altered (1)</summary>');
66-
expect(markdown).toContain('<summary>🗑️ Dropped (1)</summary>');
66+
expect(markdown).toContain('<summary> Dropped (1)</summary>');
6767
expect(markdown).toContain('| `[dbo].[Reactions]` | Table |');
6868
});
6969

@@ -120,7 +120,7 @@ describe('DeploymentSummary tests', () => {
120120

121121
it('ends with the sticky comment marker', () => {
122122
const markdown = buildSummary({ ...baseContext, report });
123-
expect(markdown.replace(/\s+$/, '').endsWith(SUMMARY_MARKER)).toBe(true);
123+
expect(markdown.trimEnd().endsWith(SUMMARY_MARKER)).toBe(true);
124124
});
125125

126126
it('never includes a password even if one is present in the context object', () => {

__tests__/Reporter.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ describe('Reporter tests', () => {
4242
createComment: jest.fn().mockResolvedValue({}),
4343
updateComment: jest.fn().mockResolvedValue({})
4444
}
45-
}
45+
},
46+
paginate: jest.fn().mockResolvedValue(comments)
4647
};
4748
(github.getOctokit as jest.Mock).mockReturnValue(octokit);
4849
return octokit;
@@ -75,6 +76,33 @@ describe('Reporter tests', () => {
7576
expect(core.setOutput).toHaveBeenCalledWith('objects-changed', '4');
7677
});
7778

79+
it('does not set the change outputs when no report was captured', async () => {
80+
mockInputs({ summary: 'true', 'comment-pr': 'off' });
81+
82+
await Reporter.report(inputs, {});
83+
84+
expect(core.setOutput).not.toHaveBeenCalledWith('changes-detected', expect.anything());
85+
expect(core.setOutput).not.toHaveBeenCalledWith('objects-changed', expect.anything());
86+
});
87+
88+
it('redacts secret-looking additional arguments from the summary', async () => {
89+
mockInputs({ summary: 'true', 'comment-pr': 'off' });
90+
91+
await Reporter.report({ ...inputs, additionalArguments: '/TargetPassword:hunter2' }, {});
92+
93+
const markdown = (core.summary.addRaw as jest.Mock).mock.calls[0][0];
94+
expect(markdown).toContain('[redacted]');
95+
expect(markdown).not.toContain('hunter2');
96+
});
97+
98+
it('falls back to the default when the summary value is unrecognized', async () => {
99+
mockInputs({ summary: 'yes', 'comment-pr': 'off' });
100+
101+
await Reporter.report(inputs, {});
102+
103+
expect(core.summary.addRaw).toHaveBeenCalled();
104+
});
105+
78106
it('creates a new comment when none exists', async () => {
79107
mockInputs({ summary: 'false', 'comment-pr': 'auto', 'github-token': 'token' });
80108
const octokit = mockOctokit([]);
@@ -125,7 +153,8 @@ describe('Reporter tests', () => {
125153
it('warns without throwing when the comment API fails', async () => {
126154
mockInputs({ summary: 'false', 'comment-pr': 'auto', 'github-token': 'token' });
127155
(github.getOctokit as jest.Mock).mockReturnValue({
128-
rest: { issues: { listComments: jest.fn().mockRejectedValue(new Error('forbidden')) } }
156+
rest: { issues: { listComments: jest.fn() } },
157+
paginate: jest.fn().mockRejectedValue(new Error('forbidden'))
129158
});
130159

131160
await expect(Reporter.report(inputs, {})).resolves.toBeUndefined();

__tests__/main.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('main.ts tests', () => {
5858

5959
expect(detectIPAddressSpy).toHaveBeenCalled();
6060
expect(getAuthorizerSpy).not.toHaveBeenCalled();
61-
expect(getInputSpy).toHaveBeenCalledTimes(6);
61+
expect(getInputSpy).toHaveBeenCalledTimes(8);
6262
expect(resolveFilePathSpy).toHaveBeenCalled();
6363
expect(addFirewallRuleSpy).not.toHaveBeenCalled();
6464
expect(actionExecuteSpy).toHaveBeenCalled();
@@ -102,7 +102,7 @@ describe('main.ts tests', () => {
102102

103103
expect(detectIPAddressSpy).toHaveBeenCalled();
104104
expect(getAuthorizerSpy).not.toHaveBeenCalled();
105-
expect(getInputSpy).toHaveBeenCalledTimes(5);
105+
expect(getInputSpy).toHaveBeenCalledTimes(7);
106106
expect(resolveFilePathSpy).toHaveBeenCalled();
107107
expect(addFirewallRuleSpy).not.toHaveBeenCalled();
108108
expect(actionExecuteSpy).toHaveBeenCalled();
@@ -148,7 +148,7 @@ describe('main.ts tests', () => {
148148

149149
expect(detectIPAddressSpy).toHaveBeenCalled();
150150
expect(getAuthorizerSpy).not.toHaveBeenCalled();
151-
expect(getInputSpy).toHaveBeenCalledTimes(5);
151+
expect(getInputSpy).toHaveBeenCalledTimes(7);
152152
expect(resolveFilePathSpy).toHaveBeenCalled();
153153
expect(addFirewallRuleSpy).not.toHaveBeenCalled();
154154
expect(actionExecuteSpy).toHaveBeenCalled();

lib/main.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.

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"dependencies": {
2323
"@actions/core": "^1.9.1",
2424
"@actions/exec": "^1.0.1",
25-
"@actions/github": "^6.0.0",
25+
"@actions/github": "6.0.0",
2626
"@actions/tool-cache": "^2.0.1",
2727
"@tediousjs/connection-string": "^0.5.0",
2828
"azure-actions-webclient": "^1.0.3",

src/DeploymentReport.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ export interface DeploymentReport {
3333
alerts: DeploymentAlert[];
3434
}
3535

36-
const EMPTY_REPORT: DeploymentReport = { operations: [], alerts: [] };
37-
3836
/**
3937
* Parses the XML of a SqlPackage deployment report into a structured summary.
4038
* The parse is namespace-agnostic and tolerant of missing or malformed input:
@@ -120,7 +118,7 @@ function friendlyType(type: string | undefined): string {
120118
return 'Object';
121119
}
122120

123-
const withoutPrefix = type.replace(/^Sql/, '');
121+
const withoutPrefix = type.startsWith('Sql') ? type.slice(3) : type;
124122
const spaced = withoutPrefix.replace(/([a-z])([A-Z])/g, '$1 $2');
125123
return spaced || type;
126124
}

src/DeploymentSummary.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ interface OperationGroup {
2323
const OPERATION_GROUPS: OperationGroup[] = [
2424
{ key: 'create', label: 'Created', icon: '➕' },
2525
{ key: 'alter', label: 'Altered', icon: '🔄' },
26-
{ key: 'drop', label: 'Dropped', icon: '🗑️' }
26+
{ key: 'drop', label: 'Dropped', icon: '' }
2727
];
2828

2929
/**
@@ -280,7 +280,7 @@ function renderScript(script: string): string[] {
280280
lines.push(`<summary>📄 Deployment T-SQL script · ${metaParts.join(' · ')}</summary>`);
281281
lines.push('');
282282
lines.push('```sql');
283-
lines.push(content.replace(/\s+$/, ''));
283+
lines.push(content.trimEnd());
284284
if (truncated) {
285285
lines.push('');
286286
lines.push('-- Script truncated for display. See the deployment-script-path output for the full script.');
@@ -368,7 +368,7 @@ function formatCounts(counts: Array<[string, number]>): string {
368368
* unescaped pipe would otherwise be interpreted as a column separator.
369369
*/
370370
function escapeCell(value: string): string {
371-
return value.replace(/\|/g, '\\|');
371+
return value.replaceAll('|', '\\|');
372372
}
373373

374374
/**
@@ -410,7 +410,7 @@ function pastTenseAction(action: string): string {
410410
* of the deployment script file.
411411
*/
412412
function stripLeadingBom(value: string): string {
413-
return value.replace(/^\uFEFF/, '');
413+
return value.startsWith('\uFEFF') ? value.slice(1) : value;
414414
}
415415

416416
/**

src/Reporter.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,19 @@ export default class Reporter {
7474
}
7575

7676
/**
77-
* Reads a boolean input, returning the default when the input is not set.
78-
* Unlike core.getBooleanInput, this does not throw on an empty value.
77+
* Reads a boolean input, returning the default when the input is empty or
78+
* unrecognized. Unlike core.getBooleanInput, this does not throw on an empty
79+
* or unexpected value.
7980
*/
8081
private static _getBooleanInput(name: string, defaultValue: boolean): boolean {
8182
const raw = core.getInput(name).trim().toLowerCase();
82-
if (raw === '') {
83-
return defaultValue;
83+
if (raw === 'true') {
84+
return true;
8485
}
85-
return raw === 'true';
86+
if (raw === 'false') {
87+
return false;
88+
}
89+
return defaultValue;
8690
}
8791

8892
/**
@@ -98,7 +102,7 @@ export default class Reporter {
98102
report: this._readReport(result.reportPath),
99103
script: this._readScript(result.scriptPath),
100104
durationMs: result.durationMs,
101-
options: inputs.additionalArguments,
105+
options: this._redactSecrets(inputs.additionalArguments),
102106
actor: context.actor || undefined,
103107
commit: context.sha ? context.sha.substring(0, 7) : undefined,
104108
runUrl: this._buildRunUrl(context)
@@ -116,6 +120,18 @@ export default class Reporter {
116120
return `${serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
117121
}
118122

123+
/**
124+
* Redacts additional arguments that may contain secrets before they are
125+
* rendered into the summary or a pull request comment, neither of which is
126+
* covered by GitHub secret masking.
127+
*/
128+
private static _redactSecrets(args?: string): string | undefined {
129+
if (args && /password|pwd|secret|token/i.test(args)) {
130+
return '[redacted]';
131+
}
132+
return args;
133+
}
134+
119135
/**
120136
* Returns a human-readable label for the action that ran.
121137
*/
@@ -163,9 +179,11 @@ export default class Reporter {
163179
* Publishes the action outputs describing the deployment.
164180
*/
165181
private static _setOutputs(context: SummaryContext, result: IActionResult): void {
166-
const operations = context.report ? context.report.operations : [];
167-
core.setOutput('changes-detected', operations.length > 0 ? 'true' : 'false');
168-
core.setOutput('objects-changed', operations.length.toString());
182+
if (context.report) {
183+
const operations = context.report.operations;
184+
core.setOutput('changes-detected', operations.length > 0 ? 'true' : 'false');
185+
core.setOutput('objects-changed', operations.length.toString());
186+
}
169187

170188
if (result.reportPath) {
171189
core.setOutput('deployment-report-path', result.reportPath);
@@ -216,7 +234,7 @@ export default class Reporter {
216234
* Finds the id of a previously posted summary comment by its hidden marker.
217235
*/
218236
private static async _findExistingComment(octokit: ReturnType<typeof github.getOctokit>, owner: string, repo: string, issueNumber: number): Promise<number | undefined> {
219-
const { data: comments } = await octokit.rest.issues.listComments({ owner, repo, issue_number: issueNumber, per_page: 100 });
237+
const comments = await octokit.paginate(octokit.rest.issues.listComments, { owner, repo, issue_number: issueNumber, per_page: 100 });
220238
const existing = comments.find(comment => !!comment.body && comment.body.includes(SUMMARY_MARKER));
221239
return existing ? existing.id : undefined;
222240
}

src/main.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function getInputs(): IActionInputs {
9696
sqlpackagePath: core.getInput('sqlpackage-path') || undefined,
9797
additionalArguments: core.getInput('arguments') || undefined,
9898
skipFirewallCheck: core.getBooleanInput('skip-firewall-check'),
99-
captureDeploymentReport: true
99+
captureDeploymentReport: isReportingEnabled()
100100
} as IDacpacActionInputs;
101101

102102
case Constants.sqlprojExtension:
@@ -113,12 +113,26 @@ function getInputs(): IActionInputs {
113113
sqlpackagePath: core.getInput('sqlpackage-path') || undefined,
114114
additionalArguments: core.getInput('arguments') || undefined,
115115
skipFirewallCheck: core.getBooleanInput('skip-firewall-check'),
116-
captureDeploymentReport: true
116+
captureDeploymentReport: isReportingEnabled()
117117
} as IBuildAndPublishInputs;
118118

119119
default:
120120
throw new Error(`Invalid file type provided as input ${filePath}. File must be a .sql, .dacpac, or .sqlproj file.`)
121121
}
122122
}
123123

124+
/**
125+
* Determines whether deployment reporting is enabled, so the action only captures
126+
* a deployment report and script when a summary or pull request comment will be
127+
* produced. Mirrors the defaults used by the reporter: the summary is on unless
128+
* explicitly set to false, and the pull request comment is on unless set to off.
129+
*/
130+
function isReportingEnabled(): boolean {
131+
const summary = core.getInput('summary').trim().toLowerCase();
132+
const commentPr = core.getInput('comment-pr').trim().toLowerCase();
133+
const summaryEnabled = summary !== 'false';
134+
const commentEnabled = commentPr !== 'off';
135+
return summaryEnabled || commentEnabled;
136+
}
137+
124138
run();

0 commit comments

Comments
 (0)