Add deployment summary to the job summary and pull request comment - #279
Add deployment summary to the job summary and pull request comment#279chhateauuu wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
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.
| private static _getBooleanInput(name: string, defaultValue: boolean): boolean { | ||
| const raw = core.getInput(name).trim().toLowerCase(); | ||
| if (raw === '') { | ||
| return defaultValue; | ||
| } | ||
| return raw === 'true'; | ||
| } |
| 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); | ||
| } | ||
| } |
| 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; |
| 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; | ||
| } |
| 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) | ||
| }; |
| "dependencies": { | ||
| "@actions/core": "^1.9.1", | ||
| "@actions/exec": "^1.0.1", | ||
| "@actions/github": "^6.0.0", |
| alerts: DeploymentAlert[]; | ||
| } | ||
|
|
||
| const EMPTY_REPORT: DeploymentReport = { operations: [], alerts: [] }; |
| catch (error) { | ||
| core.warning(`Unable to post deployment summary comment (ensure the workflow grants 'pull-requests: write'): ${error.message}`); | ||
| } |
| 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')); | ||
| }); |
| To let the action post the pull request comment, grant the workflow `pull-requests: write`: | ||
|
|
||
| ```yaml | ||
| permissions: | ||
| pull-requests: write | ||
| ``` |
| @@ -0,0 +1,3 @@ | |||
| /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */ | |||
|
@chhateauuu can you go through the copilot suggestions, determine which are valid, and either address or close them all? |
| try { | ||
| parsed = parser.parse(xml); | ||
| } catch { | ||
| return { operations: [], alerts: [] }; |
There was a problem hiding this comment.
What's the behavior if parsing fails? Does it create a status comment saying it failed, or do we swallow failures?
| return 'Object'; | ||
| } | ||
|
|
||
| const withoutPrefix = type.replace(/^Sql/, ''); |
There was a problem hiding this comment.
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.
| const OPERATION_GROUPS: OperationGroup[] = [ | ||
| { key: 'create', label: 'Created', icon: '➕' }, | ||
| { key: 'alter', label: 'Altered', icon: '🔄' }, | ||
| { key: 'drop', label: 'Dropped', icon: '🗑️' } |
There was a problem hiding this comment.
Maybe ❌ would be more intuitive here than a trashcan emoji?
| * unescaped pipe would otherwise be interpreted as a column separator. | ||
| */ | ||
| function escapeCell(value: string): string { | ||
| return value.replace(/\|/g, '\\|'); |
There was a problem hiding this comment.
Can you go through all of the regex uses in this PR and use simple string manipulations where possible? string.replaceAll() here, for example
Summary
When
sql-actiondeploys 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:
What changed
src/DeploymentReport.ts: parses the SqlPackage deployment report XML into operations and alerts. Tolerant of missing or malformed input.src/DeploymentSummary.ts: a pure function that renders the summary Markdown.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.ymlandREADME.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)
summarytruecomment-prautooff,auto,always.github-token${{ github.token }}New outputs
changes-detected,objects-changed,deployment-report-path,deployment-script-path.Behavior and safety
pull-requests: write. When the token or permission is missing, the action logs a warning and falls back to the job summary.Compatibility notes
fast-xml-parser(report parsing) and@actions/github(comment posting).@actions/githubis pinned to the v6 CommonJS line for compatibility with the existing webpack build.lib/main.jsgrows from about 284 KB to 860 KB, driven mainly by the GitHub API client.comment-prdefaults toauto. If a more conservative default is preferred, it can default tooff(opt-in). This is a one-line change; happy to adjust.Testing
npm run buildsucceeds and the committed bundle is up to date.