From 60d7f9bd8e93bcb8c074d3dc205e9768c02627c8 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Tue, 2 Dec 2025 11:12:07 +0530 Subject: [PATCH 1/3] adding changes for returning violations only for the changed files --- README.md | 54 ++++++++++ __tests__/dependencies.test.ts | 6 +- __tests__/fakes.ts | 9 +- __tests__/main.test.ts | 99 ++++++++++++++++-- __tests__/summary.test.ts | 151 +++++++++++++++++++++++++++ action.yml | 7 ++ dist/index.js | 179 ++++++++++++++++++++++----------- src/dependencies.ts | 3 +- src/main.ts | 152 ++++++++++++++++++---------- src/summary.ts | 68 +++++++++---- src/types.ts | 1 + 11 files changed, 581 insertions(+), 148 deletions(-) diff --git a/README.md b/README.md index a865b7c..82e6550 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,13 @@ The `forcedotcom/run-code-analyzer@v2` GitHub Action is based on [Salesforce Cod * When this action is run against a pull request, you can provide a GitHub token, which is used to create a review of the pull request. The review specifies how many violations were found (both in the project as a whole and in changed files) and links to the action summary page. * This token must have write permissions for pull requests. * You can use the default GitHub token stored as the `GITHUB_TOKEN` secret, as long as you also use the job-level `permissions` property to give that token write access for pull requests. +* `changed-files-only` *(Default: `false`)* + * When set to `true`, only violations in files that were changed in the pull request will be: + * Shown in the summary + * Counted in the output variables (`num-violations`, `num-sev1-violations`, etc.) + * Violations in unchanged files will be completely excluded from both the summary and the counts. + * This option only has effect when running on a pull request with a `github-token` provided. + * **Use Case:** Enable quality gates that only fail on violations in files changed by the PR, rather than all violations in the entire codebase. This is useful when introducing code analysis to a legacy codebase with existing violations. ## v2 Outputs * `exit-code` @@ -107,6 +114,53 @@ The [Salesforce Code Analyzer v5.x](https://developer.salesforce.com/docs/platfo steps.run-code-analyzer.outputs.num-violations > 10 run: exit 1 +## Example v2 Usage with Changed Files Only + +If you want to focus only on violations in files that were changed in a pull request, use the `changed-files-only` input. When enabled, the output counts will **only include violations from changed files**, making it easy to set up quality gates that don't fail on existing violations in unchanged code. + + name: Salesforce Code Analyzer Workflow (Changed Files Only) + on: + pull_request: + jobs: + salesforce-code-analyzer-workflow: + permissions: + pull-requests: write + contents: read + actions: read + runs-on: ubuntu-latest + steps: + - name: Check out files + uses: actions/checkout@v5 + + - name: Install Salesforce CLI + run: npm install -g @salesforce/cli@latest + + - name: Install Latest Salesforce Code Analyzer CLI Plugin + run: sf plugins install code-analyzer@latest + + - name: Run Salesforce Code Analyzer (Changed Files Only) + id: run-code-analyzer + uses: forcedotcom/run-code-analyzer@v2 + with: + run-arguments: --workspace . --view detail --output-file sfca_results.json + results-artifact-name: salesforce-code-analyzer-results + github-token: ${{ github.token }} + changed-files-only: true + + - name: Quality Gate - Only Fail on Violations in Changed Files + if: | + steps.run-code-analyzer.outputs.num-sev1-violations > 0 || + steps.run-code-analyzer.outputs.num-sev2-violations > 0 || + steps.run-code-analyzer.outputs.num-violations > 10 + run: | + echo "Quality gate failed: Found violations in changed files" + echo " Critical (Sev 1): ${{ steps.run-code-analyzer.outputs.num-sev1-violations }}" + echo " High (Sev 2): ${{ steps.run-code-analyzer.outputs.num-sev2-violations }}" + echo " Total: ${{ steps.run-code-analyzer.outputs.num-violations }}" + exit 1 + +**Note:** When `changed-files-only: true` is set, all output counts (`num-violations`, `num-sev1-violations`, etc.) automatically reflect only violations in changed files. You can use the same quality gate conditions you would normally use, and they will only consider violations in files modified by the PR. + # Version: v1 The `forcedotcom/run-code-analyzer@v1` GitHub Action is based on [Salesforce Code Analyzer v4.x](https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/code-analyzer-3x.html), which is the original `@salesforce/sfdx-scanner` Salesforce CLI plugin. diff --git a/__tests__/dependencies.test.ts b/__tests__/dependencies.test.ts index f29c512..a57b52a 100644 --- a/__tests__/dependencies.test.ts +++ b/__tests__/dependencies.test.ts @@ -62,11 +62,15 @@ describe('RuntimeDependencies Code Coverage', () => { jest.spyOn(core, 'getInput').mockImplementation((name: string): string => { return `${name} Value` }) + jest.spyOn(core, 'getBooleanInput').mockImplementation((name: string): boolean => { + return name === 'changed-files-only' ? false : true + }) const inputs: Inputs = dependencies.getInputs() expect(inputs).toEqual({ runArguments: 'run-arguments Value', resultsArtifactName: 'results-artifact-name Value', - githubToken: 'github-token Value' + githubToken: 'github-token Value', + changedFilesOnly: false }) }) diff --git a/__tests__/fakes.ts b/__tests__/fakes.ts index 7aa7feb..2ad47d9 100644 --- a/__tests__/fakes.ts +++ b/__tests__/fakes.ts @@ -33,7 +33,8 @@ export class FakeDependencies implements Dependencies { getInputsReturnValue: Inputs = { runArguments: '--view detail --output-file sfca_results.json', resultsArtifactName: 'salesforce-code-analyzer-results', - githubToken: 'dummyToken' + githubToken: 'dummyToken', + changedFilesOnly: false } getInputsCallCount = 0 getInputs(): Inputs { @@ -242,9 +243,9 @@ export class FakeViolationLocation implements ViolationLocation { export class FakeSummarizer implements Summarizer { createSummaryMarkdownReturnValue = 'someSummaryMarkdown' - createSummaryMarkdownCallHistory: { results: Results }[] = [] - createSummaryMarkdown(results: Results): string { - this.createSummaryMarkdownCallHistory.push({ results }) + createSummaryMarkdownCallHistory: { results: Results; changedFiles?: string[]; changedFilesOnly?: boolean }[] = [] + createSummaryMarkdown(results: Results, changedFiles?: string[], changedFilesOnly?: boolean): string { + this.createSummaryMarkdownCallHistory.push({ results, changedFiles, changedFilesOnly }) return this.createSummaryMarkdownReturnValue } } diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 62672d8..e0c4592 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,7 +1,15 @@ import * as main from '../src/main' -import { FakeCommandExecutor, FakeDependencies, FakeResultsFactory, FakeSummarizer } from './fakes' +import { + FakeCommandExecutor, + FakeDependencies, + FakeResults, + FakeResultsFactory, + FakeSummarizer, + FakeViolationLocation +} from './fakes' import { Inputs } from '../src/types' import { MESSAGE_FCNS, MESSAGES, MIN_CODE_ANALYZER_VERSION_REQUIRED } from '../src/constants' +import { RuntimeViolation } from '../src/results' describe('main run Tests', () => { let dependencies: FakeDependencies @@ -81,7 +89,9 @@ describe('main run Tests', () => { expect(summarizer.createSummaryMarkdownCallHistory).toHaveLength(1) expect(summarizer.createSummaryMarkdownCallHistory).toContainEqual({ - results: resultsFactory.createResultsReturnValue + results: resultsFactory.createResultsReturnValue, + changedFiles: [], + changedFilesOnly: false }) expect(dependencies.writeSummaryCallHistory).toHaveLength(1) @@ -93,7 +103,8 @@ describe('main run Tests', () => { it('Test user supplies non-default inputs with various output files including json', async () => { dependencies.getInputsReturnValue = { runArguments: '-f myFile.html --output-file=another.xml -f=great.json --output-file cool.sarif -w ./src', - resultsArtifactName: 'customArtifactName' + resultsArtifactName: 'customArtifactName', + changedFilesOnly: false } await main.run(dependencies, commandExecutor, resultsFactory, summarizer) @@ -127,7 +138,8 @@ describe('main run Tests', () => { dependencies.getInputsReturnValue = { runArguments: '-f myFile.html --view table', resultsArtifactName: 'salesforce-code-analyzer-results', - githubToken: 'dummyToken' + githubToken: 'dummyToken', + changedFilesOnly: false } dependencies.isPullRequestReturnValue = true dependencies.getChangedFilesCallback = async () => ['fakeFile'] // Match the file from FakeViolationLocation to get resultsInChangedFilesCount > 0 @@ -162,7 +174,8 @@ describe('main run Tests', () => { it('When running on a pull request but missing github token, then a review is not created and an info message is logged', async () => { dependencies.getInputsReturnValue = { runArguments: '-f myFile.html --view table', - resultsArtifactName: 'salesforce-code-analyzer-results' + resultsArtifactName: 'salesforce-code-analyzer-results', + changedFilesOnly: false } dependencies.isPullRequestReturnValue = true await main.run(dependencies, commandExecutor, resultsFactory, summarizer) @@ -174,7 +187,7 @@ describe('main run Tests', () => { expect(dependencies.warnCallHistory).toHaveLength(0) expect(dependencies.infoCallHistory).toHaveLength(2) - expect(dependencies.infoCallHistory[1].infoMessage).toEqual(MESSAGES.PR_FOUND_WITHOUT_GH_TOKEN) + expect(dependencies.infoCallHistory[0].infoMessage).toEqual(MESSAGES.PR_FOUND_WITHOUT_GH_TOKEN) }) it.each([ @@ -214,7 +227,8 @@ describe('main run Tests', () => { dependencies.getInputsReturnValue = { runArguments: '-f myFile.html --view table', resultsArtifactName: 'salesforce-code-analyzer-results', - githubToken: 'dummyToken' + githubToken: 'dummyToken', + changedFilesOnly: false } dependencies.isPullRequestReturnValue = isPullRequest dependencies.getChangedFilesCallback = getChangedFilesCallback @@ -246,7 +260,8 @@ describe('main run Tests', () => { it('Test user supplies non-default inputs with non-json output file', async () => { dependencies.getInputsReturnValue = { runArguments: '-f myFile.html --view table', - resultsArtifactName: 'salesforce-code-analyzer-results' + resultsArtifactName: 'salesforce-code-analyzer-results', + changedFilesOnly: false } await main.run(dependencies, commandExecutor, resultsFactory, summarizer) @@ -272,7 +287,8 @@ describe('main run Tests', () => { it('Test user supplies non-default inputs with zero output files and no view', async () => { dependencies.getInputsReturnValue = { runArguments: '', - resultsArtifactName: 'salesforce-code-analyzer-results' + resultsArtifactName: 'salesforce-code-analyzer-results', + changedFilesOnly: false } await main.run(dependencies, commandExecutor, resultsFactory, summarizer) @@ -298,7 +314,8 @@ describe('main run Tests', () => { it('Test user supplies non-default inputs with zero output files but supplies a view', async () => { dependencies.getInputsReturnValue = { runArguments: '-c someConfig.yml --view detail', - resultsArtifactName: 'salesforce-code-analyzer-results' + resultsArtifactName: 'salesforce-code-analyzer-results', + changedFilesOnly: false } await main.run(dependencies, commandExecutor, resultsFactory, summarizer) @@ -443,7 +460,8 @@ describe('main run Tests', () => { it('Test when the user output file does not exist after run then we fail', async () => { dependencies.getInputsReturnValue = { runArguments: '-f userResults.xml', - resultsArtifactName: 'customArtifactName' + resultsArtifactName: 'customArtifactName', + changedFilesOnly: false } dependencies.fileExistsReturnValue = false await main.run(dependencies, commandExecutor, resultsFactory, summarizer) @@ -452,4 +470,63 @@ describe('main run Tests', () => { expect(dependencies.failCallHistory).toHaveLength(1) expect(dependencies.failCallHistory[0].failMessage).toContain(MESSAGE_FCNS.FILE_NOT_FOUND('userResults.xml')) }) + + it('When changed-files-only is true, outputs reflect only violations in changed files', async () => { + dependencies.getInputsReturnValue = { + runArguments: '--view detail --output-file sfca_results.json', + resultsArtifactName: 'salesforce-code-analyzer-results', + githubToken: 'dummyToken', + changedFilesOnly: true + } + dependencies.isPullRequestReturnValue = true + dependencies.getChangedFilesCallback = async () => ['changedFile.ts'] // Only one file changed + + // Create violations with specific file locations + const changedFileLocation = new FakeViolationLocation() + changedFileLocation.getFileReturnValue = 'changedFile.ts' + + const unchangedFileLocation1 = new FakeViolationLocation() + unchangedFileLocation1.getFileReturnValue = 'unchangedFile1.ts' + + const unchangedFileLocation2 = new FakeViolationLocation() + unchangedFileLocation2.getFileReturnValue = 'unchangedFile2.ts' + + // Set up results with violations in both changed and unchanged files + const fakeResults = resultsFactory.createResultsReturnValue as FakeResults + fakeResults.getViolationsSortedBySeverityReturnValue = [ + // This violation is in the changed file + new RuntimeViolation(1, 'engine1', 'rule1', undefined, 'message1', 0, [changedFileLocation]), + // These violations are in unchanged files + new RuntimeViolation(1, 'engine1', 'rule2', undefined, 'message2', 0, [unchangedFileLocation1]), + new RuntimeViolation(2, 'engine1', 'rule3', undefined, 'message3', 0, [unchangedFileLocation1]), + new RuntimeViolation(3, 'engine1', 'rule4', undefined, 'message4', 0, [unchangedFileLocation2]) + ] + + await main.run(dependencies, commandExecutor, resultsFactory, summarizer) + + // Outputs should only count the 1 violation in the changed file + expect(dependencies.setOutputCallHistory).toContainEqual({ + name: 'num-violations', + value: '1' + }) + expect(dependencies.setOutputCallHistory).toContainEqual({ + name: 'num-sev1-violations', + value: '1' + }) + expect(dependencies.setOutputCallHistory).toContainEqual({ + name: 'num-sev2-violations', + value: '0' + }) + expect(dependencies.setOutputCallHistory).toContainEqual({ + name: 'num-sev3-violations', + value: '0' + }) + + // Summary should be called with changedFilesOnly=true + expect(summarizer.createSummaryMarkdownCallHistory).toContainEqual({ + results: resultsFactory.createResultsReturnValue, + changedFiles: ['changedFile.ts'], + changedFilesOnly: true + }) + }) }) diff --git a/__tests__/summary.test.ts b/__tests__/summary.test.ts index 11338bc..99c61bb 100644 --- a/__tests__/summary.test.ts +++ b/__tests__/summary.test.ts @@ -178,4 +178,155 @@ describe('RuntimeSummarizer Tests', () => { ) expect(summaryMarkdown).toEqual(expectedSummaryMarkdown) }) + + describe('Changed files only mode', () => { + it('When changedFilesOnly is true, only violations in changed files are shown', () => { + const results: FakeResults = new FakeResults() + results.getTotalViolationCountReturnValue = 6 + results.getSev1ViolationCountReturnValue = 1 + results.getSev2ViolationCountReturnValue = 1 + results.getSev3ViolationCountReturnValue = 2 + results.getSev4ViolationCountReturnValue = 1 + results.getSev5ViolationCountReturnValue = 1 + results.getViolationsSortedBySeverityReturnValue = [] + + // Create violations in changed files + const changedFileLocation1 = new FakeViolationLocation() + changedFileLocation1.getFileReturnValue = 'changed-file1.ts' + changedFileLocation1.toStringReturnValue = 'changed-file1.ts:1:0' + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(1, 'engine1', 'rule1', undefined, 'message1', 0, [changedFileLocation1]) + ) + + const changedFileLocation2 = new FakeViolationLocation() + changedFileLocation2.getFileReturnValue = 'changed-file2.ts' + changedFileLocation2.toStringReturnValue = 'changed-file2.ts:1:0' + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(2, 'engine1', 'rule2', undefined, 'message2', 0, [changedFileLocation2]) + ) + + // Create violations in unchanged files + const unchangedFileLocation1 = new FakeViolationLocation() + unchangedFileLocation1.getFileReturnValue = 'unchanged-file1.ts' + unchangedFileLocation1.toStringReturnValue = 'unchanged-file1.ts:1:0' + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(3, 'engine1', 'rule3', undefined, 'message3', 0, [unchangedFileLocation1]) + ) + + const unchangedFileLocation2 = new FakeViolationLocation() + unchangedFileLocation2.getFileReturnValue = 'unchanged-file2.ts' + unchangedFileLocation2.toStringReturnValue = 'unchanged-file2.ts:1:0' + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(3, 'engine1', 'rule4', undefined, 'message4', 0, [unchangedFileLocation2]) + ) + + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(4, 'engine1', 'rule5', undefined, 'message5', 0, [unchangedFileLocation1]) + ) + + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(5, 'engine1', 'rule6', undefined, 'message6', 0, [unchangedFileLocation2]) + ) + + const changedFiles = ['changed-file1.ts', 'changed-file2.ts'] + const summaryMarkdown = summarizer.createSummaryMarkdown(results, changedFiles, true) + + // Should only show 2 violations in changed files + expect(summaryMarkdown).toContain('2 Violation(s) Found in Changed Files') + expect(summaryMarkdown).toContain(':black_circle: 1 Critical severity violation(s)') + expect(summaryMarkdown).toContain(':red_circle: 1 High severity violation(s)') + expect(summaryMarkdown).toContain(':orange_circle: 0 Medium severity violation(s)') + expect(summaryMarkdown).toContain(':yellow_circle: 0 Low severity violation(s)') + expect(summaryMarkdown).toContain(':white_circle: 0 Info severity violation(s)') + + // Should show changed file violations + expect(summaryMarkdown).toContain('changed-file1.ts') + expect(summaryMarkdown).toContain('changed-file2.ts') + + // Should NOT show unchanged file violations + expect(summaryMarkdown).not.toContain('unchanged-file1.ts') + expect(summaryMarkdown).not.toContain('unchanged-file2.ts') + + // Should NOT show the collapsible sections + expect(summaryMarkdown).not.toContain('violations in files changed by this pull request') + expect(summaryMarkdown).not.toContain('violations in files unchanged by this pull request') + }) + + it('When changedFilesOnly is true but no changed files have violations, shows zero violations', () => { + const results: FakeResults = new FakeResults() + results.getTotalViolationCountReturnValue = 2 + results.getSev1ViolationCountReturnValue = 0 + results.getSev2ViolationCountReturnValue = 0 + results.getSev3ViolationCountReturnValue = 2 + results.getSev4ViolationCountReturnValue = 0 + results.getSev5ViolationCountReturnValue = 0 + results.getViolationsSortedBySeverityReturnValue = [] + + // Create violations only in unchanged files + const unchangedFileLocation = new FakeViolationLocation() + unchangedFileLocation.getFileReturnValue = 'unchanged-file.ts' + unchangedFileLocation.toStringReturnValue = 'unchanged-file.ts:1:0' + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(3, 'engine1', 'rule1', undefined, 'message1', 0, [unchangedFileLocation]) + ) + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(3, 'engine1', 'rule2', undefined, 'message2', 0, [unchangedFileLocation]) + ) + + const changedFiles = ['changed-file1.ts', 'changed-file2.ts'] + const summaryMarkdown = summarizer.createSummaryMarkdown(results, changedFiles, true) + + // Should show zero violations + expect(summaryMarkdown).toContain('0 Violations Found in Changed Files') + expect(summaryMarkdown).not.toContain('unchanged-file.ts') + }) + + it('When changedFilesOnly is false, shows both changed and unchanged file violations', () => { + const results: FakeResults = new FakeResults() + results.getTotalViolationCountReturnValue = 4 + results.getSev1ViolationCountReturnValue = 1 + results.getSev2ViolationCountReturnValue = 1 + results.getSev3ViolationCountReturnValue = 1 + results.getSev4ViolationCountReturnValue = 1 + results.getSev5ViolationCountReturnValue = 0 + results.getViolationsSortedBySeverityReturnValue = [] + + // Create violations in changed files + const changedFileLocation = new FakeViolationLocation() + changedFileLocation.getFileReturnValue = 'changed-file.ts' + changedFileLocation.toStringReturnValue = 'changed-file.ts:1:0' + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(1, 'engine1', 'rule1', undefined, 'message1', 0, [changedFileLocation]) + ) + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(2, 'engine1', 'rule2', undefined, 'message2', 0, [changedFileLocation]) + ) + + // Create violations in unchanged files + const unchangedFileLocation = new FakeViolationLocation() + unchangedFileLocation.getFileReturnValue = 'unchanged-file.ts' + unchangedFileLocation.toStringReturnValue = 'unchanged-file.ts:1:0' + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(3, 'engine1', 'rule3', undefined, 'message3', 0, [unchangedFileLocation]) + ) + results.getViolationsSortedBySeverityReturnValue.push( + new RuntimeViolation(4, 'engine1', 'rule4', undefined, 'message4', 0, [unchangedFileLocation]) + ) + + const changedFiles = ['changed-file.ts'] + const summaryMarkdown = summarizer.createSummaryMarkdown(results, changedFiles, false) + + // Should show all 4 violations + expect(summaryMarkdown).toContain('4 Violation(s) Found') + expect(summaryMarkdown).not.toContain('in Changed Files') + + // Should show collapsible sections + expect(summaryMarkdown).toContain('2 violations in files changed by this pull request') + expect(summaryMarkdown).toContain('2 violations in files unchanged by this pull request') + + // Should show both changed and unchanged file violations + expect(summaryMarkdown).toContain('changed-file.ts') + expect(summaryMarkdown).toContain('unchanged-file.ts') + }) + }) }) diff --git a/action.yml b/action.yml index ac85482..3f7eec3 100644 --- a/action.yml +++ b/action.yml @@ -33,6 +33,13 @@ inputs: This token must have write permissions for pull requests. required: false + changed-files-only: + description: |- + When set to true, only show violations in files that were changed in the pull request. Violations in unchanged files will be excluded from the summary and counts. + This option only has effect when running on a pull request with a github-token provided. + Default: false + required: false + default: 'false' outputs: exit-code: diff --git a/dist/index.js b/dist/index.js index 1561a32..29d2d08 100644 --- a/dist/index.js +++ b/dist/index.js @@ -102598,7 +102598,8 @@ class RuntimeDependencies { return { runArguments: core.getInput('run-arguments'), resultsArtifactName: core.getInput('results-artifact-name'), - githubToken: core.getInput('github-token') + githubToken: core.getInput('github-token'), + changedFilesOnly: core.getBooleanInput('changed-files-only') }; } async getChangedFiles(githubToken) { @@ -102771,57 +102772,21 @@ async function run(dependencies, commandExecutor, resultsFactory, summarizer) { dependencies.startGroup(constants_1.MESSAGES.STEP_LABELS.ANALYZING_RESULTS); assertFileExists(dependencies, jsonOutputFile); const results = resultsFactory.createResults(jsonOutputFile); - dependencies.setOutput('num-violations', results.getTotalViolationCount().toString()); - dependencies.setOutput('num-sev1-violations', results.getSev1ViolationCount().toString()); - dependencies.setOutput('num-sev2-violations', results.getSev2ViolationCount().toString()); - dependencies.setOutput('num-sev3-violations', results.getSev3ViolationCount().toString()); - dependencies.setOutput('num-sev4-violations', results.getSev4ViolationCount().toString()); - dependencies.setOutput('num-sev5-violations', results.getSev5ViolationCount().toString()); - dependencies.info(`outputs:\n` + - ` exit-code: ${codeAnalyzerOutput.exitCode}\n` + - ` num-violations: ${results.getTotalViolationCount()}\n` + - ` num-sev1-violations: ${results.getSev1ViolationCount()}\n` + - ` num-sev2-violations: ${results.getSev2ViolationCount()}\n` + - ` num-sev3-violations: ${results.getSev3ViolationCount()}\n` + - ` num-sev4-violations: ${results.getSev4ViolationCount()}\n` + - ` num-sev5-violations: ${results.getSev5ViolationCount()}`); dependencies.endGroup(); dependencies.startGroup(constants_1.MESSAGES.STEP_LABELS.CREATING_SUMMARY); + let changedFiles = []; + let couldReadChangedFiles = false; + // Get changed files for PR context if (dependencies.isPullRequest() && inputs.githubToken) { - let changedFiles = []; - let couldReadChangedFiles = true; try { dependencies.info(constants_1.MESSAGES.CALCULATING_CHANGED_FILES); changedFiles = await dependencies.getChangedFiles(inputs.githubToken); + couldReadChangedFiles = true; dependencies.info(constants_1.MESSAGES.CALCULATED_CHANGED_FILES); } catch (error) { - couldReadChangedFiles = false; dependencies.warn(constants_1.MESSAGE_FCNS.FAILED_TO_GET_CHANGED_FILES((0, utils_1.getFullErrorMessage)(error))); } - const summaryMarkdown = summarizer.createSummaryMarkdown(results, changedFiles); - if (couldReadChangedFiles) { - const summaryLink = await dependencies.createActionSummaryLink(inputs.githubToken); - const changedFilesSet = new Set(changedFiles); - const violationsInChangedFilesCount = results - .getViolationsSortedBySeverity() - .filter((v) => v - .getLocations() - .map(l => l.getFile()) - .some(f => f && changedFilesSet.has(f))).length; - const summaryBody = constants_1.MESSAGE_FCNS.REVIEW_BODY(results.getTotalViolationCount(), violationsInChangedFilesCount, summaryLink); - try { - dependencies.info(constants_1.MESSAGES.ATTEMPTING_TO_CREATE_PR_REVIEW); - const reviewId = await dependencies.createPullRequestReview(inputs.githubToken, summaryBody); - dependencies.setOutput('review-id', `${reviewId}`); - dependencies.info(constants_1.MESSAGE_FCNS.CREATED_PR_REVIEW(reviewId)); - } - catch (error) { - dependencies.warn(constants_1.MESSAGE_FCNS.FAILED_TO_CREATE_REVIEW((0, utils_1.getFullErrorMessage)(error))); - } - } - await dependencies.writeSummary(summaryMarkdown); - dependencies.endGroup(); } else { if (dependencies.isPullRequest()) { @@ -102830,15 +102795,83 @@ async function run(dependencies, commandExecutor, resultsFactory, summarizer) { else { dependencies.info(constants_1.MESSAGES.NOT_PR); } - const summaryMarkdown = summarizer.createSummaryMarkdown(results); - await dependencies.writeSummary(summaryMarkdown); - dependencies.endGroup(); } + // Calculate violation counts based on mode + const violationCounts = calculateViolationCounts(results, inputs.changedFilesOnly && couldReadChangedFiles ? changedFiles : undefined); + // Set outputs with final counts + dependencies.setOutput('num-violations', violationCounts.total.toString()); + dependencies.setOutput('num-sev1-violations', violationCounts.sev1.toString()); + dependencies.setOutput('num-sev2-violations', violationCounts.sev2.toString()); + dependencies.setOutput('num-sev3-violations', violationCounts.sev3.toString()); + dependencies.setOutput('num-sev4-violations', violationCounts.sev4.toString()); + dependencies.setOutput('num-sev5-violations', violationCounts.sev5.toString()); + dependencies.info(`outputs:\n` + + ` exit-code: ${codeAnalyzerOutput.exitCode}\n` + + ` num-violations: ${violationCounts.total}\n` + + ` num-sev1-violations: ${violationCounts.sev1}\n` + + ` num-sev2-violations: ${violationCounts.sev2}\n` + + ` num-sev3-violations: ${violationCounts.sev3}\n` + + ` num-sev4-violations: ${violationCounts.sev4}\n` + + ` num-sev5-violations: ${violationCounts.sev5}`); + // Generate summary + const summaryMarkdown = summarizer.createSummaryMarkdown(results, changedFiles, inputs.changedFilesOnly); + // Create PR review if applicable + if (dependencies.isPullRequest() && inputs.githubToken && couldReadChangedFiles) { + const summaryLink = await dependencies.createActionSummaryLink(inputs.githubToken); + const changedFilesSet = new Set(changedFiles); + const violationsInChangedFilesCount = results + .getViolationsSortedBySeverity() + .filter((v) => v + .getLocations() + .map(l => l.getFile()) + .some(f => f && changedFilesSet.has(f))).length; + const summaryBody = constants_1.MESSAGE_FCNS.REVIEW_BODY(results.getTotalViolationCount(), violationsInChangedFilesCount, summaryLink); + try { + dependencies.info(constants_1.MESSAGES.ATTEMPTING_TO_CREATE_PR_REVIEW); + const reviewId = await dependencies.createPullRequestReview(inputs.githubToken, summaryBody); + dependencies.setOutput('review-id', `${reviewId}`); + dependencies.info(constants_1.MESSAGE_FCNS.CREATED_PR_REVIEW(reviewId)); + } + catch (error) { + dependencies.warn(constants_1.MESSAGE_FCNS.FAILED_TO_CREATE_REVIEW((0, utils_1.getFullErrorMessage)(error))); + } + } + await dependencies.writeSummary(summaryMarkdown); + dependencies.endGroup(); } catch (error) { dependencies.fail(`${constants_1.MESSAGES.UNEXPECTED_ERROR}\n\n${(0, utils_1.getFullErrorMessage)(error)}`); } } +/** + * Calculate violation counts, optionally filtered by changed files + * @param results - The full results from the code analyzer + * @param changedFiles - Optional array of changed file paths to filter by + * @returns Object containing counts for each severity level and total + */ +function calculateViolationCounts(results, changedFiles) { + let violations; + if (changedFiles && changedFiles.length > 0) { + // Filter to only violations in changed files + const changedFilesSet = new Set(changedFiles); + violations = results.getViolationsSortedBySeverity().filter((v) => v + .getLocations() + .map(l => l.getFile()) + .some(f => f && changedFilesSet.has(f))); + } + else { + // Use all violations + violations = results.getViolationsSortedBySeverity(); + } + return { + total: violations.length, + sev1: violations.filter(v => v.getSeverity() === 1).length, + sev2: violations.filter(v => v.getSeverity() === 2).length, + sev3: violations.filter(v => v.getSeverity() === 3).length, + sev4: violations.filter(v => v.getSeverity() === 4).length, + sev5: violations.filter(v => v.getSeverity() === 5).length + }; +} async function installSalesforceCliIfNeeded(dependencies, commandExecutor) { if (!(await commandExecutor.isSalesforceCliInstalled())) { dependencies.warn(constants_1.MESSAGES.SF_CLI_NOT_INSTALLED); @@ -103088,25 +103121,13 @@ const SEVERITY_EMOJIS = new Map([ [5, ':white_circle:'] ]); class RuntimeSummarizer { - createSummaryMarkdown(results, changedFiles = []) { + createSummaryMarkdown(results, changedFiles = [], changedFilesOnly = false) { let summary = `## Salesforce Code Analyzer Results${os_1.EOL}`; - if (results.getTotalViolationCount() === 0) { - summary += `### :white_check_mark: 0 Violations Found${os_1.EOL}`; - return summary; - } - summary += - `### :warning: ${results.getTotalViolationCount()} Violation(s) Found${os_1.EOL}` + - `
${os_1.EOL}` + - `${SEVERITY_EMOJIS.get(1)} ${results.getSev1ViolationCount()} Critical severity violation(s)
${os_1.EOL}` + - `${SEVERITY_EMOJIS.get(2)} ${results.getSev2ViolationCount()} High severity violation(s)
${os_1.EOL}` + - `${SEVERITY_EMOJIS.get(3)} ${results.getSev3ViolationCount()} Medium severity violation(s)
${os_1.EOL}` + - `${SEVERITY_EMOJIS.get(4)} ${results.getSev4ViolationCount()} Low severity violation(s)
${os_1.EOL}` + - `${SEVERITY_EMOJIS.get(5)} ${results.getSev5ViolationCount()} Info severity violation(s)${os_1.EOL}` + - `
${os_1.EOL}`; const violations = results.getViolationsSortedBySeverity(); const changedFilesSet = new Set(changedFiles); const violationsInChangedFiles = []; const violationsOutsideChangedFiles = []; + // Separate violations by whether they're in changed files for (const violation of violations) { const hasLocationInChangedFile = violation .getLocations() @@ -103119,7 +103140,44 @@ class RuntimeSummarizer { violationsOutsideChangedFiles.push(violation); } } - if (violationsInChangedFiles.length > 0 && violationsOutsideChangedFiles.length > 0) { + // Determine which violations to show based on changedFilesOnly flag + const violationsToShow = changedFilesOnly && changedFiles.length > 0 ? violationsInChangedFiles : violations; + // Calculate counts for the violations we're showing + const totalCount = violationsToShow.length; + const sev1Count = violationsToShow.filter(v => v.getSeverity() === 1).length; + const sev2Count = violationsToShow.filter(v => v.getSeverity() === 2).length; + const sev3Count = violationsToShow.filter(v => v.getSeverity() === 3).length; + const sev4Count = violationsToShow.filter(v => v.getSeverity() === 4).length; + const sev5Count = violationsToShow.filter(v => v.getSeverity() === 5).length; + if (totalCount === 0) { + if (changedFilesOnly && changedFiles.length > 0) { + summary += `### :white_check_mark: 0 Violations Found in Changed Files${os_1.EOL}`; + } + else { + summary += `### :white_check_mark: 0 Violations Found${os_1.EOL}`; + } + return summary; + } + if (changedFilesOnly && changedFiles.length > 0) { + summary += `### :warning: ${totalCount} Violation(s) Found in Changed Files${os_1.EOL}`; + } + else { + summary += `### :warning: ${totalCount} Violation(s) Found${os_1.EOL}`; + } + summary += + `
${os_1.EOL}` + + `${SEVERITY_EMOJIS.get(1)} ${sev1Count} Critical severity violation(s)
${os_1.EOL}` + + `${SEVERITY_EMOJIS.get(2)} ${sev2Count} High severity violation(s)
${os_1.EOL}` + + `${SEVERITY_EMOJIS.get(3)} ${sev3Count} Medium severity violation(s)
${os_1.EOL}` + + `${SEVERITY_EMOJIS.get(4)} ${sev4Count} Low severity violation(s)
${os_1.EOL}` + + `${SEVERITY_EMOJIS.get(5)} ${sev5Count} Info severity violation(s)${os_1.EOL}` + + `
${os_1.EOL}`; + // Show violations in appropriate format + if (!changedFilesOnly && + changedFiles.length > 0 && + violationsInChangedFiles.length > 0 && + violationsOutsideChangedFiles.length > 0) { + // Show both sections when not filtering and both exist const violationsInsideFilesTable = createTable(violationsInChangedFiles, TABLE_ROWS_CHAR_LIMIT); const violationsOutsideFilesTable = createTable(violationsOutsideChangedFiles, TABLE_ROWS_CHAR_LIMIT - violationsInsideFilesTable.length); summary += @@ -103134,7 +103192,8 @@ class RuntimeSummarizer { `${os_1.EOL}`; } else { - summary += createTable(violations, TABLE_ROWS_CHAR_LIMIT); + // Show only the filtered violations + summary += createTable(violationsToShow, TABLE_ROWS_CHAR_LIMIT); } return summary; } diff --git a/src/dependencies.ts b/src/dependencies.ts index 97898d1..ed148cf 100644 --- a/src/dependencies.ts +++ b/src/dependencies.ts @@ -73,7 +73,8 @@ export class RuntimeDependencies implements Dependencies { return { runArguments: core.getInput('run-arguments'), resultsArtifactName: core.getInput('results-artifact-name'), - githubToken: core.getInput('github-token') + githubToken: core.getInput('github-token'), + changedFilesOnly: core.getBooleanInput('changed-files-only') } } diff --git a/src/main.ts b/src/main.ts index 2a60ab0..caa9d41 100644 --- a/src/main.ts +++ b/src/main.ts @@ -61,82 +61,128 @@ export async function run( dependencies.startGroup(MESSAGES.STEP_LABELS.ANALYZING_RESULTS) assertFileExists(dependencies, jsonOutputFile) const results: Results = resultsFactory.createResults(jsonOutputFile) - dependencies.setOutput('num-violations', results.getTotalViolationCount().toString()) - dependencies.setOutput('num-sev1-violations', results.getSev1ViolationCount().toString()) - dependencies.setOutput('num-sev2-violations', results.getSev2ViolationCount().toString()) - dependencies.setOutput('num-sev3-violations', results.getSev3ViolationCount().toString()) - dependencies.setOutput('num-sev4-violations', results.getSev4ViolationCount().toString()) - dependencies.setOutput('num-sev5-violations', results.getSev5ViolationCount().toString()) - dependencies.info( - `outputs:\n` + - ` exit-code: ${codeAnalyzerOutput.exitCode}\n` + - ` num-violations: ${results.getTotalViolationCount()}\n` + - ` num-sev1-violations: ${results.getSev1ViolationCount()}\n` + - ` num-sev2-violations: ${results.getSev2ViolationCount()}\n` + - ` num-sev3-violations: ${results.getSev3ViolationCount()}\n` + - ` num-sev4-violations: ${results.getSev4ViolationCount()}\n` + - ` num-sev5-violations: ${results.getSev5ViolationCount()}` - ) dependencies.endGroup() dependencies.startGroup(MESSAGES.STEP_LABELS.CREATING_SUMMARY) + let changedFiles: string[] = [] + let couldReadChangedFiles = false + + // Get changed files for PR context if (dependencies.isPullRequest() && inputs.githubToken) { - let changedFiles: string[] = [] - let couldReadChangedFiles = true try { dependencies.info(MESSAGES.CALCULATING_CHANGED_FILES) changedFiles = await dependencies.getChangedFiles(inputs.githubToken) + couldReadChangedFiles = true dependencies.info(MESSAGES.CALCULATED_CHANGED_FILES) } catch (error) { - couldReadChangedFiles = false dependencies.warn(MESSAGE_FCNS.FAILED_TO_GET_CHANGED_FILES(getFullErrorMessage(error))) } - - const summaryMarkdown = summarizer.createSummaryMarkdown(results, changedFiles) - - if (couldReadChangedFiles) { - const summaryLink: string = await dependencies.createActionSummaryLink(inputs.githubToken) - const changedFilesSet: Set = new Set(changedFiles) - const violationsInChangedFilesCount: number = results - .getViolationsSortedBySeverity() - .filter((v: Violation): boolean => - v - .getLocations() - .map(l => l.getFile()) - .some(f => f && changedFilesSet.has(f)) - ).length - const summaryBody = MESSAGE_FCNS.REVIEW_BODY( - results.getTotalViolationCount(), - violationsInChangedFilesCount, - summaryLink - ) - try { - dependencies.info(MESSAGES.ATTEMPTING_TO_CREATE_PR_REVIEW) - const reviewId: number = await dependencies.createPullRequestReview(inputs.githubToken, summaryBody) - dependencies.setOutput('review-id', `${reviewId}`) - dependencies.info(MESSAGE_FCNS.CREATED_PR_REVIEW(reviewId)) - } catch (error) { - dependencies.warn(MESSAGE_FCNS.FAILED_TO_CREATE_REVIEW(getFullErrorMessage(error))) - } - } - - await dependencies.writeSummary(summaryMarkdown) - dependencies.endGroup() } else { if (dependencies.isPullRequest()) { dependencies.info(MESSAGES.PR_FOUND_WITHOUT_GH_TOKEN) } else { dependencies.info(MESSAGES.NOT_PR) } - const summaryMarkdown = summarizer.createSummaryMarkdown(results) - await dependencies.writeSummary(summaryMarkdown) - dependencies.endGroup() } + + // Calculate violation counts based on mode + const violationCounts = calculateViolationCounts( + results, + inputs.changedFilesOnly && couldReadChangedFiles ? changedFiles : undefined + ) + + // Set outputs with final counts + dependencies.setOutput('num-violations', violationCounts.total.toString()) + dependencies.setOutput('num-sev1-violations', violationCounts.sev1.toString()) + dependencies.setOutput('num-sev2-violations', violationCounts.sev2.toString()) + dependencies.setOutput('num-sev3-violations', violationCounts.sev3.toString()) + dependencies.setOutput('num-sev4-violations', violationCounts.sev4.toString()) + dependencies.setOutput('num-sev5-violations', violationCounts.sev5.toString()) + dependencies.info( + `outputs:\n` + + ` exit-code: ${codeAnalyzerOutput.exitCode}\n` + + ` num-violations: ${violationCounts.total}\n` + + ` num-sev1-violations: ${violationCounts.sev1}\n` + + ` num-sev2-violations: ${violationCounts.sev2}\n` + + ` num-sev3-violations: ${violationCounts.sev3}\n` + + ` num-sev4-violations: ${violationCounts.sev4}\n` + + ` num-sev5-violations: ${violationCounts.sev5}` + ) + + // Generate summary + const summaryMarkdown = summarizer.createSummaryMarkdown(results, changedFiles, inputs.changedFilesOnly) + + // Create PR review if applicable + if (dependencies.isPullRequest() && inputs.githubToken && couldReadChangedFiles) { + const summaryLink: string = await dependencies.createActionSummaryLink(inputs.githubToken) + const changedFilesSet: Set = new Set(changedFiles) + const violationsInChangedFilesCount: number = results + .getViolationsSortedBySeverity() + .filter((v: Violation): boolean => + v + .getLocations() + .map(l => l.getFile()) + .some(f => f && changedFilesSet.has(f)) + ).length + + const summaryBody = MESSAGE_FCNS.REVIEW_BODY( + results.getTotalViolationCount(), + violationsInChangedFilesCount, + summaryLink + ) + try { + dependencies.info(MESSAGES.ATTEMPTING_TO_CREATE_PR_REVIEW) + const reviewId: number = await dependencies.createPullRequestReview(inputs.githubToken, summaryBody) + dependencies.setOutput('review-id', `${reviewId}`) + dependencies.info(MESSAGE_FCNS.CREATED_PR_REVIEW(reviewId)) + } catch (error) { + dependencies.warn(MESSAGE_FCNS.FAILED_TO_CREATE_REVIEW(getFullErrorMessage(error))) + } + } + + await dependencies.writeSummary(summaryMarkdown) + dependencies.endGroup() } catch (error) { dependencies.fail(`${MESSAGES.UNEXPECTED_ERROR}\n\n${getFullErrorMessage(error)}`) } } +/** + * Calculate violation counts, optionally filtered by changed files + * @param results - The full results from the code analyzer + * @param changedFiles - Optional array of changed file paths to filter by + * @returns Object containing counts for each severity level and total + */ +function calculateViolationCounts( + results: Results, + changedFiles?: string[] +): { total: number; sev1: number; sev2: number; sev3: number; sev4: number; sev5: number } { + let violations: Violation[] + + if (changedFiles && changedFiles.length > 0) { + // Filter to only violations in changed files + const changedFilesSet = new Set(changedFiles) + violations = results.getViolationsSortedBySeverity().filter((v: Violation): boolean => + v + .getLocations() + .map(l => l.getFile()) + .some(f => f && changedFilesSet.has(f)) + ) + } else { + // Use all violations + violations = results.getViolationsSortedBySeverity() + } + + return { + total: violations.length, + sev1: violations.filter(v => v.getSeverity() === 1).length, + sev2: violations.filter(v => v.getSeverity() === 2).length, + sev3: violations.filter(v => v.getSeverity() === 3).length, + sev4: violations.filter(v => v.getSeverity() === 4).length, + sev5: violations.filter(v => v.getSeverity() === 5).length + } +} + async function installSalesforceCliIfNeeded( dependencies: Dependencies, commandExecutor: CommandExecutor diff --git a/src/summary.ts b/src/summary.ts index 5592d25..83f5e68 100644 --- a/src/summary.ts +++ b/src/summary.ts @@ -14,31 +14,19 @@ const SEVERITY_EMOJIS: Map = new Map([ ]) export interface Summarizer { - createSummaryMarkdown(results: Results, changedFiles?: string[]): string + createSummaryMarkdown(results: Results, changedFiles?: string[], changedFilesOnly?: boolean): string } export class RuntimeSummarizer implements Summarizer { - createSummaryMarkdown(results: Results, changedFiles: string[] = []): string { + createSummaryMarkdown(results: Results, changedFiles: string[] = [], changedFilesOnly = false): string { let summary = `## Salesforce Code Analyzer Results${EOL}` - if (results.getTotalViolationCount() === 0) { - summary += `### :white_check_mark: 0 Violations Found${EOL}` - return summary - } - - summary += - `### :warning: ${results.getTotalViolationCount()} Violation(s) Found${EOL}` + - `
${EOL}` + - `${SEVERITY_EMOJIS.get(1)} ${results.getSev1ViolationCount()} Critical severity violation(s)
${EOL}` + - `${SEVERITY_EMOJIS.get(2)} ${results.getSev2ViolationCount()} High severity violation(s)
${EOL}` + - `${SEVERITY_EMOJIS.get(3)} ${results.getSev3ViolationCount()} Medium severity violation(s)
${EOL}` + - `${SEVERITY_EMOJIS.get(4)} ${results.getSev4ViolationCount()} Low severity violation(s)
${EOL}` + - `${SEVERITY_EMOJIS.get(5)} ${results.getSev5ViolationCount()} Info severity violation(s)${EOL}` + - `
${EOL}` const violations: Violation[] = results.getViolationsSortedBySeverity() const changedFilesSet = new Set(changedFiles) const violationsInChangedFiles: Violation[] = [] const violationsOutsideChangedFiles: Violation[] = [] + + // Separate violations by whether they're in changed files for (const violation of violations) { const hasLocationInChangedFile: boolean = violation .getLocations() @@ -51,7 +39,50 @@ export class RuntimeSummarizer implements Summarizer { } } - if (violationsInChangedFiles.length > 0 && violationsOutsideChangedFiles.length > 0) { + // Determine which violations to show based on changedFilesOnly flag + const violationsToShow: Violation[] = + changedFilesOnly && changedFiles.length > 0 ? violationsInChangedFiles : violations + + // Calculate counts for the violations we're showing + const totalCount = violationsToShow.length + const sev1Count = violationsToShow.filter(v => v.getSeverity() === 1).length + const sev2Count = violationsToShow.filter(v => v.getSeverity() === 2).length + const sev3Count = violationsToShow.filter(v => v.getSeverity() === 3).length + const sev4Count = violationsToShow.filter(v => v.getSeverity() === 4).length + const sev5Count = violationsToShow.filter(v => v.getSeverity() === 5).length + + if (totalCount === 0) { + if (changedFilesOnly && changedFiles.length > 0) { + summary += `### :white_check_mark: 0 Violations Found in Changed Files${EOL}` + } else { + summary += `### :white_check_mark: 0 Violations Found${EOL}` + } + return summary + } + + if (changedFilesOnly && changedFiles.length > 0) { + summary += `### :warning: ${totalCount} Violation(s) Found in Changed Files${EOL}` + } else { + summary += `### :warning: ${totalCount} Violation(s) Found${EOL}` + } + + summary += + `
${EOL}` + + `${SEVERITY_EMOJIS.get(1)} ${sev1Count} Critical severity violation(s)
${EOL}` + + `${SEVERITY_EMOJIS.get(2)} ${sev2Count} High severity violation(s)
${EOL}` + + `${SEVERITY_EMOJIS.get(3)} ${sev3Count} Medium severity violation(s)
${EOL}` + + `${SEVERITY_EMOJIS.get(4)} ${sev4Count} Low severity violation(s)
${EOL}` + + `${SEVERITY_EMOJIS.get(5)} ${sev5Count} Info severity violation(s)${EOL}` + + `
${EOL}` + + // Show violations in appropriate format + if ( + !changedFilesOnly && + changedFiles.length > 0 && + violationsInChangedFiles.length > 0 && + violationsOutsideChangedFiles.length > 0 + ) { + // Show both sections when not filtering and both exist const violationsInsideFilesTable: string = createTable(violationsInChangedFiles, TABLE_ROWS_CHAR_LIMIT) const violationsOutsideFilesTable: string = createTable( violationsOutsideChangedFiles, @@ -68,7 +99,8 @@ export class RuntimeSummarizer implements Summarizer { violationsOutsideFilesTable + `${EOL}` } else { - summary += createTable(violations, TABLE_ROWS_CHAR_LIMIT) + // Show only the filtered violations + summary += createTable(violationsToShow, TABLE_ROWS_CHAR_LIMIT) } return summary diff --git a/src/types.ts b/src/types.ts index 50019de..6c99f0e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,7 @@ export type Inputs = { runArguments: string resultsArtifactName: string githubToken?: string + changedFilesOnly: boolean } export type EnvironmentVariables = { [key: string]: string } From 1cb03e999f26dcae9881b4bf99dd34ffaded9648 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Tue, 2 Dec 2025 13:20:08 +0530 Subject: [PATCH 2/3] fixing msg --- __tests__/main.test.ts | 5 +++-- dist/index.js | 7 +++---- src/main.ts | 10 ++++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index e0c4592..023378d 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -186,8 +186,9 @@ describe('main run Tests', () => { expect(resultsFactory.createResultsCallHistory).toHaveLength(1) expect(dependencies.warnCallHistory).toHaveLength(0) - expect(dependencies.infoCallHistory).toHaveLength(2) - expect(dependencies.infoCallHistory[0].infoMessage).toEqual(MESSAGES.PR_FOUND_WITHOUT_GH_TOKEN) + expect(dependencies.infoCallHistory).toHaveLength(3) + expect(dependencies.infoCallHistory[0].infoMessage).toContain('Parsed results from') + expect(dependencies.infoCallHistory[1].infoMessage).toEqual(MESSAGES.PR_FOUND_WITHOUT_GH_TOKEN) }) it.each([ diff --git a/dist/index.js b/dist/index.js index 29d2d08..dd22265 100644 --- a/dist/index.js +++ b/dist/index.js @@ -102772,6 +102772,7 @@ async function run(dependencies, commandExecutor, resultsFactory, summarizer) { dependencies.startGroup(constants_1.MESSAGES.STEP_LABELS.ANALYZING_RESULTS); assertFileExists(dependencies, jsonOutputFile); const results = resultsFactory.createResults(jsonOutputFile); + dependencies.info(`Parsed results from ${jsonOutputFile}: found ${results.getTotalViolationCount()} total violations across all files`); dependencies.endGroup(); dependencies.startGroup(constants_1.MESSAGES.STEP_LABELS.CREATING_SUMMARY); let changedFiles = []; @@ -102851,6 +102852,8 @@ async function run(dependencies, commandExecutor, resultsFactory, summarizer) { */ function calculateViolationCounts(results, changedFiles) { let violations; + // Use all violations + violations = results.getViolationsSortedBySeverity(); if (changedFiles && changedFiles.length > 0) { // Filter to only violations in changed files const changedFilesSet = new Set(changedFiles); @@ -102859,10 +102862,6 @@ function calculateViolationCounts(results, changedFiles) { .map(l => l.getFile()) .some(f => f && changedFilesSet.has(f))); } - else { - // Use all violations - violations = results.getViolationsSortedBySeverity(); - } return { total: violations.length, sev1: violations.filter(v => v.getSeverity() === 1).length, diff --git a/src/main.ts b/src/main.ts index caa9d41..2a69022 100644 --- a/src/main.ts +++ b/src/main.ts @@ -61,6 +61,9 @@ export async function run( dependencies.startGroup(MESSAGES.STEP_LABELS.ANALYZING_RESULTS) assertFileExists(dependencies, jsonOutputFile) const results: Results = resultsFactory.createResults(jsonOutputFile) + dependencies.info( + `Parsed results from ${jsonOutputFile}: found ${results.getTotalViolationCount()} total violations across all files` + ) dependencies.endGroup() dependencies.startGroup(MESSAGES.STEP_LABELS.CREATING_SUMMARY) @@ -159,6 +162,9 @@ function calculateViolationCounts( ): { total: number; sev1: number; sev2: number; sev3: number; sev4: number; sev5: number } { let violations: Violation[] + // Use all violations + violations = results.getViolationsSortedBySeverity() + if (changedFiles && changedFiles.length > 0) { // Filter to only violations in changed files const changedFilesSet = new Set(changedFiles) @@ -168,11 +174,7 @@ function calculateViolationCounts( .map(l => l.getFile()) .some(f => f && changedFilesSet.has(f)) ) - } else { - // Use all violations - violations = results.getViolationsSortedBySeverity() } - return { total: violations.length, sev1: violations.filter(v => v.getSeverity() === 1).length, From 03bee7f2003df00362ed01ffa46a11869e8b67c0 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Tue, 2 Dec 2025 13:30:50 +0530 Subject: [PATCH 3/3] docs: Add changed-files-only section with use case description (without example script) --- README.md | 50 ++++++++------------------------------------------ 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 82e6550..1722f6b 100644 --- a/README.md +++ b/README.md @@ -118,48 +118,14 @@ The [Salesforce Code Analyzer v5.x](https://developer.salesforce.com/docs/platfo If you want to focus only on violations in files that were changed in a pull request, use the `changed-files-only` input. When enabled, the output counts will **only include violations from changed files**, making it easy to set up quality gates that don't fail on existing violations in unchanged code. - name: Salesforce Code Analyzer Workflow (Changed Files Only) - on: - pull_request: - jobs: - salesforce-code-analyzer-workflow: - permissions: - pull-requests: write - contents: read - actions: read - runs-on: ubuntu-latest - steps: - - name: Check out files - uses: actions/checkout@v5 - - - name: Install Salesforce CLI - run: npm install -g @salesforce/cli@latest - - - name: Install Latest Salesforce Code Analyzer CLI Plugin - run: sf plugins install code-analyzer@latest - - - name: Run Salesforce Code Analyzer (Changed Files Only) - id: run-code-analyzer - uses: forcedotcom/run-code-analyzer@v2 - with: - run-arguments: --workspace . --view detail --output-file sfca_results.json - results-artifact-name: salesforce-code-analyzer-results - github-token: ${{ github.token }} - changed-files-only: true - - - name: Quality Gate - Only Fail on Violations in Changed Files - if: | - steps.run-code-analyzer.outputs.num-sev1-violations > 0 || - steps.run-code-analyzer.outputs.num-sev2-violations > 0 || - steps.run-code-analyzer.outputs.num-violations > 10 - run: | - echo "Quality gate failed: Found violations in changed files" - echo " Critical (Sev 1): ${{ steps.run-code-analyzer.outputs.num-sev1-violations }}" - echo " High (Sev 2): ${{ steps.run-code-analyzer.outputs.num-sev2-violations }}" - echo " Total: ${{ steps.run-code-analyzer.outputs.num-violations }}" - exit 1 - -**Note:** When `changed-files-only: true` is set, all output counts (`num-violations`, `num-sev1-violations`, etc.) automatically reflect only violations in changed files. You can use the same quality gate conditions you would normally use, and they will only consider violations in files modified by the PR. +**Use Case:** This is ideal when introducing code quality checks to a legacy codebase with existing violations. Instead of blocking all PRs due to pre-existing issues, you can ensure that new code meets quality standards while allowing gradual cleanup of existing code. + +When `changed-files-only: true` is set: +- The action scans the entire workspace for comprehensive analysis +- Violations are filtered to only those in files changed by the PR +- All output counts (`num-violations`, `num-sev1-violations`, etc.) reflect only changed file violations +- Quality gates can evaluate only new violations, not existing codebase issues +- PR reviews show both total violations (for context) and changed file violations (for quality gate decisions) # Version: v1 The `forcedotcom/run-code-analyzer@v1` GitHub Action is based on [Salesforce Code Analyzer v4.x](https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/code-analyzer-3x.html), which is the original `@salesforce/sfdx-scanner` Salesforce CLI plugin.