Version Control¶
TODO(David)
Review and extend this page. First draft, 2026-10-05. The first two repository standards are from your notes of that day; the rest restates what Section 4, Section 5, and Appendix C already say.
TODO
Branch strategy and branch names, commit conventions, repository settings, CODEOWNERS in full, the central configuration repository
Standards for the version control system: the repository, who can reach it, and how a change enters a protected branch. The reasoning is in Workflow architecture and Governance at scale.
A pipeline is the path that a change takes from the org where it is made to production, and the checks that it passes on the way. Each stop on the path checks the change and approves it before it moves on. GitHub branches and workflows are how these standards build that path. A deployment tool can build it another way, and the standards still apply.
Repositories¶
- A repository that holds an organisation's Salesforce source is private. The source describes the org: its objects, its automation, its permissions, and its integrations. Set in the GitHub organisation's settings, which can stop a member creating a public repository and changing a repository's visibility.
- Access to a repository is granted to teams, never to individual users. A team's membership is reviewed in one place, and a person who changes role or leaves loses access by leaving the team (About teams). No workflow checks this. It is checked when access is reviewed (Audit Continuously).
- The files that configure the pipeline, such as the workflows, the rulesets for static analysis, and the Prettier configuration file, are owned by the team that governs the pipeline, and change only with that team's review. Enforced by
CODEOWNERSand a branch ruleset that requires a code owner's review (Ruleset Files as a Governance Artifact). - The npm packages of a project are among those files:
package.json,package-lock.json, and the.husky/folder are owned by the team that governs the pipeline. The packages are the tools of the pipeline, such as Prettier, ESLint, Jest, Husky, and lint-staged, and a new version of one can change what a gate accepts. A developer does not upgrade a package by hand. An upgrade comes as a pull request from Renovate or Dependabot (Dependency Scanning), and a developer who needs a new package opens a pull request, which that team reviews. Enforced by the sameCODEOWNERSfile (Appendix D).
Sources:
Project Repository Structure¶
- Every project repository has the structure shown below, so that a developer who moves between projects finds each file where the last project kept it. A new repository is created from the organisation's template repository, which is how the structure reaches it (Repository Bootstrap Script). No workflow checks this.
- A file that configures the pipeline sits in
.github/when its tool finds it there, or when only a workflow reads it. It sits at the root only when its tool, or that tool's IDE extension, looks nowhere else. The root is where developers work, and every file there that they never change is one more to read past. A file is not moved to a place where its tool would need a setting to find it. - The PMD rulesets and the JAR of the custom Java rules sit together in
config/pmd/, andcode-analyzer.ymlnames them there. What the folder holds is in Appendix F. .vscode/extensions.jsonlists the IDE extensions that the repository recommends, and the team that governs the pipeline owns it; the sample is in Sample VS Code Files..vscode/settings.jsonholds the organisation's settings for the VS Code IDE and comes from the organisation's template; the sample is in Sample VS Code Files. It changes through review like any other file, and nothing in the pipeline depends on a setting in it.- A developer's own settings stay out of that file, in a VS Code profile. A profile holds its own settings, keyboard shortcuts, snippets, tasks, and extensions, and when a developer selects a profile with a project open, VS Code associates the profile with that folder and opens the folder with it from then on (Profiles in Visual Studio Code). So each developer creates one profile for the organisation's projects, puts personal preferences in it, and opens every project repository with that profile; the default profile stays as it was for everything else. Where the committed file and the profile set the same setting, the committed file wins, because workspace settings take precedence over user settings (User and workspace settings).
CONTRIBUTING.md, at the root, tells every developer how to work in the repository: the IDE extensions to install, the profile to open the repository with, the commit conventions, and what to look for on the status of a pull request.README.md, at the root, starts as the organisation's default README, which has the sections that every project fills in: what the project is, who owns it, and how to set it up and run its tests. Each project's team keeps those sections current, through pull requests like any other change, so that in an organisation with several projects each repository describes its own. No workflow checks this.- The metadata folder is
force-app, the name a new Salesforce DX project gives it. Some organisations name itsfdx-sourceinstead. Either name works, as long as every project of the organisation uses the same one: the workflows name the folder, in the step that builds the delta package, in the scans, and in the deployment, so a project that names it differently needs its own workflow inputs, and a developer who moves between projects finds the folder where the last project had it. No workflow checks this. config/project-scratch-def.jsonis each project's own. The organisation's template gives the project its first one, the team adapts it to the project's needs (the edition, the features, and the settings its tests require), and an architect approves the change, as every pull request needs an approval (Approvals).package.jsoncomes from the organisation's template with the organisation's npm packages already in it, as the starters of every repository: the pipeline tools (Prettier, ESLint, Jest, Husky, and lint-staged) and the shared configurations that the organisation publishes (Packages and Their Configuration Files). A project adds what it needs and removes nothing of these.
The first tree is the whole repository, with what each file is for. Which of the files a new Salesforce DX project brings, and what the organisation adds to them, is said in the templates folder of the configuration repository (Appendix D). A file marked "Organisation default" comes from that folder, and each project keeps it current. A package-based project usually has a repository for each package. An org whose metadata is kept in functional groupings has several folders where the tree shows force-app/.
my-project/
├── .github/
│ ├── actions/ # Composite actions that the workflows share
│ ├── workflows/ # Every workflow: the second tree
│ ├── CODEOWNERS # Who must review a change to each path
│ ├── codecov.yml # Codecov configuration
│ ├── metadata-risk-tiers.yml # The risk matrix that the pipeline reads
│ ├── pull_request_template.md # The pull request description, with its test section
│ ├── release-please-config.json # Release Please: release type and changelog sections
│ ├── .release-please-manifest.json # Release Please: the current version
│ └── renovate.json # Renovate: one line that extends the organisation's file
├── .husky/
│ └── pre-commit # The checks that run before a commit, through lint-staged
├── .vscode/
│ ├── extensions.json # The editor extensions the repository recommends
│ ├── launch.json # Launch configurations for the Salesforce extensions
│ └── settings.json # The organisation's IDE settings; a developer's own go in a VS Code profile
├── config/
│ ├── pmd/ # The PMD rulesets and the JAR of the Java rules
│ └── project-scratch-def.json # The scratch org definition
├── force-app/ # The metadata folder, named the same in every project
│ └── main/default/ # The metadata, in a folder for each type
├── manifest/
│ └── package.xml # What drift detection retrieves from the org
├── scripts/
│ ├── apex/ # Anonymous Apex scripts for the CLI
│ └── soql/ # SOQL queries for the CLI
├── .forceignore # Source excluded from syncing, including the Tier 0 block
├── .gitignore # SFDX default, plus the Tier 0 entries (Appendix B) and additions
├── .prettierignore # What Prettier leaves out
├── CHANGELOG.md # Written by Release Please
├── CONTRIBUTING.md # How to work in this repository
├── README.md # Organisation default, with sections each project keeps current
├── code-analyzer.yml # Code Analyzer configuration
├── eslint.config.js # ESLint configuration, the organisation's default
├── jest.config.js # Jest configuration, in place of the default file
├── jest.setup.js # Setup for every Jest test: the accessibility check
├── package.json # The npm packages of the pipeline tools, and their scripts
├── package-lock.json # Written by npm
├── prettier.config.mjs # Prettier configuration, calling the organisation's default
└── sfdx-project.json # The project file: package directories, API version, and settings
The second tree is the workflows folder. It lists every workflow, as a repository that holds its own copies has them. A repository that calls the reusable workflows and the composite actions from the configuration repository holds the caller workflows only (Governance at scale).
.github/workflows/
│
│ ── Caller workflows (one per trigger event) ──
├── ci.yml # Push baseline: runs on every push
├── pr-prod.yml # Gates for a pull request to the production branch
├── pr-pipeline.yml # Gates for a pull request to a pipeline branch
├── pr-risk-tier.yml # Classifies the risk tier again when auto-merge is enabled
├── scheduled-project-scan.yml # Weekly: drift report, compilation check, and full Code Analyzer scan
├── scheduled-test-runner.yml # Nightly: full-org Apex tests and all Jest tests
├── manual-code-analyzer-scan.yml # On demand: scans the whole repository with Code Analyzer
├── dependabot-rename.yml # Gives a pull request from Dependabot a Conventional Commit title
│
│ ── Reusable workflows (called by the callers) ──
├── release-please.yml # Changelog and tagged releases
├── sf-apex-compilation.yml # Checks that every Apex class and trigger in the org compiles
├── sf-apex-test-selection.yml # Chooses the Apex tests for a pull request
├── sf-apex-tests.yml # Runs every local test in the org
├── sf-changelog-check.yml # Fails a pull request that changes the changelog by hand
├── sf-code-analyzer.yml # Static analysis with Salesforce Code Analyzer
├── sf-destructive-check.yml # Finds a destructive change and labels the pull request
├── sf-org-scan.yml # Scans one org: drift detection and the compilation check
├── sf-generate-delta.yml # Builds the delta package of what a pull request changes
├── sf-jest-tests.yml # Jest tests for Lightning web components, with coverage
├── sf-notify-slack.yml # Posts a report or a failure notice to Slack
├── sf-prettier-verify.yml # Checks formatting with Prettier
├── sf-prohibited-metadata.yml # Fails a pull request that holds prohibited metadata
├── sf-risk-tier.yml # Labels a pull request by its metadata risk tier
├── sf-secret-scan.yml # Secret scanning
├── sf-semver-check.yml # Checks the title of a pull request
└── sf-validate-deployment.yml # Validates the deployment and runs the chosen Apex tests
Pipeline Environments and Authentication¶
The pipeline deploys to a fixed set of Salesforce orgs, one for each environment in the topology (Environment Topology and Org Mapping). This section says which org each environment is, how the pipeline logs in to it, and where its credentials are kept. The reasoning is in Section 7.
| Environment | Org | Branch | Suffix in the names of its values |
|---|---|---|---|
| Dev sandbox | A developer sandbox for org-based work | feat/ |
None |
| Scratch org | A scratch org for package-based work | feat/ |
None |
| Integration | The integration sandbox, a Developer Pro sandbox | int |
INT |
| QA | The QA sandbox | qa |
QA |
| Staging | The staging sandbox, usually a full sandbox | staging |
STAGING |
| Production | The production org | main |
PROD |
- One login method. The pipeline logs in to every org in the table with the JWT Bearer Flow. It does not use an access token or an SFDX auth URL. An access token is a copy of a session, and an auth URL holds a refresh token, so neither gives each login a record of its own (Section 7). The authentication action of A.3.2 implements the flow. No workflow checks this.
- One key pair for each org. Each org has its own External Client App, with the certificate of its own key pair uploaded to it. A leaked private key then reaches one org only (Appendix A.4).
- Each environment reads only its own key. A workflow for a pull request to
qareads the key with the suffixQA, and no other. The "Consumed by" column of the table in Appendix A.4 names the workflow that reads each value. No workflow checks this. - A dedicated integration user. The External Client App of each org logs in as a dedicated integration user, and no person's account. Every deployment then has one identity, and a person who leaves the team does not take the pipeline's access with them (Section 7). No workflow checks this.
- Environments hold the credentials. Each org has a GitHub Environment of its own, named for its environment in the table. The environment holds that org's key and login values, and only the workflows that deploy to it can read them. The production environment requires an approval before a job can use its key (Section 7). No workflow checks this.
- A deployment tool does not change the rules. An organisation that uses a deployment tool, such as Copado, Gearset, or Salesforce DevOps Center, still stores credentials securely for each workflow that authenticates to an org, under the rules above.
- External Client Apps by default. Each org's login uses an External Client App. The one exception is a Dev Hub that creates scratch orgs, which is covered in the next bullet. No workflow checks this.
- The Dev Hub exception. An External Client App fails to create scratch orgs (forcedotcom/cli#3515, still open), so a Dev Hub that creates scratch orgs needs a Connected App. Until that bug is fixed, the Dev Hub's owner asks Salesforce Customer Support to enable the creation of Connected Apps in that org, a one-time step per org, and then creates the Connected App there (Salesforce CLI guide; Section 7). When the bug is fixed, the exception comes out of this page, Section 4, and Section 7.
- Restrict who can authorise the app. Set the Permitted Users of each app to "Admin approved users are pre-authorized", and grant access only through the profiles and permission sets that the app's policies name, so that a user without one of them cannot authorise the app (Salesforce Help). Switching an app from self-authorisation can lock out its current users, so the permission sets are in place before the switch. No workflow checks this.
- Watch the bypass. A user with the Use Any API Client permission can reach any connected app or External Client App, even one that is restricted to pre-authorised users (Salesforce Help). Grant that permission only where it is needed, and treat it as an exception to the restriction above. No workflow checks this.
- Check the setup of each app. Whoever creates or changes an app checks its policies and the profiles and permission sets that can reach it, because a wide setting in either opens the app to more users than the pipeline needs. Setting up a Connected App needs the Approve Uninstalled Connected Apps permission, which org administrators have by default, so check who holds it. No workflow checks this.
- One run at a time for each org. A workflow that connects to an org names a concurrency group for that org and branch, so that two runs do not compete for the org's test lock (Section 4). No workflow checks this.
- No credential in the repository. A key, a secret, or an auth URL is never in a file, a workflow, or a commit (Nothing Secret in Version Control).
Branches, Protections, and Approvals¶
Each environment that receives deployments has a branch, and the protection of a branch grows as the branch nears production (Environment Topology and Org Mapping). This section names the branches, what protects each one, and who approves a change to it.
Branches¶
mainis production. It holds what is deployed to the production org, and nothing else.stagingis the sandbox where testing at scale happens, with all automations and integrations. It is usually a full sandbox.qais the sandbox where the QA team tests the functionality and checks it against the requirements.intis the integration sandbox, where the changes of several developers meet first. It is a Developer Pro sandbox, for its larger storage.- A feature branch is short-lived, and its name starts with
feat/. A developer makes it for one change, and it is deleted when its pull request is merged. It has no protection. - Various branch and promotion strategies exist, and an organisation uses the one that it thinks is best. The strategy on this page is here to start a discussion, more than to be an ironclad policy (Branch Strategy).
A change moves towards production one branch at a time, and each step is a pull request:
---
config:
layout: dagre
flowchart:
wrappingWidth: 300
---
flowchart TB
accTitle: The path of a change from a feature branch to production
accDescr: A change starts on a feature branch. A pull request with one approval takes it to the int branch. A pull request with two approvals takes it to the qa branch. A pull request with two approvals takes it to the staging branch. A pull request with two approvals takes it to the main branch, which is production.
feature["<b>feat/</b><br>A feature branch, and a scratch org"]
int["<b>int</b><br>The integration sandbox,<br>a Developer Pro sandbox"]
qa["<b>qa</b><br>The QA team tests the functionality<br>and checks the requirements"]
staging["<b>staging</b><br>Testing at scale, with all automations and integrations<br>Usually a full sandbox"]
main["<b>main</b><br>Production"]
feature -- "Pull request<br>1 approval" --> int
int -- "Pull request<br>2 approvals, 1 from a code owner" --> qa
qa -- "Pull request<br>2 approvals, 1 from a code owner" --> staging
staging -- "Pull request<br>2 approvals, 1 from a code owner" --> main
Protections¶
One ruleset protects each level of branch. The rulesets belong to the GitHub organisation and not to a repository, so every repository has the same protection, and no repository can weaken it (Appendix C).
| What a pull request needs | int |
qa and staging |
main |
|---|---|---|---|
| A change arrives only through a pull request | Yes | Yes | Yes |
| The pull request is merged by squash | Yes | Yes | Yes |
The Quality Gates check has passed |
Yes | Yes | Yes |
| The deployment is validated against | The int sandbox |
The qa or staging sandbox |
The production org |
| The pull request is up to date with its target branch | No | Yes | Yes |
| Approvals | 1 | 2 | 2 |
| One approval is from a code owner | No | Yes | Yes |
| A new push removes the approvals | Yes | Yes | Yes |
| A person cannot approve their own pull request | Yes | Yes | Yes |
| A person other than the one who pushed last has approved | Yes | Yes | Yes |
| Every review conversation is resolved | Yes | Yes | Yes |
| A force push is blocked | Yes | Yes | Yes |
| The branch cannot be deleted | Yes | Yes | Yes |
| Every commit is signed | Yes | Yes | Yes |
The bypass list of a ruleset is short, and it is written down and reviewed (Bypass Lists).
Approvals¶
- A pull request to
intneeds one approval, from a peer. - A pull request to
qa,staging, ormainneeds two approvals, and one of them is from a code owner. For the metadata inforce-appthe code owner is an architect, so a peer approves the change, and an architect approves it as well. - Every code owner is a team, never a person (Repositories).
CODEOWNERSsets the team for each path (Appendix D):
| What the pull request changes | Who approves as code owner |
|---|---|
Anything that no other line names, the metadata in force-app among it |
The architect team |
| High-risk metadata, such as permissions, sharing, and credentials (Tier 1) | The architect team. The pull request is also merged by hand: a gate turns auto-merge off |
The files of the pipeline: .github/, config/, the configuration file of each tool, package.json, and .husky/ |
The team that governs the pipeline |
| The three files that a release changes, in an org-based repository (Release Please) | A release manager, an architect, or a member of the team that governs the pipeline |
GitHub has three limits here, and these standards accept them:
- It counts the approvals and does not check their order. That the peer reviews first is a convention.
- It does not check who the first approver is. Two architects can approve a change.
- The number of approvals belongs to the branch and not to a folder. Every pull request to
qa,staging, ormainneeds two, whatever it changes.
Pull Requests¶
- A protected branch changes only through a pull request, and a pull request is merged by squash. Enforced by the branch protection rulesets.
- A pull request merges only when every quality gate that applies to it has passed. Enforced by the one required check on each protected branch.
- The title of a pull request follows Conventional Commits, because the title becomes the commit message on the protected branch. Enforced by the Semantic PR Title Check workflow.
Fixing a Blocking Violation¶
- A blocking violation is fixed in the pull request that reports it, or accepted (Accepting a Blocking Violation). Leaving it to be fixed in a separate pull request is not recommended: a team that promises its approver to fix the violation later has no enforceable requirement to do so, unless a third-party tool holds it to the promise. No workflow checks this. It is checked by the reviewer who is asked to approve.
- A pipeline that checks only the lines a pull request changes does not enforce the later fix either. Once the pull request has merged, only a scan of the whole class, or of the whole codebase, brings the violation up again.
Packages and Their Configuration Files¶
A project repository holds a configuration file for each package that the pipeline runs. This section takes the packages one at a time, and gives for each one its purpose, its files, the settings that these standards set, the settings that each organisation decides, how the package runs, and sample files.
Four files have a page of their own and are not repeated here: code-analyzer.yml (Appendix E), the PMD rulesets (Appendix F), metadata-risk-tiers.yml (Appendix B), and the GitHub rulesets (Appendix C).
Prettier¶
Prettier formats every source file in the repository to one standard, from Apex classes and Lightning web components to metadata XML, and the commit hook formats each file before it is committed (How It Runs). This is not just cosmetic. Consistent formatting makes the code consistently readable, so a reviewer spends attention on logic and intent rather than on style. Formatting is then never a matter for review, and a pull request shows only the lines that changed in substance (Formatting).
Files¶
- The repository's Prettier configuration file sits at the root. Prettier looks for it in the folder of the file it is formatting, and then in each folder above (Configuration File).
.prettierignoresits beside it and names what Prettier leaves alone. A new Salesforce DX project comes with one, and an organisation adds the files that a tool generates.- The team that governs the pipeline owns both files (Repositories).
- An organisation with more than one repository writes its configuration once and publishes it as an npm package, so that every repository formats code the same way and the configuration has one version history. A repository that changes nothing names the package as the
prettiervalue in itspackage.json. A repository that changes a setting imports the package into itsprettier.config.mjsand adds only what differs (Sharing configurations). - The shared package is an npm dependency like any other, so a new version of it reaches each repository as a pull request from Renovate or Dependabot (Renovate and Dependabot).
Settings These Standards Set¶
| Setting | Value | Reason |
|---|---|---|
plugins |
prettier-plugin-apex and @prettier/plugin-xml |
Prettier formats neither Apex nor XML by itself. These two are the minimum, and a new Salesforce DX project already lists both. |
useTabs |
true |
Code is indented with tabs, because a tab's width is set by each reader (Accessibility). |
tabWidth |
2 |
Prettier counts a tab as 2 columns when it measures a line, and indents by 2 spaces where a file cannot use tabs. |
printWidth |
180 |
The width these standards recommend. A trigger may have 200, which keeps its list of events on one line. |
singleQuote |
true |
Strings in JavaScript are in single quotes. Aura and Visualforce markup keep double quotes. |
trailingComma |
"none" |
Salesforce requires it for Aura components (Prettier Code Formatter). |
endOfLine |
"lf" |
Developers on Windows and on macOS work in the same repository, and every file ends its lines the same way. |
apexInsertFinalNewline |
false |
A class retrieved from an org has no final newline, so a formatted class and a retrieved class end alike. |
bracketSameLine |
true |
The > that closes a tag with several attributes stays on the line of the last attribute, in templates, in Aura and Visualforce markup, and in XML. |
Some settings apply to one kind of file, and the configuration sets each of them as an override:
- Apex. Classes and triggers use the
apexparser, and anonymous Apex in a.apexfile usesapex-anonymous. - Lightning Web Component templates. They use the
lwcparser, as in a new Salesforce DX project. - Aura and Visualforce markup. They use the
htmlparser. For a Visualforce page or component the sample stateshtmlWhitespaceSensitivityas"css", which is Prettier's default, so that the choice is in plain view. - Metadata XML. A
-meta.xmlfile and a manifest are indented with 4 spaces and not with tabs, which is how Salesforce writes them, so a formatted file and a retrieved file differ in as few lines as possible. An empty element has no space before its closing slash (xmlSelfClosingSpaceisfalse). The override for metadata XML comes after the override for every XML file, and holds only the settings that differ: Prettier applies every override that matches a file, in the order of the list, and a later one wins. - White space in XML.
xmlWhitespaceSensitivityis"preserve"for every XML file. Prettier then indents the elements and leaves the text inside an element, such as a description or a formula, exactly as it is. The plugin's default leaves the indentation of a file as it finds it, and its"ignore"value changes the text inside an element. - Text inside a metadata element.
embeddedLanguageFormattingis"off"for metadata (Embedded Language Formatting). Without it, the XML plugin formats the text of an element whose name is also the name of a language, such asflow,json, orcss, as that language. The name of a Flow in aflowelement then gains line breaks and a semicolon, and the file no longer names the Flow.
What an Organisation Customises¶
- Other plugins. An organisation may add any plugin it finds useful. None is needed for the standards above.
- Another print width. 180 is a recommendation. An organisation that prefers another number sets it once, and may give a trigger the same number as every other file.
- White space in Visualforce markup. With
"css", the page that a user sees does not change, but where two tags touch in the source, one of them ends on the next line of the file. With"ignore", each tag has a line of its own, and Prettier can put a space between two inline elements that had none, which the page then shows (HTML Whitespace Sensitivity). An organisation that prefers the cleaner file can choose"ignore"and accept that risk. - Any other setting of Prettier. These standards take no position on a setting that the table above does not name.
- Other kinds of file, such as CSS, Markdown, and JSON. The sample below indents Markdown and JSON with spaces, and says nothing of CSS. YAML is always indented with spaces, because YAML does not allow tabs.
How It Runs¶
- In the IDE, on save. The Prettier extension is one of those that
.vscode/extensions.jsonrecommends. - At a commit. Husky and lint-staged run
prettier --writeon the staged files (Husky and lint-staged). - On a pull request. A workflow runs
prettier --checkand fails when a file is not formatted (Prettier Formatting Verification). It changes no file.
Sample Prettier Files¶
A repository uses one of the two files below, not both.
The first is complete in itself. It is a .prettierrc file, the name a new Salesforce DX project uses, and it holds every setting above.
The second, prettier.config.mjs, imports a shared configuration from an npm package and adds one override to it. Here the project gives a trigger the print width of every other file. Prettier documents the pattern under Extending a Shareable Config. The plugins come with the package, as its dependencies.
Expand to show .prettierrc and prettier.config.mjs
{
"$schema": "https://json.schemastore.org/prettierrc.json",
"apexInsertFinalNewline": false,
"bracketSameLine": true,
"endOfLine": "lf",
"printWidth": 180,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": true,
"overrides": [
{
"files": "*.{cls,trigger}",
"options": { "parser": "apex" }
},
{
"files": "*.trigger",
"options": { "printWidth": 200 }
},
{
"files": "*.apex",
"options": { "parser": "apex-anonymous" }
},
{
"files": "**/lwc/**/*.html",
"options": { "parser": "lwc", "printWidth": 120 }
},
{
"files": "*.{cmp,component,page,app,auradoc,design,evt,intf,tokens}",
"options": { "parser": "html", "singleQuote": false }
},
{
"files": "*.{page,component}",
"options": {
"htmlWhitespaceSensitivity": "css"
}
},
{
"files": "*.{auradoc,design,evt,intf,tokens}",
"options": { "htmlWhitespaceSensitivity": "ignore" }
},
{
"files": "*.xml",
"options": {
"parser": "xml",
"xmlSelfClosingSpace": true,
"xmlWhitespaceSensitivity": "preserve"
}
},
{
"files": [
"*-meta.xml",
"manifest/*.xml",
"package.xml",
"destructiveChanges*.xml"
],
"options": {
"embeddedLanguageFormatting": "off",
"useTabs": false,
"tabWidth": 4,
"xmlSelfClosingSpace": false
}
},
{
"files": "*.{yml,yaml}",
"options": { "useTabs": false }
},
{
"files": "*.json",
"options": { "printWidth": 80, "singleQuote": false, "useTabs": false }
},
{
"files": "*.md",
"options": { "useTabs": false }
}
],
"plugins": ["prettier-plugin-apex", "@prettier/plugin-xml"]
}
import baseConfig from '@my-org/prettier-config' with { type: 'json' };
/**
* The shared configuration, with one override added for this project.
*
* @see https://prettier.io/docs/sharing-configurations
* @type {import("prettier").Config}
*/
const config = {
...baseConfig,
overrides: [
...baseConfig.overrides,
// This project keeps a trigger to the print width of every other file.
{
files: '*.trigger',
options: {
printWidth: 180
}
}
]
};
export default config;
The sample imports @my-org/prettier-config, which stands for the organisation's own package. @x2od/prettier-config is a public package of this kind, with settings like those above, and more. Its repository is further reading, and a model for an organisation's own package.
PMD¶
TODO
Describe PMD here like for other tools
TODO: Placeholder text for PMD description similar to ESLint.
ESLint¶
ESLint checks the JavaScript of Lightning Web Components and Aura components against rules: the rules of the Salesforce component model, and the rules that the JavaScript standards and the Lightning Web Components and Aura standards name.
A new Salesforce DX project comes with a configuration for the component model. It uses Salesforce's ESLint packages, which hold rules for Lightning Web Components, for how a component calls Apex, and for Aura, beside ESLint's own rules and rules for Jest tests. These standards start from that configuration. They add a package that keeps ESLint from conflicting with Prettier, and they set the rules that the standards name.
Files¶
eslint.config.jssits at the root, where ESLint looks for it (Configuration Files). A new Salesforce DX project comes with one.- The repository has one ESLint configuration file, and every tool reads it: the IDE, the commit hook, and Code Analyzer. A rule then means the same thing wherever it runs.
- The team that governs the pipeline owns the file (Repositories). It is a governance artifact, and how the lint gate runs in the pipeline is in Section 5.
- An organisation with more than one repository can publish its configuration as an npm package. The file of each repository then extends the package and adds what differs (Share Configurations). These standards give no sample of that form.
The packages are installed as devDependencies in the project's package.json. Every package in the table, except the one marked optional, is in every project, whether the project uses it yet: an organisation that writes no Aura component today keeps @salesforce/eslint-plugin-aura and the Aura part of the configuration, so that the first Aura component is linted from its first commit. An organisation that forbids Aura may leave them out (An Organisation Without Aura). A package whose target files do not exist costs nothing at lint time, and Renovate keeps it current with the rest (Renovate and Dependabot).
The sample eslint.config.js does not name every package. @salesforce/eslint-config-lwc loads some of them itself, but it does not install them: they are its peer dependencies, so the project installs them beside it. The last column says how the file loads each package:
| Package | Purpose | How the sample file loads it |
|---|---|---|
eslint |
The ESLint engine. The Salesforce packages accept version 9 and not version 10. | The file takes defineConfig from it |
@eslint/js |
ESLint's own recommended rules. Version 9, to match ESLint. | For Jest mock files |
@lwc/eslint-plugin-lwc |
The rules of the Lightning Web Component model | Through @salesforce/eslint-config-lwc |
@lwc/eslint-plugin-lwc-platform |
Salesforce's rules for Lightning web components that run in an org, such as the rules on Aura globals. Code Analyzer has a copy of its own, but ESLint alone, as at a commit, needs this one. | For Lightning Web Component files |
@salesforce/eslint-config-lwc |
Salesforce's ESLint configuration for Lightning Web Components, in its recommended preset |
For Lightning Web Component files |
@salesforce/eslint-plugin-aura |
The Aura rules and the Locker rules. It is the one package that the Aura part of the file needs, and Code Analyzer does not bring it. | For Aura files |
@salesforce/eslint-plugin-lightning |
Rules for the Lightning platform. The recommended preset uses its rule on how a component calls an Apex method. |
Through @salesforce/eslint-config-lwc |
eslint-config-prettier |
Turns off the ESLint rules that Prettier makes unnecessary or that conflict with Prettier | For every file, from eslint-config-prettier/flat |
eslint-plugin-import |
Rules for import and export statements |
Through @salesforce/eslint-config-lwc |
eslint-plugin-jest |
Rules for Jest tests, and the globals of Jest | Through @salesforce/eslint-config-lwc, and by the file for Jest mocks |
eslint-plugin-sonarjs |
Optional. SonarSource's JavaScript rules for ESLint; see SonarJS | Not loaded |
globals |
Lists of global variables, such as those of Node.js | For Jest tests and Jest mock files |
A new Salesforce DX project lists seven of these packages. It does not list @eslint/js, @lwc/eslint-plugin-lwc-platform, eslint-config-prettier, or globals. @eslint/js comes with ESLint 9, and globals with the Salesforce packages, but a project names each package that its own file loads. One command adds the four:
ESLint 10 and Salesforce packages
At the time of writing, in 2026, the newest major version of ESLint is 10, and the Salesforce ESLint packages accept only version 9: @salesforce/eslint-config-lwc, @salesforce/eslint-plugin-aura, @salesforce/eslint-plugin-lightning, @lwc/eslint-plugin-lwc, and @lwc/eslint-plugin-lwc-platform each name ESLint 9 as a peer dependency. An organisation keeps eslint and @eslint/js at version 9 until Salesforce updates its packages. Without that, Renovate proposes version 10, and the update fails when npm installs the packages. This entry in the packageRules list of the central Renovate file (Renovate and Dependabot) holds both at version 9:
{
"description": "Hold ESLint at version 9 until the Salesforce ESLint packages accept version 10",
"matchPackageNames": ["eslint", "@eslint/js"],
"allowedVersions": "<10"
}
Remove the entry when the Salesforce packages accept version 10.
Code Analyzer needs the packages too, because it loads the repository's file (How It Runs).
Settings These Standards Set¶
The settings are in the order of the blocks in the sample file.
- The version of Jest. The file states the version of Jest that the project has. The Lightning Web Component configuration loads rules for Jest tests, and those rules look for Jest from the folder that ESLint runs in. Code Analyzer runs ESLint from a folder of its own, where that search fails and the scan stops with an error.
- The recommended Salesforce rules. For Aura, the recommended rules and the Locker rules of
@salesforce/eslint-plugin-aura. For Lightning Web Components, the recommended configuration of@salesforce/eslint-config-lwc. This is what a new Salesforce DX project has, and these standards ask for no stricter set. - No conflict with Prettier.
eslint-config-prettierturns off the rules that Prettier makes unnecessary or that conflict with Prettier (eslint-config-prettier). The Salesforce configurations turn on four of them:no-confusing-arrow,no-floating-decimal,no-unexpected-multiline, andwrap-iife. Without the package, a commit can fail on code that Prettier formatted. For example, when Prettier breaks a long line, it removes the brackets around the conditional body of an arrow function, andno-confusing-arrowthen reports that line. The file loads the package after the Salesforce blocks, so that it turns their rules off. curlystays on.eslint-config-prettieralso turnscurlyoff, because two options ofcurly,"multi-line"and"multi-or-nest", can conflict with Prettier (curly). These standards usecurlywith its default option,"all", which does not conflict with Prettier. The file sets the rules of these standards aftereslint-config-prettier, socurlyis on again.- No error for the form of an Aura file. The Salesforce Aura rules make
no-unused-expressionsan error. That rule reports the({ ... })form of every Aura controller, helper, and renderer (Review recommended rules config for func-names and no-unused-expressions). The file turns the rule off for Aura files, so that the commit hook does not stop every commit that has an Aura file. Code Analyzer does not run the rule by default, so the gate does not change. - The rules that the JavaScript standards and the Lightning Web Components and Aura standards name. The file sets each of them, so that the levels are in plain view; the Salesforce configurations already make many of them an error. One block sets the rules of ESLint itself, for Aura components and Lightning Web Components. A second block sets the rules of the two LWC plugins, for Lightning Web Components only, because ESLint loads those plugins only for them. The file patterns also match the Jest tests in the folder of a Lightning web component, so the rules hold in a test too.
- Error or warning. ESLint has two levels, and these standards have five severities. In the file, a rule at severity 1 or 2 is an error, and a rule at a lower severity is a warning. The severity itself is set in
code-analyzer.yml(the sample). - Never below Salesforce, for a rule that a standard names. Where a Salesforce configuration makes such a rule an error, the rule blocks a pull request here: it has severity 2 or higher. The Salesforce packages make many more rules an error than Code Analyzer blocks on, and these standards do not list those rules. How an ESLint Rule Reaches a Pull Request explains the difference and lists them.
topandlocation. Salesforce suggestsno-shadowwith itsbuiltinGlobalsoption for the standard on these two globals (Lightning Web Security Limitations). With that option, the rule also reports every other global of the browser, such as a parameter namedeventin every handler. So the file usesno-restricted-syntax, with selectors that find a variable, parameter, function, class, or import namedtoporlocation, and nothing else. The selectors are at the top of the sample file.no-shadowkeeps the setting of the Salesforce configurations.
| Rule | Level in the file | An error in Salesforce's configurations | Severity | Standard |
|---|---|---|---|---|
curly |
error |
No | 2 | Braces |
no-console |
error |
For Aura components | 2 | Console and Debugger Statements |
no-debugger |
error |
Yes | 2 | Console and Debugger Statements |
no-implied-eval |
error |
Yes | 2 | Strings Run as Code |
no-script-url |
error |
Yes | 2 | Strings Run as Code |
no-undef |
error |
Yes | 2 | Strict Mode |
no-global-assign |
error |
Yes | 2 | Strict Mode |
no-extend-native |
error |
Yes | 2 | Objects the Component Does Not Own |
no-shadow-restricted-names |
error |
Yes | 2 | The Globals top and location |
no-restricted-syntax |
error |
No | 2 | The Globals top and location |
@lwc/lwc/no-inner-html |
error |
For Lightning Web Components | 2 | Input and Inserted HTML |
@lwc/lwc-platform/no-aura |
error |
For Lightning Web Components | 2 | Aura Globals |
@lwc/lwc-platform/no-aura-libs |
error |
For Lightning Web Components | 2 | Aura Globals |
no-implicit-globals |
warn |
No | 3 | Strict Mode |
no-param-reassign |
warn |
No | 3 | Objects the Component Does Not Own |
What an Organisation Customises¶
- Other plugins. These standards recommend
eslint-plugin-sonarjsfor five standards (SonarJS). The sample file does not load it. - A stricter set of rules for Lightning Web Components, such as the extended configuration of
@salesforce/eslint-config-lwc. - Other restricted syntax.
no-restricted-syntaxtakes one list of selectors, and a second setting of the rule replaces the first. An organisation that restricts other syntax adds its selectors to the list in the sample. - Any other rule. ESLint and its plugins hold a very large number of rules, and every package adds more. These standards name the rules of the packages they endorse and the rules that enforce their standards, and they take no position on any other rule. An organisation may add rules of its own to the repository's
eslint.config.js.
How It Runs¶
- In the IDE. These standards recommend the Salesforce Code Analyzer extension in place of the ESLint extension, so that a rule is enforced the same way everywhere. The extension runs ESLint through Code Analyzer, from the
code-analyzer.ymlof the repository. A developer then sees each rule at the severity that the gate uses, and sees the rules of the other engines for JavaScript, such as PMD, as well as the ESLint rules. The ESLint extension shows only the two levels of the ESLint packages, which differ from the gate for many rules (How an ESLint Rule Reaches a Pull Request). The settings that make the extension run the same rules as the gate are in Appendix E. - At a commit. Husky and lint-staged run
eslinton the staged JavaScript of Aura components and Lightning Web Components, beside Prettier, as a new Salesforce DX project is set up to do (Husky and lint-staged). ESLint runs by itself there, so the levels in the file apply: a rule aterrorstops the commit, and a rule atwarndoes not. - On a pull request. Code Analyzer runs ESLint with the repository's file, which
eslint_config_filenames incode-analyzer.yml, and reports each rule at the severity thatcode-analyzer.ymlgives it (Appendix E). The workflow runsnpm cifirst. With a package missing, the scan reports an error and no ESLint result.
Sample ESLint File¶
The sample, eslint.config.js, is the file of a new Salesforce DX project, with blocks added before it and after it. The block before it states the version of Jest, and the selectors for top and location are defined above the blocks. The blocks after it load eslint-config-prettier, turn off no-unused-expressions for Aura files, and set the rules of the table above: first for both kinds of component, and then for Lightning Web Components only.
Expand to show eslint.config.js
const { defineConfig } = require('eslint/config');
const eslintJs = require('@eslint/js');
const jestPlugin = require('eslint-plugin-jest');
const auraConfig = require('@salesforce/eslint-plugin-aura');
const lwcConfig = require('@salesforce/eslint-config-lwc/recommended');
const lwcPlatformPlugin = require('@lwc/eslint-plugin-lwc-platform');
const eslintConfigPrettier = require('eslint-config-prettier/flat');
const globals = require('globals');
// Lightning Web Security reaches the globals top and location through window,
// and a name that shadows one of them breaks the code that it rewrites. These
// selectors find a variable, parameter, function, class, or import with
// either name, and no other use of the two words.
const topOrLocation = '[name=/^(?:top|location)$/]';
const topOrLocationSelectors = [
`VariableDeclarator > Identifier.id${topOrLocation}`,
`:function > Identifier.params${topOrLocation}`,
`:matches(FunctionDeclaration, FunctionExpression, ClassDeclaration, ClassExpression) > Identifier.id${topOrLocation}`,
`CatchClause > Identifier.param${topOrLocation}`,
`ObjectPattern > Property > Identifier.value${topOrLocation}`,
`ArrayPattern > Identifier.elements${topOrLocation}`,
`AssignmentPattern > Identifier.left${topOrLocation}`,
`RestElement > Identifier.argument${topOrLocation}`,
`:matches(ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier) > Identifier.local${topOrLocation}`
];
const topOrLocationMessage = 'Do not name anything top or location. Reach them as window.top and window.location.';
const bubblesComposedSelector =
"NewExpression[callee.name='CustomEvent'] > ObjectExpression:has(Property[key.name='bubbles'][value.value=true]):has(Property[key.name='composed'][value.value=true])";
const bubblesComposedMessage = 'Do not dispatch an event with both bubbles and composed set to true.';
const secureCookieSelector =
"AssignmentExpression:has(> :matches(MemberExpression[object.name='document'], MemberExpression[object.type='MemberExpression'][object.object.name='window'][object.property.name='document'])[property.name='cookie'].left):not(:has(Literal[value=/;\\s*secure\\s*(;|$)/i])):not(:has(TemplateElement[value.raw=/;\\s*secure\\s*(;|$)/i]))";
const secureCookieMessage = 'Set every cookie with the Secure attribute.';
module.exports = defineConfig([
// Tell the Jest rules which Jest this project has. They otherwise look for
// Jest from the folder that ESLint runs in, and Code Analyzer runs ESLint
// from a folder of its own, where that search fails.
{
settings: {
jest: {
version: require('jest/package.json').version
}
}
},
// Aura configuration
{
files: ['**/aura/**/*.js'],
extends: [...auraConfig.configs.recommended, ...auraConfig.configs.locker]
},
// LWC configuration
{
files: ['**/lwc/**/*.js'],
extends: [lwcConfig]
},
// LWC configuration with override for LWC test files
{
files: ['**/lwc/**/*.test.js'],
extends: [lwcConfig],
rules: {
'@lwc/lwc/no-unexpected-wire-adapter-usages': 'off'
},
languageOptions: {
globals: {
...globals.node
}
}
},
// Jest mocks configuration
{
files: ['**/jest-mocks/**/*.js'],
languageOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
globals: {
...globals.node,
...globals.es2021,
...jestPlugin.environments.globals.globals
}
},
plugins: {
eslintJs
},
extends: ['eslintJs/recommended']
},
// The blocks from "Aura configuration" to here are the file of a new
// Salesforce DX project. The blocks below change it.
// Turn off the rules that Prettier makes unnecessary or that conflict with
// Prettier, such as no-confusing-arrow. This comes after the Salesforce
// blocks, so that it turns their rules off. It also turns off curly, so it
// comes before the blocks that set the rules of the standards.
eslintConfigPrettier,
// Salesforce's Aura rules report the ({ ... }) form of every Aura
// controller, helper, and renderer as an unused expression
// (https://github.com/forcedotcom/eslint-plugin-aura/issues/8). Turn that
// rule off for Aura, so that a commit with an Aura file is not stopped.
{
files: ['**/aura/**/*.js'],
rules: {
'no-unused-expressions': 'off'
}
},
// The locker configuration of Salesforce's Aura rules holds Lightning
// Locker's lists of the members of document and window. Lightning Web
// Security, the security architecture these standards require, gives a
// component the real objects, so these two rules report code that runs
// there. Turn them off for Aura; aura-api and ecma-intrinsics stay on.
{
files: ['**/aura/**/*.js'],
rules: {
'@salesforce/aura/secure-document': 'off',
'@salesforce/aura/secure-window': 'off'
}
},
// The last two blocks set the rules that the JavaScript standards and the
// Lightning Web Components and Aura standards name. A rule that blocks a
// pull request (severity 1 or 2) is an error, and a rule at a lower
// severity is a warning. The severity itself is set in code-analyzer.yml.
// The Salesforce configurations above already make many of these rules an
// error. The blocks state them all, so that the levels are in plain view.
{
files: ['**/aura/**/*.js', '**/lwc/**/*.js'],
rules: {
curly: 'error',
'no-console': 'error',
'no-debugger': 'error',
'no-implied-eval': 'error',
'no-script-url': 'error',
'no-undef': 'error',
'no-global-assign': 'error',
'no-extend-native': 'error',
'no-shadow-restricted-names': 'error',
'no-cond-assign': 'error',
'no-unreachable': 'error',
radix: 'error',
eqeqeq: 'error',
'consistent-return': 'error',
'no-else-return': 'error',
'no-restricted-syntax': [
'error',
...topOrLocationSelectors.map((selector) => ({ selector, message: topOrLocationMessage })),
{ selector: bubblesComposedSelector, message: bubblesComposedMessage },
{ selector: secureCookieSelector, message: secureCookieMessage }
],
'no-implicit-globals': 'warn',
'no-lone-blocks': 'warn',
'no-param-reassign': ['warn', { props: true }],
'max-depth': ['warn', 3],
complexity: ['warn', 10],
'max-lines-per-function': ['warn', { max: 50, skipBlankLines: true, skipComments: true }]
}
},
// The rules of the two LWC plugins apply only to Lightning web components.
// @salesforce/eslint-config-lwc loads @lwc/lwc for these files, and this
// block loads @lwc/lwc-platform.
{
files: ['**/lwc/**/*.js'],
plugins: {
'@lwc/lwc-platform': lwcPlatformPlugin
},
rules: {
'@lwc/lwc/no-inner-html': 'error',
'@lwc/lwc-platform/no-aura': 'error',
'@lwc/lwc-platform/no-aura-libs': 'error',
'@lwc/lwc-platform/no-inline-disable': 'error'
}
}
]);
Husky and lint-staged¶
Husky runs a script when a developer makes a commit, and lint-staged gives that script the files of the commit and no others. Together they run the checks of the repository on the developer's own machine, before the commit exists. A problem that the hook stops there never reaches GitHub, and that is the best place to stop it. A problem that gets past the hook is caught later, because the GitHub workflows are what enforce the standards (Local Checks Before the Push).
Files¶
.husky/pre-commitis the script that Git runs before each commit. A new Salesforce DX project comes with one, and it holds one command.- The lint-staged settings are in
package.json, under the keylint-staged, where a new Salesforce DX project has them. They have no file of their own. - Two scripts in
package.jsonjoin the two packages:prepareinstalls the hooks, andprecommitruns lint-staged. - The team that governs the pipeline owns the hook and
package.json(Repositories).
The packages are installed as devDependencies in the project's package.json:
| Package | Purpose |
|---|---|
husky |
Points Git at the hooks that the repository keeps in .husky/ |
lint-staged |
Runs a command on the staged files that match a pattern, and on no others |
Settings These Standards Set¶
- What the hook runs. Three commands, each on the staged files that match its pattern, as a new Salesforce DX project is set up to do.
| Staged files | Command | What happens |
|---|---|---|
| Every kind of file that Prettier formats | prettier --write |
The file is formatted, and the formatted file goes into the commit |
| The JavaScript of Aura components and Lightning Web Components | eslint |
A rule at error stops the commit (ESLint) |
| Any file of a Lightning Web Component | sfdx-lwc-jest -- --bail --findRelatedTests --passWithNoTests |
The Jest tests that relate to the staged files run, and a failing test stops the commit |
- The file types that Prettier formats. The pattern names every kind of file that the Prettier configuration of these standards formats:
.apex,.app,.auradoc,.cls,.cmp,.component,.css,.design,.evt,.html,.intf,.js,.json,.md,.page,.tokens,.trigger,.xml,.yaml, and.yml. A new Salesforce DX project names 13 of the 20. It leaves out the anonymous Apex file (.apex) and six kinds of Aura file (.app,.auradoc,.design,.evt,.intf, and.tokens), so those files are never formatted at a commit and never checked on a pull request. - The pattern goes with the Prettier configuration. It names only kinds of file that the configuration has a parser for, and the Prettier sample has one for all 20. With the Prettier file of a new Salesforce DX project, six of them have no parser (
.app,.auradoc,.design,.evt,.intf, and.tokens): Prettier stops with an error on such a file, and the commit is not made. An organisation that adds a kind of file to the pattern gives it a parser in the Prettier configuration first. Prettier's--ignore-unknownoption, which makes it pass over a file that has no parser and report no error, is available to an organisation that prefers that, and it has to be added to every Prettier command: in the workflow files and inpackage.json. - One list, in three places. The same list is in the lint-staged settings, in the npm scripts
prettierandprettier:verify, and in the workflow that checks the formatting on a pull request (Prettier Formatting Verification). A kind of file that is in one list and not in another is formatted in one place and passed over in the other.
What an Organisation Customises¶
- A secret scan. The pre-commit hook is the place for it, because a credential is then found before it is in a commit (Secret Scanning). These standards name no scanner, so the sample hook has a comment where the command goes (Secret Scanning).
- A
commit-msghook. It is the place where an organisation can enforce the format of commit messages, if it wants to. These standards enforce the format on the title of a pull request, and leave the messages of the commits on a branch to each organisation (PR and Commit Message Conventions). - Code Analyzer in a hook. It is an option, and these standards do not ask for it. The developer has the findings in the IDE, and GitHub reports them on the push and on the pull request. That is enough.
- Any other command, for another kind of file.
How It Runs¶
- A developer runs
npm installafter cloning the repository. Thepreparescript runs Husky, which points Git at the hooks in.husky/. The|| truein that script keeps an install from failing where Husky is not installed, such as an install without the development packages. - At
git commit, Git runs.husky/pre-commit, which runs lint-staged. - lint-staged runs each command on the staged files that match its pattern. A file that Prettier changes is staged again, so the commit holds the formatted file.
- When a command fails, the commit is not made, and the developer sees the reason.
The hook runs only on a machine where it is installed, so the workflows do not rely on it. They run the same three checks on every push and on every pull request: Prettier Formatting Verification, Static Code Analysis, and LWC Jest Tests.
Sample Husky Hook and lint-staged Configuration¶
The hook, which is kept as .husky/pre-commit:
.husky/pre-commit
The parts of package.json that belong to the two packages, which go beside what the project's own package.json already holds:
package.json: the Husky and lint-staged parts
{
"scripts": {
"prettier": "prettier --write \"**/*.{apex,app,auradoc,cls,cmp,component,css,design,evt,html,intf,js,json,md,page,tokens,trigger,xml,yaml,yml}\"",
"prettier:verify": "prettier --check \"**/*.{apex,app,auradoc,cls,cmp,component,css,design,evt,html,intf,js,json,md,page,tokens,trigger,xml,yaml,yml}\"",
"prepare": "husky || true",
"precommit": "lint-staged"
},
"lint-staged": {
"**/*.{apex,app,auradoc,cls,cmp,component,css,design,evt,html,intf,js,json,md,page,tokens,trigger,xml,yaml,yml}": [
"prettier --write"
],
"**/{aura,lwc}/**/*.js": ["eslint"],
"**/lwc/**": [
"sfdx-lwc-jest -- --bail --findRelatedTests --passWithNoTests"
]
}
}
Renovate and Dependabot¶
The npm packages and the GitHub Actions of a repository get new versions all the time, and some of those versions close a security hole. Two tools open the pull requests for them, so that no developer has to upgrade a package by hand (Repositories). Renovate opens a pull request when a newer version of a dependency exists. Dependabot, which is part of GitHub, raises an alert when a dependency has a known vulnerability, and opens a pull request for the version that corrects it.
These standards recommend Renovate for version updates, and Dependabot for security alerts and security updates.
Files¶
.github/renovate.jsonin each repository has one line, which extends the organisation's central file.- The central file,
renovate.jsonc, is in the.github/folder of the configuration repository (Appendix D); Renovate reads a preset from any folder of a repository, named with//and its path. It is written once, and every repository takes its settings from it. JSONC is JSON that allows comments. - Neither tool needs a workflow to run. Renovate runs as a GitHub App, which the organisation installs and gives its repositories to (Renovate). Dependabot's alerts and security updates are switched on in the settings of the repository or of the organisation (Dependabot).
- One small workflow,
dependabot-rename.yml, gives a pull request from Dependabot its title (Appendix A.1.8). - The team that governs the pipeline owns both Renovate files:
CODEOWNERSgives it the.github/folder of each repository, and the central file in the configuration repository (Repositories).
Settings These Standards Set¶
- What is tracked. The npm packages, and the GitHub Actions in the workflows, the organisation's own reusable workflows and actions among them, which a caller pins to a release tag (Appendix G).
- No auto-merge. A person merges every update, and that person belongs to the team that governs the pipeline. An update changes
package.jsonand its lock file, or a workflow file, andCODEOWNERSgives all of them to that team, so no update is merged without its review. - Titles. The title of an update is a Conventional Commit, with the name of the package as its scope:
buildfor an npm package, andcifor a GitHub Action. Renovate's own default ischore(deps), and the central file replaces it. Dependabot's titles cannot be set in the same way, so a workflow renames its pull requests to the same form (Appendix A.1.8). The title becomes the commit on the protected branch, and the title gate checks it (Semantic PR Title Check). - A waiting time. Renovate does not propose a new version of an npm package until the version is three days old, which is the time that Renovate's own preset sets (Minimum Release Age). A release that turns out to be bad is often withdrawn within that time. A security update does not wait. Once a pull request is open, it can be merged at once.
- One central file. The settings above are in the central file, and no repository repeats them.
What an Organisation Customises¶
- A schedule, and groups. Renovate can open its pull requests at set times, and can put related packages, such as all those from Salesforce, into one pull request.
- Other kinds of dependency, such as the Maven dependencies of a project of custom PMD rules.
- Auto-merge for small updates. These standards do not use it. An organisation that wants it sets it in the central file, where the team that governs the pipeline decides it for every repository.
- A
dependabot.ymlfile. Dependabot needs none for alerts and security updates, and the rename workflow gives its pull requests their titles. An organisation adds one when it wants to set something else for those pull requests, such as who is assigned to them.
How It Runs¶
- Renovate reads each repository that it has been given. When a dependency has a newer version, and the waiting time is over, it opens a pull request that changes
package.jsonand the lock file, or the workflow file that names the action. - When GitHub learns of a vulnerability in a dependency, Dependabot raises an alert, and opens a pull request for the corrected version.
- Either pull request runs the same gates as any other, from the same caller workflows (Appendix A). A pull request from Dependabot is first given its title by the rename workflow.
- The team that governs the pipeline reviews the pull request and merges it.
Sample Renovate Files¶
The central file, renovate.jsonc, kept in the .github/ folder of the configuration repository:
renovate.jsonc
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"description": "The Renovate settings of the organisation. The renovate.json of each repository extends this file.",
"extends": [
"config:recommended",
// Do not propose a new version of an npm package until it is 3 days old
"security:minimumReleaseAgeNpm"
],
// A person merges every update
"automerge": false,
// Titles are Conventional Commits with the package name as the scope, such as
// build(prettier) and ci(actions/checkout), in place of Renovate's chore(deps)
"semanticCommits": "enabled",
"semanticCommitScope": "{{depName}}",
"packageRules": [
{
"description": "npm packages",
"matchManagers": ["npm"],
"semanticCommitType": "build"
},
{
"description": "GitHub Actions, and the reusable workflows and actions of the organisation",
"matchManagers": ["github-actions"],
"semanticCommitType": "ci"
}
]
}
The file of a project repository, kept as .github/renovate.json. Replace my-org with the name of the organisation:
renovate.json
Release Please¶
Release Please reads the commits on main, and from them it writes the changelog, chooses the next version number, and makes the tagged release (Release Please). It can do this because every commit on a protected branch is a Conventional Commit: the title of a pull request becomes that commit, and a gate checks the title (Semantic PR Title Check). No person writes a changelog, and no person chooses a version number.
Files¶
.github/release-please-config.jsonholds the settings: the release type, and the sections of the changelog..github/.release-please-manifest.jsonholds the current version. A person writes it once, when the repository is created, and after that only Release Please changes it. That one entry is checked with care: an incorrect initial version entry is the most common cause of first-release failures.CHANGELOG.md, at the root, is written by Release Please. It is listed in.prettierignore: Release Please writes it in its own format, andnpm run prettierwould otherwise rewrite it.release-please.ymlis the workflow that runs Release Please (Appendix A.2.13).- The team that governs the pipeline owns the configuration file, with the rest of the
.github/folder (Repositories).CODEOWNERShas lines of its own for the three files that a release changes:CHANGELOG.md, the manifest, andsfdx-project.json(Appendix D).
Settings These Standards Set¶
- The
sfdxrelease type. It is the release type for a Salesforce DX project. In each release it writes the new entries ofCHANGELOG.md, and it sets theversionNumberof the default package directory insfdx-project.jsonto the new version, followed by.NEXT. - One package in a repository. The
sfdxrelease type sets the version of the default package directory and of no other, so it does not serve a repository that builds more than one package. These standards do not recommend it there. They give each package a repository of its own (Section 4). - The version comes from the commits. A
fixraises the patch number, afeatraises the minor number, and a breaking change raises the major number (Appendix A.2.13). - Only Release Please writes
CHANGELOG.md. A gate fails any other pull request that changes the file (Changelog Check). - Who releases a package. In a package-based repository, anyone who can merge a pull request merges the release, at any time, and that merge is the event that starts the creation of a new package version. The three files that a release changes have no owner in
CODEOWNERS. - Who releases to an org. In an org-based repository, a release needs the approval of a release manager, an architect, or a member of the team that governs the pipeline.
CODEOWNERSgives the three files that a release changes to those three teams, and the approval of one person from any of them meets the code owner rule. The release still needs the number of approvals that the ruleset of the branch asks of every pull request, which is two for production (Appendix C). GitHub cannot limit who presses the merge button of one pull request, so the rule is on the approval. - A token that is not the default one. The workflow gives Release Please the token of a GitHub App or of a service account. GitHub starts no workflow for a pull request that the default
GITHUB_TOKENopens, so with that token the gates do not run on the release pull request. - A
vbefore the version in the tag, as inv1.4.0(include-v-in-tag). - One request at a time. Release Please makes its pull requests and its releases one after the other, and not all at once, so that GitHub does not limit its requests (
sequential-calls).
What an Organisation Customises¶
- Which types of commit the changelog shows. The sample shows every type. An organisation that wants a shorter changelog sets
hiddentotruefor the types that it leaves out. - Versions before 1.0.0. Until the first major version, Release Please can raise a smaller number: the minor number for a breaking change, and the patch number for a feature (
bump-minor-pre-majorandbump-patch-for-minor-pre-major). - The name in the tag. Release Please puts the
namefromsfdx-project.jsonbefore the version, as inmy-project-v1.4.0. Withinclude-component-in-tagset tofalse, the tag isv1.4.0. - When Release Please runs. The sample workflow runs it on every push to
main(Appendix A.1.1). It can run on another event. An organisation that makes a release with each deployment to production can run it only when a deployment succeeds: as the last job of the workflow that deploys, or from a call that a deployment tool such as Gearset sends to GitHub (Gearset). It can also run from the "Run workflow" button. Whatever the event, Release Please makes the tag and the release on its first run after the release pull request is merged. - The job that creates a package version. The workflow tells its caller that a release was created, and gives it the tag and the version. The job that makes the package version from them is each organisation's own: the life of a package after its release is outside these standards (Section 4).
- The Slack message that the workflow sends when a release is created.
How It Runs¶
- A pull request is merged into
main. Its title is now a Conventional Commit on that branch. - The workflow runs Release Please, which opens the release pull request, or updates the one that is open. The pull request changes three files:
CHANGELOG.md, the manifest, andsfdx-project.json. - The release pull request runs the same gates as any other. Its title has the form
chore(main): release 1.4.0. - A person merges the release pull request. In an org-based repository, a release manager, an architect, or a member of the team that governs the pipeline approves it first.
- On its next run, Release Please finds the merged pull request. It makes the tag and the GitHub Release, with the new entries of the changelog as the release notes.
Sample Release Please Files¶
Sample Release Please files
The configuration file, kept as .github/release-please-config.json:
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "sfdx",
"sequential-calls": true,
"include-v-in-tag": true,
"changelog-sections": [
{ "type": "feat", "section": "Features", "hidden": false },
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
{ "type": "test", "section": "Tests", "hidden": false },
{ "type": "chore", "section": "Miscellaneous Chores", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "revert", "section": "Reverts", "hidden": false },
{ "type": "deps", "section": "Dependencies", "hidden": false },
{ "type": "build", "section": "Build System", "hidden": false },
{ "type": "ci", "section": "Continuous Integration", "hidden": false },
{ "type": "style", "section": "Styles", "hidden": false },
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
{ "type": "perf", "section": "Performance Improvements", "hidden": false }
],
"packages": {
".": {}
}
}
The manifest is kept as .github/.release-please-manifest.json. Write the version that the repository has now: 0.1.0 before a first release, or the last version that was released without Release Please. A wrong first version gives a wrong first release, and a tag that is pushed is hard to take back.
Codecov¶
Codecov keeps the history of test coverage. For each commit and each pull request it shows which lines the tests run, which lines no test runs, and how coverage changed (Codecov). The pipeline sends it the coverage of the Apex tests and of the Jest tests.
Files¶
.github/codecov.ymlholds the settings. Codecov reads the file from the root of the repository, fromdev/, or from.github/(Codecov YAML).- The secret
CODECOV_TOKENlets a workflow upload a report. A private repository needs it (Appendix A.4). pr-codecov-config.ymlis a small workflow that checks the file when a pull request changes it (the second sample). It is not a required gate.- The team that governs the pipeline owns the file, with the rest of the
.github/folder (Repositories).
Settings These Standards Set¶
- Two workflows send coverage. The nightly run of every Apex test uploads a report, and so do the Jest tests (Appendix A). The validation of a pull request uploads nothing. Apex coverage in Codecov is always from the full run of the night, and never from a pull request: a pull request runs only the tests that bear on it, and the coverage file that a validation can write names each class
no-map/<name>, which Codecov cannot match to a file. - Two flags. A report is uploaded as
Apexor asLWC, and each flag has its folders. Codecov then shows the coverage of each one apart. - A flag is carried forward. A run that uploads only one flag keeps the last coverage of the other, so a pull request with no Lightning web component does not show the coverage of the components as lost.
- Codecov reports, and does not block. Its statuses are set to
informational, and the status for the changed lines alone,patch, is off. The gate that blocks is the validation, which fails a changed class or trigger below 75% coverage (Metadata Validation and Apex Tests). For Lightning web components, the Jest gate is the one that blocks (Jest). - Three figures to watch. Coverage of the whole project must not fall by more than 10% against the target branch. Apex has a target of 85%, the floor of these standards (Apex: Testing), and Lightning web components have a target of 80%.
- What is left out. Aura components, Apex test classes, Apex mock classes, and the Jest test folders are not counted.
- A change to the file is validated on the pull request. Codecov does not report an invalid file: it keeps the last valid one. So a workflow sends the changed file to the Codecov validator. For an invalid file the job fails, and it writes the reason in the job summary and in a comment on the pull request. While the file stays invalid, each run updates that one comment. When the file is valid again, the run updates the comment to say so. The job is not a required check: the pull request shows a failed check, and it is not blocked. When the validator does not answer, the job passes with a warning. The same command checks a file by hand:
What an Organisation Customises¶
- The targets and the threshold.
- A status that blocks. These standards require one check on a pull request,
Quality Gates. An organisation that wants Codecov to block removesinformationaland adds the Codecov status to its required checks. - The comment on a pull request. Codecov writes one unless the file turns it off.
- Other paths to leave out, such as static resources.
- Path fixes. When the paths in a coverage report differ from the paths in the repository, a
fixeslist maps one to the other. - One file for the organisation. Codecov has a file for a whole team in its own settings. The file of a repository is merged into it, and the repository wins where both set a value.
How It Runs¶
- A workflow runs tests and writes a coverage report: the Apex tests through
sf apex run test, and the Jest tests throughsfdx-lwc-jest. - The workflow uploads the report with
codecov/codecov-action, the token, and the flagApexorLWC. - Codecov reads
.github/codecov.ymlfrom the repository, joins the reports of the commit, and carries forward a flag that the commit did not upload. - Codecov posts its statuses on the pull request. They report, and they do not block.
- When a pull request changes
.github/codecov.yml, the validation workflow runs, and no other pull request starts it.
Sample Codecov File and Workflow¶
The configuration, kept as .github/codecov.yml:
Expand to show codecov.yml
# Codecov configuration, kept as .github/codecov.yml.
# Check a change to this file before it is merged:
# curl --data-binary @.github/codecov.yml https://codecov.io/validate
codecov:
require_ci_to_pass: true
coverage:
precision: 2
round: down
range: "75...100"
status:
# The statuses report, and never fail. The validation gate of the
# pipeline is what blocks a change below 75% coverage.
project:
default:
target: auto
threshold: 10%
informational: true
apex:
target: 85%
flags:
- Apex
informational: true
lwc:
target: 80%
flags:
- LWC
informational: true
patch: false
flag_management:
default_rules:
# A run that uploads one flag keeps the last coverage of the other.
carryforward: true
individual_flags:
- name: Apex
paths:
- force-app/main/default/classes/
- force-app/main/default/triggers/
- name: LWC
paths:
- force-app/main/default/lwc/
# When the paths in a coverage report differ from the paths in the
# repository, a `fixes` list maps one to the other.
ignore:
- "**/aura/**"
# A regular expression: a class whose name ends with Test or Tests.
- '.*Tests?\.cls$'
- "**/*Mock.cls"
- "**/__tests__/**"
slack_app: false
The workflow that validates it is kept as .github/workflows/pr-codecov-config.yml. The comment starts with a hidden marker, and a later run finds the comment by it. Two actions from the GitHub Marketplace find the comment and write it: find-comment and create-or-update-comment. A file that was never invalid gets no comment.
Expand to show pr-codecov-config.yml
# .github/workflows/pr-codecov-config.yml
# Checks the Codecov configuration when a pull request changes it.
#
# The job fails on an invalid file. It is not a required check, so it does
# not block the merge: the pull request shows a failed check and a comment.
# While the file stays invalid, each run updates that one comment. When the
# file is valid again, the run updates the comment to say so.
# When the Codecov validator does not answer, the job passes with a
# warning, because the file was not checked.
name: Codecov Config Validation
on:
pull_request:
paths:
- .github/codecov.yml
permissions:
contents: read
pull-requests: write
env:
# The hidden first line of the comment. A later run finds the comment by it.
COMMENT_MARKER: "<!-- CODECOV_VALIDATION_COMMENT -->"
jobs:
validate:
name: Validate codecov.yml
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Send the file to the Codecov validator
id: validate
env:
CONFIG: .github/codecov.yml
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ ! -f "$CONFIG" ]; then
echo "status=removed" >> "$GITHUB_OUTPUT"
{
echo "## Codecov Configuration Check"
echo ""
echo "This pull request removes \`$CONFIG\`, at commit \`${HEAD_SHA:0:7}\`. Nothing to check."
} > summary.md
cat summary.md >> "$GITHUB_STEP_SUMMARY"
{
echo "$COMMENT_MARKER"
cat summary.md
} > comment.md
exit 0
fi
# The validator answers 200 for a valid file and 400 for an
# invalid one. It needs no token.
status=$(curl --silent --show-error --max-time 30 \
--output result.txt --write-out '%{http_code}' \
--data-binary @"$CONFIG" https://codecov.io/validate) || status=000
echo "status=$status" >> "$GITHUB_OUTPUT"
{
echo "## Codecov Configuration Check"
echo ""
if [ "$status" = "200" ]; then
echo "\`$CONFIG\` is valid, at commit \`${HEAD_SHA:0:7}\`."
elif [ "$status" = "400" ]; then
echo "\`$CONFIG\` is not valid, at commit \`${HEAD_SHA:0:7}\`. Codecov does not use an invalid file: it keeps the last valid one."
echo ""
echo '```text'
cat result.txt
echo ""
echo '```'
else
echo "The Codecov validator did not answer (HTTP $status), so \`$CONFIG\` was not checked. Run this job again later."
fi
} > summary.md
cat summary.md >> "$GITHUB_STEP_SUMMARY"
# The comment is the summary, after the hidden marker.
{
echo "$COMMENT_MARKER"
cat summary.md
} > comment.md
if [ "$status" = "400" ]; then
echo "::error file=$CONFIG::Codecov rejects this file. The job summary has the reason."
exit 1
elif [ "$status" != "200" ]; then
echo "::warning::The Codecov validator did not answer. The file was not checked."
fi
- name: Find the comment of an earlier run
id: find-comment
if: always()
uses: peter-evans/find-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: ${{ env.COMMENT_MARKER }}
# Invalid file: write the comment, or update the one that is there.
# Valid file, or no file: update the comment of an earlier run to say so.
# A file that was never invalid gets no comment.
- name: Write or update the comment
if: >-
always() && (steps.validate.outputs.status == '400'
|| (steps.find-comment.outputs.comment-id != ''
&& (steps.validate.outputs.status == '200' || steps.validate.outputs.status == 'removed')))
uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
body-path: comment.md
edit-mode: replace
Secret Scanning¶
A secret scanner reads a change for credentials: a password, a token, a private key, or an authorisation URL. A credential that reaches a commit stays in the history of the repository after the file is corrected, so the scan runs before the commit and again in GitHub (Secret Scanning). The principle behind it: nothing in the repository is of use to a person who gains access to it (Security). These standards name no scanner. They say what a scan must do, and the sample workflow has a place for the scanner that the organisation chooses.
Some scanners in common use:
- Gitleaks is open source, under the MIT licence. It matches patterns that are kept in a configuration file. The program is free, and its GitHub action needs a licence key for an account that is an organisation (gitleaks-action).
- TruffleHog is open source, under the AGPL-3.0 licence. It can test a credential that it finds against the service that issued it, and so say if the credential still works. It has detectors for Salesforce credentials.
- detect-secrets is open source, under the Apache-2.0 licence. It keeps a baseline file of the findings that an organisation has accepted, and reports only what is new.
- GitHub has secret scanning and push protection of its own, which require a paid plan (What an Organisation Customises).
Files¶
- The configuration file of the scanner holds the patterns that the scanner looks for, and the list of findings that it lets pass. Its name depends on the scanner. One file is kept in
config/of the configuration repository, and each project repository has a copy in its root (Appendix D). sf-secret-scan.ymlis the reusable workflow that runs the scan (Secret Scanning)..husky/pre-commitis the hook that can run the same scan before a commit (Husky and lint-staged).- The team that governs the pipeline owns all three (Repositories).
Settings These Standards Set¶
- One set of patterns, in two places. The hook and the workflow read the same configuration file, so a developer's computer finds what the pipeline finds.
- The scanner's own patterns stay on. The configuration file adds patterns to the ones that the scanner ships, and takes none away. Those patterns already find the common forms, such as a private key.
- Two Salesforce patterns. Where the scanner has no pattern for one of these, the configuration file adds it. The first is an SFDX authorisation URL: it starts with
force://and holds a refresh token (The Salesforce Authentication Surface). The second is an access token: in Salesforce's own examples it starts with00D, the first characters of an org ID, and then an exclamation mark (Set Up Authentication). - A scan in the Husky hook. The Husky pre-commit hook runs the scanner on the staged files. That is the best practice, because the hook is the first chance to stop credential exposure (How It Runs).
- GitHub push protection is on. Every repository turns on GitHub's push protection, which refuses a push that holds a credential that GitHub recognises, on GitHub's server, before the credential reaches the repository. GitHub recognises Salesforce access tokens, refresh tokens, and OAuth consumer keys and secrets (Supported secret scanning patterns). It augments the Husky pre-commit hook and does not replace it. Matching patterns are GitHub's own, plus any custom patterns set by the organisation's GitHub administrators, not the central configuration file.
- A finding blocks. On a pull request the scan is one of the gates, and a finding stops the merge. On a push, a finding fails the run (The Gates at a Glance) and posts to Slack.
- The scan reads the commits of the change, and not only the files as they are at its end. A credential that one commit adds and a later commit removes is still in the history. The sample workflow checks out the full history for this reason.
- A scanner in a workflow is one stated version. A workflow that installs the scanner's program installs one stated version, and verifies the checksum of the file before it runs it. A workflow that uses the scanner's action pins the action to a release.
- One list of findings that pass. A finding that is not a credential is added to the list in the central configuration file, and nowhere else. Only the team that governs the pipeline changes that file. A comment in the code that tells the scanner to pass a line is not permitted, and where the scanner has a switch that makes it ignore such comments, the workflow sets it.
What an Organisation Customises¶
- The scanner. Any scanner that can do what these standards ask is a good choice.
- GitHub Secret Protection. For a private or internal repository, push protection and GitHub's own secret scanning come with GitHub Secret Protection, a paid product for GitHub Team and GitHub Enterprise Cloud; for a public repository, both are free (About secret scanning). GitHub's scan reads every commit in a repository, on every branch. By default, anyone who can write to the repository can pass a block of push protection by giving a reason (which is logged), and an organisation can limit who may do so (About push protection).
- The Husky hook. The sample hook has the scan as a comment, because these standards do not choose a scanner (Sample Husky Hook and lint-staged Configuration). An organisation puts its scanner's command there, and gives each developer the program first; the line stops every commit of a developer who does not have it, so the hook fails safe.
How It Runs¶
- Locally, before a commit, the Husky hook scans the staged files. The hook is the first chance to stop a credential before it reaches GitHub: after a push, the scan can only report it.
- On a push, GitHub's push protection refuses the push if it holds a credential that GitHub recognises. It is the last chance to stop a credential before it reaches GitHub.
- On a pull request and on a push, the workflow for that event calls
sf-secret-scan.yml. - The workflow checks out the history of the repository and runs the scanner with the central configuration file. In the sample, this step is a placeholder that finds nothing.
- A finding fails the gate and is posted to the security channel in Slack, with links to the file, the commit, and the pull request, and a mention of the security officer (Secret Scanning).
- The credential is treated as exposed and is revoked or rotated at once. Removing it from the code later does not remove the copies already made.
Rules That Find Credentials in Code¶
A scanner recognises a credential by its form. Some rules of the code scan find one by what the code does with it, or by the name that the code gives it, and so they catch what a scanner can miss. These standards run ten of them:
ApexSuggestUsingNamedCred, a stock PMD rule, reports a credential that Apex writes into a request to an endpoint. An External Credential holds it instead, and Apex calls the endpoint through a Named Credential. It runs at severity 1.ApexBadCrypto, a stock PMD rule, reports a key or an initialisation vector that is written into a call of theCryptoclass. It runs at severity 1.- {N} rules that Salesforce wrote for its AppExchange security review report a credential that is written into Apex: in a field, in a variable, in the header of a request, and in a call to
System.setPassword. The last runs at severity 1, as Salesforce ships it, and the others at severity 2 (Apex: Hard-Coded Credentials). - {N} more of those rules report a credential that is the default value of an attribute of an Aura component or of a Visualforce component. They run at severity 2 (Credentials in Markup).
- One more,
ProtectSensitiveData, reports a field whose name suggests a credential, on a custom object or on a custom metadata type or a custom setting that is public. It runs at severity 2 (Secrets in Fields).
These rules are a screen. Most of them read a name and not a value, so some of their reports are false alarms, such as a field called HASH_ALGORITHM that holds the name of an algorithm. That is the nature of a screening test, and these standards prefer a false alarm to a credential that gets through. The owning team accepts a false alarm (Fixing a Blocking Violation).
Salesforce Code Analyzer ships the {N} AppExchange rules without the Recommended tag, and the sample code-analyzer.yml gives them the tag of this reference, so that the gate runs them (The code-analyzer.yml Configuration File). Appendix F.6 lists every rule that the gate runs.
Where the Secret Scan Goes¶
These standards publish no configuration file for a scanner, because the file belongs to the scanner that the organisation chooses. The place for the scan is in the workflow sf-secret-scan.yml (Secret Scanning), and in the sample hook (Sample Husky Hook and lint-staged Configuration).
VS Code IDE¶
One committed file, .vscode/settings.json, gives every developer of a project the same formatting and the same checks in the IDE, on every computer. Nothing in the pipeline depends on a setting in it: the commit hook and the workflows run the same checks again.
Files¶
.vscode/settings.jsonis in each project repository. It comes from the organisation's template repository (Project Repository Structure)..vscode/extensions.json, beside it, lists the extensions that the organisation recommends. VS Code offers to install them when a developer opens the project. The organisation's file recommends these, at a minimum:- Salesforce Extension Pack, which includes the Salesforce Code Analyzer extension
- Prettier
- XML, from Red Hat
- Apex Log Analyzer
- The organisation's file leaves out two extensions that a developer may know. The Code Analyzer extension runs ESLint and PMD with the project's own
code-analyzer.yml, as the pipeline does (Appendix E), so these standards recommend it in place of both:- the ESLint extension, which new SFDX projects recommend by default (ESLint)
- the Apex PMD extension, which runs PMD from a ruleset file of its own
- The team that governs the pipeline owns
.vscode/extensions.json, byCODEOWNERS..vscode/settings.jsonchanges through review, like any other file.
Settings These Standards Set¶
- The same formatting across the project. Prettier is the formatter of every kind of file that the project has, and the IDE formats a file on save, so a file is formatted before the commit hook sees it.
prettier.requireConfigmakes the IDE format with the project's configuration, or not at all. - The Salesforce Extension Pack. Its extensions warn of a conflict with the org before a deployment from the IDE, never deploy on a save, and retrieve the code coverage with a test run. There are other extensions made by Salesforce, and developers may install those if they wish.
- Code Analyzer in the editor. It checks a file when it opens and when it is saved, with the rule selection of the pull request gate, and it shows a violation at severity 1 or 2 as an error. The settings are those of Appendix E.
What an Organisation Customises¶
- The settings of other tools that the organisation's projects use, such as the formatter of another kind of file.
- The file types and rule selection of Code Analyzer, to match the organisation's own
code-analyzer.yml. The categories must be set in both places. - Nothing essential that a developer may want to change. The file wins over a developer's own settings, so a personal preference does not go in it.
How It Runs¶
- VS Code reads
.vscode/settings.jsonwhen a developer opens the project folder. A value in the file wins over the same value in the developer's profile or user settings, because workspace settings take precedence over user settings. A developer keeps personal settings in a VS Code user profile (Project Repository Structure). - On save, Prettier formats the file, and Code Analyzer checks it.
- The commit hook and the pull request checks then find a file that is already formatted (Husky and lint-staged): the hook formats it again, and the pull request check confirms it, to be safe.
Sample VS Code Files¶
The settings, kept as .vscode/settings.json, and the recommended extensions, kept as .vscode/extensions.json:
Expand to show .vscode/settings.json and .vscode/extensions.json
{
"[apex]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[visualforce]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[xml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"codeAnalyzer.analyzeAutomatically.fileTypes": ".cls,.js,.apex,.trigger,.ts,.xml,.cmp,.page,.component,.html,.css,.app,.cjs,.mjs",
"codeAnalyzer.analyzeOnOpen.enabled": true,
"codeAnalyzer.analyzeOnSave.enabled": true,
"codeAnalyzer.includeFixes": true,
"codeAnalyzer.ruleSelectors": "Recommended DevSecOps",
"codeAnalyzer.severity 1": "Error",
"codeAnalyzer.severity 2": "Error",
"codeAnalyzer.severity 5": "Info",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"files.associations": {
"*.apex": "apex-anon",
"**/.husky/**": "shellscript"
},
"files.trimTrailingWhitespace": true,
"prettier.requireConfig": true,
"salesforcedx-vscode-apex-testing.retrieve-test-code-coverage": true,
"salesforcedx-vscode-core.detectConflictsForDeployAndRetrieve": true,
"salesforcedx-vscode-core.push-or-deploy-on-save.enabled": false,
"search.exclude": {
"**/.sf": true,
"**/.sfdx": true,
"**/coverage": true,
"**/node_modules": true
}
}
Salesforce DX Project Files¶
Three files in the root of a Salesforce DX project say what the project is, and what stays out of it. sfdx-project.json describes the project to the Salesforce CLI (Salesforce DX Project Configuration). .forceignore lists what the CLI does not deploy and does not retrieve (How to Exclude Source When Syncing). .gitignore lists what Git does not track (gitignore).
Files¶
sfdx-project.json,.forceignore, and.gitignoreare in the root of the repository. A new project has all three, fromsf project generate.- A template of each is kept in
templates/of the configuration repository (Appendix D). - The team that governs the pipeline owns the two ignore files.
sfdx-project.jsonis one of the three files that a release changes, soCODEOWNERSgives it to the teams that can approve a release (Release Please).
Settings These Standards Set¶
- Both ignore files hold the Tier 0 paths. Tier 0 is the metadata that never enters version control (Tier 0 Enforcement). With its paths in
.forceignore, the CLI does not retrieve them into the project and does not deploy them. With the same paths in.gitignore, Git does not stage them by accident. The block is the same in both files. .forceignorekeeps the entries of a new project:package.xml, the configuration files of Lightning Web Components, the Jest tests, andnode_modules/. So a Jest test is in the repository, and it is never deployed..gitignorekeeps the entries of a new project: the folders in which the CLI keeps its state (.sf/and.sfdx/), coverage reports, logs,node_modules/, and.env.- No file that holds a credential is tracked.
.gitignorenames a private key (*.keyand*.pem) and the file that holds an SFDX authorisation URL (authFile.json, the name in the CLI's own examples), so that neither is committed by accident (Security). The entry is a help and not a control: the secret scan is the control (Secret Scanning). - The output of the pipeline is not tracked. A developer can run on a computer what a workflow runs in GitHub.
.gitignorenames what those runs leave behind: the result files of Code Analyzer, thedelta/folder of the delta package, thetest-results/folder of the Apex tests, and anapex/folder of Apex retrieved from an org. The last has a leading slash, so that thescripts/apex/folder of a new project stays tracked. sourceApiVersionis the API version of production. The value sets the fields that the CLI retrieves and deploys for each metadata type, so a project at an older version than its org loses the newer fields. The team that governs the pipeline raises it, with a pull request, when production has the new release. It is not the floor for theapiVersionof each component, which has a standard of its own (API Version).namespaceis empty. The project of an org has no namespace, and neither has an unlocked package that was made without one.- Release Please writes the
versionNumberof the default directory. It sets the value in each release, in the package directory that hasdefaultset totrue, and in no other (Release Please). Nobody sets that value by hand.
What an Organisation Customises¶
- The package directories. A new project has one,
force-app, and every project of the organisation names it the same (Repository Structure). An organisation that builds unlocked packages, which is the recommended model (Repository Structure), gives each package a directory of its own, withpackage,versionName, andversionNumber, and names the package inpackageAliases. The sample has two directories:force-appfor the metadata that is in no package, andservice-cloudfor one package. The step that builds the delta package reads the project metadata directories from an input or from the repository variableSF_METADATA_DIR, and usesforce-appwhen neither has a value (A.2.8). sfdcLoginUrl. A new project hashttps://login.salesforce.com. The value is the address that the login commands of the CLI open, and it does nothing else: it takes no part in a deployment. An organisation whose users log in at one address, such as its My Domain address, can state that address here.replacements. The CLI can put the value of an environment variable, or the content of a file, in place of a text when it deploys. A value that differs from org to org is then not written into the repository, and neither is a value that must stay out of it (Replace Strings in Code Before Deploying or Packaging).- Decomposed metadata (
sourceBehaviorOptions). The CLI can split a large metadata file into small files, so that two developers who change one permission set change two files. It is encouraged, and it is optional. Salesforce offers it for custom labels, permission sets, sharing rules, workflows, and external service registrations, and all of them are in beta (Decomposed Metadata Types). Three things to know before an organisation switches one on. First, it is easiest at the start of a project, and it can be done later: commit everything, runsf project convert source-behavior, which writes the setting and converts the files, and create the source-tracked orgs again. Second, the setting is not written by hand: the command writes it. Third, it can stop a PMD rule from reporting. The rules of this reference that read a permission set report nothing on a decomposed one, because the small files do not carry the XML namespace that the rules look for. An organisation that decomposes permission sets changes those rules first (Appendix F). - More entries in
.forceignore, for metadata that the org holds and the repository does not manage. - A deployment tool other than the Salesforce CLI.
.forceignoreis a file of the Salesforce CLI. Gearset reads it for work that starts from a Git branch (Gearset). For Copado and for AutoRABIT ARM, these standards found no statement that the tool reads the file. An organisation that deploys with such a tool tests it with a Tier 0 path, and uses the tool's own list of exclusions where it has one.
How It Runs¶
- The Salesforce CLI reads
sfdx-project.jsonin every project command, on a developer's computer and in every workflow that deploys, retrieves, or validates. - The CLI leaves out every path that
.forceignorenames when it deploys, retrieves, or converts.sf project list ignoredshows the files that it leaves out. - Git leaves out every path that
.gitignorenames when a developer stages files. A file that Git tracks already stays tracked, andgit add --forceoverrides the file. - Release Please writes the
versionNumberof the default directory in the release pull request.
Sample Salesforce DX Files¶
The CLI's ignore file, kept as .forceignore. It is the file of a new project, with the Tier 0 block after it:
Expand to show .forceignore
# List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status
# More information: https://developer.salesforce.com/docs/platform/sfdx-dev/guide/sfdx-dev-exclude-source.html
#
package.xml
# LWC configuration files
**/jsconfig.json
**/.eslintrc.json
# LWC Jest
**/__tests__/**
node_modules/
# Tier 0 - prohibited metadata. Never commit, retrieve, or deploy.
# The entries below are examples only. Include every item on your
# organisation's own Tier 0 list, not just these.
# Keep in step with the tier0 list in metadata-risk-tiers.yml, the Tier 0
# push ruleset, and the ProhibitedMetadataType rule.
# FlowDefinition: Salesforce manages the active Flow version server-side
**/flowDefinitions/
**/*.flowDefinition-meta.xml
# Certificate: managed manually in each org
**/certs/
**/*.crt
**/*.crt-meta.xml
# ExtlClntAppGlobalOauthSettings: holds the OAuth consumer key and secret
**/extlClntAppGlobalOauthSets/
**/*.ecaGlblOauth-meta.xml
# Scontrol: superseded by Visualforce pages
**/scontrols/
**/*.scf
**/*.scf-meta.xml
Git's ignore file, kept as .gitignore. It is the file of a new project, with three blocks after it: the files that hold a credential, the output of the pipeline, and the Tier 0 block:
Expand to show .gitignore
# This file is used for Git repositories to specify intentionally untracked files that Git should ignore.
# If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore
# For useful gitignore templates see: https://github.com/github/gitignore
# Salesforce cache
.sf/
.sfdx/
.localdevserver/
deploy-options.json
# LWC VSCode autocomplete
**/lwc/jsconfig.json
# LWC Jest coverage reports
coverage/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Dependency directories
node_modules/
# ApexPMD cache
.pmdCache
# Eslint cache
.eslintcache
# MacOS system files
.DS_Store
# Windows system files
Thumbs.db
ehthumbs.db
[Dd]esktop.ini
$RECYCLE.BIN/
# Local environment variables
.env
# Python Salesforce Functions
**/__pycache__/
**/.venv/
**/venv/
# Credentials. No file that holds a credential is committed.
# A private key, such as the key of a JWT authorisation
*.key
*.pem
# A file that holds an SFDX authorisation URL
authFile.json
# Output of the pipeline tools and of the sample workflows, when they run on a
# developer's computer
code-analyzer*
!code-analyzer.yml
delta/
test-results/
# Apex that was retrieved from an org. The leading slash keeps scripts/apex/.
/apex/
# Tier 0 - prohibited metadata. Never commit, retrieve, or deploy.
# The entries below are examples only. Include every item on your
# organisation's own Tier 0 list, not just these.
# Keep in step with the tier0 list in metadata-risk-tiers.yml, the Tier 0
# push ruleset, and the ProhibitedMetadataType rule.
# FlowDefinition: Salesforce manages the active Flow version server-side
**/flowDefinitions/
**/*.flowDefinition-meta.xml
# Certificate: managed manually in each org
**/certs/
**/*.crt
**/*.crt-meta.xml
# ExtlClntAppGlobalOauthSettings: holds the OAuth consumer key and secret
**/extlClntAppGlobalOauthSets/
**/*.ecaGlblOauth-meta.xml
# Scontrol: superseded by Visualforce pages
**/scontrols/
**/*.scf
**/*.scf-meta.xml
The project file, kept as sfdx-project.json. It has a directory for the metadata that is in no package, and a directory for one unlocked package with no namespace. packageAliases holds the ID that Salesforce gives the package when it is created, and the ID here is a placeholder. force-app is the default directory, so at a release Release Please writes a versionNumber there, and it leaves the versionNumber of the package as it is:
sfdx-project.json
{
"packageDirectories": [
{
"path": "force-app",
"default": true
},
{
"path": "service-cloud",
"package": "Service Cloud",
"versionName": "Version 1.4",
"versionNumber": "1.4.0.NEXT"
}
],
"name": "my-project",
"namespace": "",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "67.0",
"packageAliases": {
"Service Cloud": "0Ho000000000000AAA"
}
}
Jest¶
Jest runs the unit tests of Lightning web components. It is how the Testing principle, that behaviour is proven and not assumed, is applied to them (Testing). The tests run in Node.js, with no org and no login, so they are among the fastest checks of the pipeline (LWC Jest Test Execution). Jest does not test Aura components.
Most organisations start with few Jest tests, or with none. So these standards ask for coverage only where a test exists: a component that has a Jest test has tests that run 80% of its lines, and a component with no test is held to nothing.
Files¶
jest.config.js, in the root of the repository, holds the settings. It starts from the settings of Salesforce's own package.jest.setup.js, beside it, runs before each test file. It registers the accessibility check, and it is the place for any other setup that every test needs.- A test is a file whose name ends in
.test.js, in the__tests__folder of its component..forceignorekeeps these folders out of every deployment (Salesforce DX Project Files). package.jsonhas the npm scripts that run Jest, and the two packages below.- The team that governs the pipeline owns
jest.config.js,jest.setup.js, andpackage.json(Repositories).
| Package | Purpose |
|---|---|
@salesforce/sfdx-lwc-jest |
Runs Jest for Lightning web components. It brings Jest, the settings that the configuration file starts from, and stand-ins for the lightning and @salesforce modules that a component imports. A new Salesforce DX project has it. |
@sa11y/jest |
Accessibility checks for Jest tests, from Salesforce. A new project does not have it: npm install --save-dev @sa11y/jest adds it. |
Settings These Standards Set¶
- The settings of Salesforce's package come first.
jest.config.jstakesjestConfigfrom@salesforce/sfdx-lwc-jest/configand adds to it. Those settings count the JavaScript of every Lightning web component in the coverage, also a component that no test loads, which then counts as 0%. - A component that has a test has 80% line coverage.
jest.config.jsfinds each component folder that has a test file, and gives it acoverageThresholdof 80% for lines, counted over all the JavaScript files of the component. The number is in that file, and in no other. Jest then ends with an error when a tested component is below 80%, on a developer's computer and in GitHub. - A failing test blocks. On a pull request the gate fails, and on a push the run fails.
- A tested component below 80% blocks. On a pull request the gate fails, and on a push and each night the run fails. The rule holds for every tested component of the repository, and not only for the components that a pull request changes: Jest is fast, it runs every test each time, and a developer can run the same check on a computer before the push.
- A component with no test is held to nothing. No test means no failure. The workflow names each component that has no test file in its report, and that is all.
- The result is on the page of the workflow run. The job summary has the same kind of tables as the report of the Apex tests: a test summary, the failing tests with the message of each, the tested components below their target, the components with no test, and the coverage of every component.
- The pull request has the result too. The workflow writes one comment on the pull request, and each run updates it. The comment is the same report, without the coverage of every component.
- The result files are stored. The workflow uploads the JSON result of Jest, the report, the coverage of each component, and the coverage files as an artifact named
jest-results, so that a later job or another workflow can read them. - Every test runs each night. The nightly workflow runs all Jest tests with coverage and sends the report to Slack, as it does for the Apex tests (Scheduled Nightly Test Runner).
- Jest does not read a delta package. A delta package holds a copy of each changed component, with its tests.
jest.config.jsleaves thedelta/folder out, and the workflow removes the copies of the tests, so that each test runs once. - Coverage goes to Codecov with the flag
LWC, where it is reported and does not block (Codecov).
What an Organisation Customises¶
- Where to start. An organisation with few tests writes them one component at a time. Each component is held to the target from its first test, and the others wait.
- The number. 80% of lines is the target of these standards. An organisation can set a higher number, or add a target for branches, functions, or statements, in the same file.
- How a test checks accessibility.
@sa11y/jesthas two ways, and the setup file chooses one. The first is a line in the test,await expect(element).toBeAccessible();, which the sample uses; a test without the line checks nothing. The second is the automatic checks: the package checks what each test rendered, with no line in the test, so a new test has the check from its first run. - What the check does not see. The tests run without a browser, so the package does not check colour contrast, audio, or video. A check in a browser covers those (Accessibility).
- Stand-ins of its own. Salesforce's package brings a stand-in for each
lightningmodule. An organisation that needs a different one keeps it inforce-app/test/jest-mocks/, and names it inmoduleNameMapperinjest.config.js, as Salesforce's sample applications do (visualforce-to-lwc). - Tests that run before a commit. The commit hook runs the tests that relate to the staged files (Husky and lint-staged).
How It Runs¶
- A developer runs
npm run test:unitwhile writing a component, and the commit hook runs the related tests. - On a pull request, the workflow runs when the delta package holds a Lightning web component. It then runs every Jest test, with coverage. On a push and each night, it runs every test.
- Jest writes its result as a JSON file, and its coverage as a summary and as a report for Codecov.
- The workflow builds the report from those files, writes it to the job summary, and, on a pull request, to the comment.
- The workflow stores the result files as an artifact, and uploads the coverage to Codecov.
- The job fails for a failing test, and for a component that has a test and is below its target.
Sample Jest Files¶
The configuration and the setup file, kept as jest.config.js and jest.setup.js:
Expand to show jest.config.js and jest.setup.js
const fs = require('fs');
const path = require('path');
const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
// A Lightning web component that has a Jest test must have tests that run
// this share of its lines. A component with no test is not held to it.
const LINE_COVERAGE = 80;
// One threshold for each component folder that has a test file. Salesforce's
// settings name the lwc folder of each package directory.
const coverageThreshold = {};
const lwcFolders = (jestConfig.collectCoverageFrom || [])
.filter((pattern) => !pattern.startsWith('!') && pattern.endsWith('/lwc/**/*'))
.map((pattern) => pattern.replace('/**/*', ''));
for (const lwcFolder of lwcFolders) {
const folder = path.join(jestConfig.rootDir, lwcFolder);
if (!fs.existsSync(folder)) {
continue;
}
for (const component of fs.readdirSync(folder, { withFileTypes: true })) {
const tests = path.join(folder, component.name, '__tests__');
if (component.isDirectory() && fs.existsSync(tests) && fs.readdirSync(tests).some((file) => /\.test\.[jt]s$/.test(file))) {
coverageThreshold[`./${lwcFolder}/${component.name}/`] = { lines: LINE_COVERAGE };
}
}
}
module.exports = {
...jestConfig,
// A delta package is a copy of the changed components, with their tests.
// Jest does not read it, so that each test runs once.
modulePathIgnorePatterns: ['<rootDir>/.localdevserver', '<rootDir>/delta/'],
testPathIgnorePatterns: [...(jestConfig.testPathIgnorePatterns || []), '<rootDir>/delta/'],
// Jest's own reports, and a summary that the workflow reads.
coverageReporters: ['clover', 'json', 'lcov', 'text', 'json-summary'],
coverageThreshold,
// The setup file runs before each test file. It registers the accessibility
// matcher of @sa11y/jest, and it is the place for other setup of the tests.
setupFilesAfterEnv: [...(jestConfig.setupFilesAfterEnv || []), '<rootDir>/jest.setup.js']
};
The setup file, kept as jest.setup.js:
const { setup } = require('@sa11y/jest');
// Register the matcher, so that a test can say:
// await expect(element).toBeAccessible();
setup();
// The other way: check what each test rendered, with no line in the test.
// Use this call in place of the one above.
// setup({ autoCheckOpts: { runAfterEach: true } });
A test with the accessibility line. The check is asynchronous, so the test function is async and the line starts with await:
Jest test file
import { createElement } from 'lwc';
import GoodCard from 'c/goodCard';
describe('c-good-card', () => {
afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
});
it('is accessible', async () => {
const element = createElement('c-good-card', { is: GoodCard });
document.body.appendChild(element);
await expect(element).toBeAccessible();
});
});
The workflow file is in Appendix A: LWC Jest Tests.
Sources:
- Apex Log Analyzer - Visual Studio Marketplace
- Apex PMD - Visual Studio Marketplace
- Codecov YAML
- Codecov: Code Coverage Testing & Insights Solution
- Configuration File · Prettier
- Configuration Files - ESLint - Pluggable JavaScript Linter
- Decomposed Metadata Types - Salesforce DX Developer Guide
- Dependabot - GitHub Docs
- ESLint - Visual Studio Marketplace
- Gearset integration with GitHub Actions - Gearset Help Center
- Git - gitignore Documentation
- gitleaks/gitleaks-action: Protect your secrets using Gitleaks-Action
- gitleaks/gitleaks: Find secrets with Gitleaks
- googleapis/release-please: generate release PRs based on the conventionalcommits.org spec
- How to Exclude Source When Syncing - Salesforce DX Developer Guide
- Is the component you're deploying referenced in the .forceignore /… - Gearset Help Center
- jest · salesforce/sa11y
- jest.config.js · trailheadapps/visualforce-to-lwc
- LWS Limitations - Security for Lightning Components
- Minimum Release Age - Renovate Docs
- Options · Prettier
- peter-evans/create-or-update-comment: A GitHub action to create or update an issue or pull request…
- peter-evans/find-comment: A GitHub action to find an issue or pull request comment
- Prettier - Code formatter - Visual Studio Marketplace
- Prettier Code Formatter - Salesforce Extensions for Visual Studio Code
- prettier/eslint-config-prettier: Turns off all rules that are unnecessary or might conflict with Prettier.
- Push protection - GitHub Docs
- Renovate Docs
- Replace Strings in Code Before Deploying or Packaging - Salesforce DX Developer Guide
- Review recommended rules config for func-names and no-unused-expressions · forcedotcom/eslint-plugin-aura
- Salesforce Code Analyzer - Visual Studio Marketplace
- Salesforce DX Project Configuration - Salesforce DX Developer Guide
- Salesforce Extension Pack - Visual Studio Marketplace
- salesforce/sfdx-lwc-jest: Run Jest against LWC components in SFDX workspace environment
- Secret scanning - GitHub Docs
- Share Configurations - ESLint - Pluggable JavaScript Linter
- Sharing configurations · Prettier
- Step Two: Set Up Authentication - REST API Developer Guide
- Supported secret scanning patterns - GitHub Docs
- trufflesecurity/trufflehog: Find, verify, and analyze leaked credentials
- x2od/prettier-config: Central Prettier config file
- XML - Visual Studio Marketplace
- Yelp/detect-secrets: An enterprise friendly way of detecting and preventing secrets in code.