Skip to content

Add deployment summary to the job summary and pull request comment - #279

Open
chhateauuu wants to merge 10 commits into
masterfrom
t-achhatkuli/dev/pr-deploy-summary
Open

Add deployment summary to the job summary and pull request comment#279
chhateauuu wants to merge 10 commits into
masterfrom
t-achhatkuli/dev/pr-deploy-summary

Conversation

@chhateauuu

Copy link
Copy Markdown

Summary

When sql-action deploys a database, the details of what actually changed are only visible in the build logs. A reviewer sees a green check and has to open the run and read the logs to learn what happened to the database.

This PR makes the action publish a concise deployment summary of what the deployment changed. It is written to the GitHub Actions job summary and, on pull requests, posted as a single auto-updating sticky comment. It reports the objects created, altered, and dropped, warns about possible data loss, and includes the full deployment T-SQL in a collapsible section.

The information already exists: SqlPackage computes the change set during every publish. This PR captures the deployment report and script it produces, parses them, formats a summary, and publishes it. No deployment logic is added or changed.

code-walkthrough.md

What it looks like

On a pull request that changes the schema, the action posts a summary like this:

SQL deployment summary

Published Database.sqlproj to myserver / mydb — 10 changes (5 created, 4 altered, 1 dropped), 3 data-loss warnings

A metadata table (action, target, duration, triggering user, commit, options), followed by:

Changes (10)

By-type and by-schema breakdowns, then collapsible Created / Altered / Dropped sections listing each object and its type.

Possible data loss (3)

  • The column [dbo].[Messages].[LegacyFlag] is being dropped, data loss could occur.
  • The table [dbo].[OldAudit] is being dropped, data loss could occur.

A collapsible block with the full deployment T-SQL script.

What changed

  • New src/DeploymentReport.ts: parses the SqlPackage deployment report XML into operations and alerts. Tolerant of missing or malformed input.
  • New src/DeploymentSummary.ts: a pure function that renders the summary Markdown.
  • New src/Reporter.ts: writes the job summary, posts or updates the sticky PR comment, and sets outputs. Best-effort; it can never fail a deployment.
  • src/AzureSqlAction.ts: during a Publish, captures the deployment report and script (respecting any caller-supplied paths) and returns their locations.
  • src/main.ts: measures the deploy duration and invokes the reporter after a successful deploy.
  • action.yml and README.md: document the new inputs and outputs.

The change is organized into small, logical commits so it can be reviewed step by step.

New inputs (all optional)

Input Default Description
summary true Write the deployment summary to the job summary.
comment-pr auto Post the summary as a sticky PR comment. One of off, auto, always.
github-token ${{ github.token }} Token used to post the comment.

New outputs

changes-detected, objects-changed, deployment-report-path, deployment-script-path.

Behavior and safety

  • Backward compatible. No existing input, output, or deploy behavior changes. Existing workflows run identically with no edits.
  • Reporting is fully guarded. Any error is logged as a warning; the deployment result is never affected.
  • No secrets. The target is shown as server and database only; user id and password are already masked and never appear in the summary.
  • The comment requires pull-requests: write. When the token or permission is missing, the action logs a warning and falls back to the job summary.
  • Object rows and the script are truncated with a clear marker so a comment cannot become excessively large; table cells escape pipe characters.

Compatibility notes

  • Two dependencies were added: fast-xml-parser (report parsing) and @actions/github (comment posting). @actions/github is pinned to the v6 CommonJS line for compatibility with the existing webpack build.
  • The committed lib/main.js grows from about 284 KB to 860 KB, driven mainly by the GitHub API client.
  • Open question: comment-pr defaults to auto. If a more conservative default is preferred, it can default to off (opt-in). This is a one-line change; happy to adjust.

Testing

  • New Jest suites cover the parser (real sample report plus single-node, empty, blank, and malformed input), the renderer (grouping, counts, breakdowns, data-loss, truncation, pipe escaping, byte-order-mark handling, and a check that no password is rendered), the reporter (comment created when absent, updated when present, warn-without-fail on missing token, non-PR, or permission errors), and the capture argument injection.
  • Full suite green at 177 tests; npm run build succeeds and the committed bundle is up to date.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds post-deployment reporting to sql-action by capturing SqlPackage’s deployment report/script, converting them into a concise Markdown summary, and publishing that summary to the GitHub Actions job summary and (optionally) a sticky pull request comment.

Changes:

  • Capture SqlPackage deployment report XML and deployment script during publish, and return their paths from the action execution.
  • Parse the deployment report and render a Markdown “deployment summary” including change breakdowns, data-loss warnings, and an embedded script section.
  • Add a reporter that writes the job summary, upserts a sticky PR comment, and sets new outputs describing changes and artifact paths.

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/Reporter.ts Implements summary publishing (job summary + sticky PR comment) and sets new outputs.
src/main.ts Measures deploy duration and invokes the reporter; enables deployment report capture in inputs.
src/DeploymentSummary.ts Renders GitHub-flavored Markdown summary (headline, metadata, changes, alerts, script).
src/DeploymentReport.ts Parses SqlPackage deployment report XML into operations and alerts.
src/AzureSqlAction.ts Adds optional capture of deployment report/script paths for publish executions.
README.md Documents new inputs/outputs and deployment summary behavior.
package.json Adds dependencies for GitHub API usage and XML parsing.
package-lock.json Locks the dependency graph for newly added packages.
lib/main.js.LICENSE.txt Updates bundled-license notices due to added dependencies.
action.yml Adds new inputs/outputs and continues to point to the bundled JS entrypoint.
tests/Reporter.test.ts Adds tests for summary publishing and sticky comment upsert behavior.
tests/main.test.ts Updates mocks to reflect execute() now returning an action result object.
tests/DeploymentSummary.test.ts Adds tests for Markdown rendering, grouping, truncation, and escaping.
tests/DeploymentReport.test.ts Adds tests for report XML parsing and malformed/edge inputs.
tests/AzureSqlAction.test.ts Adds tests verifying capture arg injection and path reuse behavior.
testdata/deployReport.xml Adds a representative deployment report fixture for parser tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Reporter.ts
Comment on lines +80 to +86
private static _getBooleanInput(name: string, defaultValue: boolean): boolean {
const raw = core.getInput(name).trim().toLowerCase();
if (raw === '') {
return defaultValue;
}
return raw === 'true';
}
Comment thread src/Reporter.ts
Comment on lines +165 to +176
private static _setOutputs(context: SummaryContext, result: IActionResult): void {
const operations = context.report ? context.report.operations : [];
core.setOutput('changes-detected', operations.length > 0 ? 'true' : 'false');
core.setOutput('objects-changed', operations.length.toString());

if (result.reportPath) {
core.setOutput('deployment-report-path', result.reportPath);
}
if (result.scriptPath) {
core.setOutput('deployment-script-path', result.scriptPath);
}
}
Comment thread src/main.ts
Comment on lines 113 to 117
sqlpackagePath: core.getInput('sqlpackage-path') || undefined,
additionalArguments: core.getInput('arguments') || undefined,
skipFirewallCheck: core.getBooleanInput('skip-firewall-check')
skipFirewallCheck: core.getBooleanInput('skip-firewall-check'),
captureDeploymentReport: true
} as IBuildAndPublishInputs;
Comment thread src/Reporter.ts
Comment on lines +218 to +222
private static async _findExistingComment(octokit: ReturnType<typeof github.getOctokit>, owner: string, repo: string, issueNumber: number): Promise<number | undefined> {
const { data: comments } = await octokit.rest.issues.listComments({ owner, repo, issue_number: issueNumber, per_page: 100 });
const existing = comments.find(comment => !!comment.body && comment.body.includes(SUMMARY_MARKER));
return existing ? existing.id : undefined;
}
Comment thread src/Reporter.ts
Comment on lines +99 to +105
script: this._readScript(result.scriptPath),
durationMs: result.durationMs,
options: inputs.additionalArguments,
actor: context.actor || undefined,
commit: context.sha ? context.sha.substring(0, 7) : undefined,
runUrl: this._buildRunUrl(context)
};
Comment thread package.json
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/exec": "^1.0.1",
"@actions/github": "^6.0.0",
Comment thread src/DeploymentReport.ts
alerts: DeploymentAlert[];
}

const EMPTY_REPORT: DeploymentReport = { operations: [], alerts: [] };
Comment thread src/Reporter.ts
Comment on lines +210 to +212
catch (error) {
core.warning(`Unable to post deployment summary comment (ensure the workflow grants 'pull-requests: write'): ${error.message}`);
}
Comment on lines +125 to +133
it('warns without throwing when the comment API fails', async () => {
mockInputs({ summary: 'false', 'comment-pr': 'auto', 'github-token': 'token' });
(github.getOctokit as jest.Mock).mockReturnValue({
rest: { issues: { listComments: jest.fn().mockRejectedValue(new Error('forbidden')) } }
});

await expect(Reporter.report(inputs, {})).resolves.toBeUndefined();
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('pull-requests: write'));
});
Comment thread README.md
Comment on lines +65 to +70
To let the action post the pull request comment, grant the workflow `pull-requests: write`:

```yaml
permissions:
pull-requests: write
```
Comment thread lib/main.js.LICENSE.txt
@@ -0,0 +1,3 @@
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's with this file?

@Benjin

Benjin commented Jul 29, 2026

Copy link
Copy Markdown
Member

@chhateauuu can you go through the copilot suggestions, determine which are valid, and either address or close them all?

Comment thread src/DeploymentReport.ts
try {
parsed = parser.parse(xml);
} catch {
return { operations: [], alerts: [] };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the behavior if parsing fails? Does it create a status comment saying it failed, or do we swallow failures?

Comment thread src/DeploymentReport.ts
return 'Object';
}

const withoutPrefix = type.replace(/^Sql/, '');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For something simple like string.StartsWith("Sql"), it's better to manipulate the string directly instead of using regex. It's more readable and performant.

Comment thread src/DeploymentSummary.ts
const OPERATION_GROUPS: OperationGroup[] = [
{ key: 'create', label: 'Created', icon: '➕' },
{ key: 'alter', label: 'Altered', icon: '🔄' },
{ key: 'drop', label: 'Dropped', icon: '🗑️' }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe ❌ would be more intuitive here than a trashcan emoji?

Comment thread src/DeploymentSummary.ts
* unescaped pipe would otherwise be interpreted as a column separator.
*/
function escapeCell(value: string): string {
return value.replace(/\|/g, '\\|');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you go through all of the regex uses in this PR and use simple string manipulations where possible? string.replaceAll() here, for example

@Benjin
Benjin self-requested a review July 29, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants