Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <b>`changed-files-only`</b> *(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`
Expand Down Expand Up @@ -107,6 +114,19 @@ 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.

**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.

Expand Down
6 changes: 5 additions & 1 deletion __tests__/dependencies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
})

Expand Down
9 changes: 5 additions & 4 deletions __tests__/fakes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
Expand Down
100 changes: 89 additions & 11 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -173,7 +186,8 @@ describe('main run Tests', () => {
expect(resultsFactory.createResultsCallHistory).toHaveLength(1)
expect(dependencies.warnCallHistory).toHaveLength(0)

expect(dependencies.infoCallHistory).toHaveLength(2)
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)
})

Expand Down Expand Up @@ -214,7 +228,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
Expand Down Expand Up @@ -246,7 +261,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)

Expand All @@ -272,7 +288,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)

Expand All @@ -298,7 +315,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)

Expand Down Expand Up @@ -443,7 +461,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)
Expand All @@ -452,4 +471,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
})
})
})
Loading
Loading