Once a Salesforce org is big enough, manual peer review stops keeping up with the technical debt and security risk across Apex, Lightning Web Components (LWCs) and a lot of metadata. Static code analysis (SCA) closes that gap with automated quality gates. Wire Salesforce Code Analyzer, PMD and ESLint into a CI/CD pipeline such as Azure DevOps and you have the "shift-left" pattern. Post the findings as pull request (PR) comments and the developer reads them while the code is still open for change.
The tools that read Salesforce code
Static analysis only works if the engine can parse the language in front of it. PMD has been the Apex standard for years, catching SOQL queries inside loops and high cyclomatic complexity. The current stack also has LWCs and Aura components, which need broader analysis than PMD gives on its own.
Salesforce Code Analyzer is the official, Salesforce-supported tool that unifies several engines. It runs PMD for Apex and ESLint for LWCs and Aura behind one interface, which keeps version management simple and the rule sets in line with Salesforce best practices.
This guide assumes you know your way around Azure DevOps. If not, start with the Azure DevOps Pipelines documentation.
Feedback only helps if it lands where the developer is working. Line-level comments on the PR spare them the trip through the build log. Add branch policies on top and only compliant code merges.
Repository structure
An Azure DevOps pipeline for Salesforce usually reads a azure-pipelines.yml file at the project root. A common layout:
my-salesforce-project/
├── force-app/
│ └── main/
│ └── default/
│ ├── classes/
│ ├── triggers/
│ └── lwc/
├── azure-pipelines.yml
└── pmd-rules/ (optional)
└── apex-ruleset.xml
Azure DevOps configuration
Set the pipeline up as follows.
Enable the system access token
In Azure DevOps, open Pipeline Settings, check Allow scripts to access the OAuth token and save.
Set branch policies
Go to Repos, then Branches. Select your main branch and open Branch policies. Add a Build validation requirement and point it at your static analysis pipeline.
Agent requirements
Pick an agent pool. ubuntu-latest is the recommendation for Microsoft-hosted builds.
Ruleset definition
Define custom ruleset XML files such as apex-ruleset.xml to say which PMD rules apply.
Violation thresholds
Set objective pass/fail criteria. Fail the build on any critical security issue, for instance, but allow a limited number of minor warnings.
Artifact storage strategy
Publish the scan reports (XML, JSON or HTML) as pipeline artifacts for later audit and review.
Organizational prerequisites
Some of this is governance, not YAML. Settle it before you build the pipeline:
- Documented coding standards, so the scanner configuration matches the internal security and maintainability policy it enforces.
- Defined violation thresholds. Critical security violations fail the build, for instance.
- Repository access control. The pipeline's service identity needs "Contribute to pull requests" permissions in Azure Repos to create automated threads.
- Ruleset definition. Keep rulesets such as
apex-ruleset.xmlin version control so local development and the pipeline check the same things.
Pipeline flow
The steps run in this order:
- PR context validation. Checks that the pipeline is running inside a pull request.
- Scanner execution. Invokes the Salesforce Code Analyzer engine.
- Results processing. Parses the JSON output to count violations.
- API configuration. Builds the connection to the Azure DevOps API.
- Path normalization. Aligns file paths between the agent and the repository.
- Comment generation. Creates the feedback strings developers will read.
- Guidance logic. Adds rule-specific "Quick Fix" suggestions.
- API request construction. Packages the comments into the PR diff view.
- Error handling. Manages API rate limits and connection retries.
The script, step by step
These snippets are the parts of the azure-pipelines.yml that carry the work.
Step 1: PR context validation
Check for the SYSTEM_PULLREQUEST_PULLREQUESTID environment variable and exit early when it is missing, so no scan runs outside a PR.
if (!$env:SYSTEM_PULLREQUEST_PULLREQUESTID) {
Write-Host "Not in PR context"
exit 0
}
Step 2: PMD scanner execution
This runs the Salesforce CLI scanner against the force-app directory and writes the results to a JSON file.
cmd /c "sfdx scanner:run --target force-app --engine pmd --format json --outfile $outputFile 2>&1" | Out-Null
Step 3: Results processing and validation
Parse the JSON output and count the violations. -Raw reads the whole file as one string so it converts cleanly into a PowerShell object.
$jsonContent = Get-Content $outputFile -Raw
$results = $jsonContent | ConvertFrom-Json
$totalViolations = 0
foreach ($file in $results) {
if ($file.violations) {
$totalViolations += $file.violations.Count
}
}
Step 4: Azure DevOps API configuration
Build the API endpoint URL from the predefined Azure DevOps variables.
$org = $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI.TrimEnd('/')
$project = $env:SYSTEM_TEAMPROJECT
$repoId = $env:BUILD_REPOSITORY_ID
$prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID
$baseUrl = "$org/$project/_apis/git/repositories/$repoId/pullRequests/$prId"
Step 5: File path normalization
Normalize the file paths so the comments land on the right lines in the Azure DevOps PR UI.
if ($filePath -match '.*\(force-app\.*)') {
$filePath = $Matches[1] -replace '\', '/'
} elseif ($filePath -match '.*(force-app\.*)') {
$filePath = $Matches[1] -replace '\', '/'
} else {
$workingDir = Get-Location
$filePath = $filePath -replace [regex]::Escape($workingDir), '' -replace '^\', '' -replace '\', '/'
}
Step 6: Comment generation
Assemble the comment text, with the severity and the issue description.
$comment = "$severityIcon $cleanRuleName`n`n"
$comment += "File: $filePath`n"
$comment += "Line: $($violation.line)"
if ($violation.column) {
$comment += ", Column: $($violation.column)"
}
$comment += "`n`n"
$comment += "Issue: $($violation.message.Trim())`n`n"
if ($violation.category) {
$comment += "Category: $($violation.category)`n"
}
$priorityText = switch ($violation.severity) {
1 { "Critical - Fix Immediately" }
2 { "Important - Should Fix" }
3 { "Suggestion - Consider Fixing" }
default { "Info" }
}
$comment += "Priority: $priorityText`n"
Step 7: Rule-specific fix suggestions
Attach targeted advice for the common violation types.
switch -Wildcard ($cleanRuleName) {
"*SOQL*" {
$comment += "- Move SOQL queries outside of loops`n"
$comment += "- Use bulk operations and collections`n"
$comment += "- Consider using Map<Id, SObject> for efficient lookups"
}
"*Security*" {
$comment += "- Validate user permissions before SOQL/DML operations`n"
$comment += "- Use 'WITH SECURITY_ENFORCED' in SOQL queries`n"
$comment += "- Escape user input to prevent injection attacks"
}
# Additional patterns...
}
Step 8: API request construction
Build the Azure DevOps API request body, with the thread context that pins each comment to a line.
$commentBody = @{
comments = @(
@{
parentCommentId = 0
content = $comment
commentType = 1
}
)
threadContext = @{
filePath = $filePath
rightFileStart = @{
line = $startLine
offset = 1
}
rightFileEnd = @{
line = $endLine
offset = 1
}
}
}
Leave a Comment