Skip to content

GitHub Workflow Architecture for Salesforce

Repository Structure - Organising for Governance

The repository structure decision is the first architectural choice a Salesforce delivery team makes, and it has downstream consequences for every governance pattern described in this whitepaper. Two primary models exist, with a third variant worth acknowledging.

Package-based development is the recommended model. In a package-based project, the Salesforce codebase is decomposed into unlocked packages - discrete, versioned units of functionality with explicit dependency declarations. Each package lives in its own repository, has its own pipeline, its own quality gates, and its own release lifecycle. The sfdx-project.json defines the package directory structure, and the pipeline validates and promotes package versions independently. Ideally an org uses unlocked packages, together with org-dependent unlocked packages for the metadata that is not yet untangled from the rest of the org. An org-dependent unlocked package may depend on unpackaged metadata in the org where it is installed, and its metadata is validated when the package is installed and not when the version is created (Create Org-Dependent Unlocked Packages).

Automated package version creation - triggering sf package version create as part of a merge workflow, producing beta package versions on merges to the int, qa, or staging branch and finalised promoted versions on merges to main - is a natural extension of this pipeline architecture. Package version creation and the broader package release lifecycle are outside the scope of this whitepaper for the reasons described in the scope note at the end of this section.

The governance implications of package-based development are significant and should be planned for explicitly. When quality gate configuration - code-analyzer.yml, PMD ruleset XML, eslint.config.js, jest.config.js, secret scanning configuration, Prettier config - lives across multiple package repositories, the risk of configuration drift between repositories is real and consequential. A PMD ruleset that diverges between the core package and the integration package means the same Apex pattern is flagged in one context and silently accepted in another. The central repository pattern from Section 8 is the most elegant solution: configuration standards are defined once or defined and updated via centralised scripts, consumed by all package repositories, and protected so that no individual package team can modify them unilaterally. Package-based development and centralised governance are not in tension - they require each other.

Org-based development - a single repository containing all metadata for the org, deployed as a unit - remains a valid and widely used model, particularly for projects with a single Salesforce org, a small team, or a codebase that has not been decomposed into discrete functional domains. The governance architecture described in this whitepaper applies equally to org-based development; the pipeline structure is simpler because there is only one repository to govern, but the quality gate chain, branch protection model, and enforcement mechanisms are identical. For new projects where package decomposition is feasible, package-based development is the preferred starting point - org-based development should be a deliberate choice based on project constraints, not the default chosen by inertia.

Monorepo is a third pattern worth acknowledging: all packages live in a single repository, with path-based filtering used to scope pipeline execution to the packages affected by a given change. This offers the governance simplicity of a single configuration surface with some of the modularity benefits of package-based development. It introduces its own complexity - path filtering logic, package-scoped test execution, and concurrent pipeline execution across affected packages - and is most appropriate for teams with strong monorepo tooling experience. It is a valid architectural choice but outside the primary scope of this whitepaper.

Two topics are adjacent to this whitepaper's scope but deliberately excluded from it.

Package version creation and validation sf package version create, sf package version validate, beta-to-released promotion, dependency version pinning, and the subscriber installation lifecycle are all package release lifecycle concerns, not DevSecOps governance concerns. The quality gates this whitepaper defines (static analysis, test execution, metadata validation, secrets scanning) apply equally to package-based and org-based development and are a prerequisite to any package promotion workflow. But the promotion workflow itself (the mechanics of creating, validating, and installing a package version) is a distinct architectural concern that belongs in a dedicated package development standard rather than here. The GitHub Actions workflow patterns in Appendix A provide the structural foundation from which package version creation jobs can be composed as additional merge-triggered steps once the quality gate chain is stable.

Scratch org creation and developer environment management org shape design, project-scratch-def.json authoring, scratch org pool architecture, org snapshot strategy, and the tooling used to provision and reclaim ephemeral environments are all developer environment concerns. They are essential for teams adopting package-based development and warrant careful planning, but they are not DevSecOps controls. This whitepaper addresses scratch orgs only in the context of the environment topology they occupy in the branch-to-org mapping and the lightweight quality gates that run against feature branches targeting them. The full design of a scratch org provisioning pipeline is outside scope.

Organisations adopting package-based development for their production org should treat both topics as required companion standards to this whitepaper, not as covered by it.

The Pipeline and What It Protects

TODO(David)

Review this section. First draft, 2026-10-27. It explains in plain words what a pipeline is, what can go wrong on the way to production, and what these standards protect against. It is for a reader who meets the quality gates for the first time in the sections that follow.

A pipeline is the one path that a change progresses along from the org where a developer makes it to the production org. The path runs through a series of stops, called branches, in the repository, and each branch is connected to one org. Each step along the path is a pull request, and the pipeline checks each change at every step. No change reaches production another way.

The Path of a Change

A change starts in a scratch org, on a feature branch, where one developer builds it and tests it alone. A pull request takes it to the int branch, and the pipeline deploys it to the int sandbox. That is the first org where the changes of several developers meet. The next pull request takes it to the qa branch and the QA sandbox, where the QA team tests the functionality against the requirements. The next takes it to the staging branch and the staging sandbox, usually a full sandbox and the closest copy of production. There the change is tested at scale, with every automation and integration. The last pull request takes it to main, and the pipeline deploys it to production. The branches and the orgs are mapped under Environment Topology and Org Mapping below. The approvals that each step needs, with a picture of the path, are under Branches, Protections, and Approvals on the Version Control page.

Code and configuration take the same path. An Apex class, a Flow, a permission set, and a validation rule are all metadata in the repository. The pipeline deploys, checks, and reviews each of them in the same way.

The pipeline is also the only path. A change made by hand in a sandbox or in production, with clicks in Setup, is in no branch. No gate has checked it, and no reviewer has seen it (The Platform Is Different, and Governance Must Account for That). A scheduled check compares the production org with the repository and reports every such change (Scheduled: Metadata Drift Detection).

What Can Go Wrong

A change can break an org in many ways, and a developer sees few of them in the editor. Some examples:

  • An Apex class has no test, or a test that proves nothing, and it fails in production on data that the developer never saw.
  • A Flow has no fault path, and a business process stops without a message to anyone.
  • A permission set grants every user a field that only a few people should see.
  • A credential is committed in a file, and everyone who can read the repository can use it.
  • A field is deleted, and a report, a formula, or an integration that depended on it fails.
  • A component bundles a JavaScript library with a known vulnerability, and every page that uses the component carries it.
  • A change deploys to the scratch org and fails to deploy to production, because production holds metadata that the scratch org did not.

The pipeline checks for each of these before the change reaches the next org, and the earliest check is the cheapest. A defect found in the scratch org costs the developer a few minutes, and nobody else is affected. The same defect found in the int sandbox blocks the other developers. Found in QA, it costs a test cycle. Found in staging, it costs a release. Found in production, it costs the users, sometimes their data, and the fix passes every gate again. So the pipeline runs most of its checks at the first pull request. The editor and the commit hook run some of them before the change leaves the developer's machine (Shift-Left Security in a Salesforce Context, Core Principles). Each later stage adds the checks that only it can make, such as a validation against the full sandbox. A change that passed every earlier gate is not checked less.

What the Standards Protect

These standards exist to prevent two outcomes: a poorly functioning org, and data that leaves it for someone who should not have it. The first is an outage, a failed deployment, or an automation that fails silently. It is also a drifted org that no longer deploys, and technical debt that makes every later change slower and riskier. The second is a record that the wrong user can read, or a credential in a file or a log. It is also a guest user with more access than a page needs, a library with a known vulnerability, or an agent with more actions than its purpose requires. Most begin as a change that no gate checked and no reviewer saw.

The protection is in two places. In the pipeline, every change is formatted, scanned, tested, and validated before it is deployed, and a person approves it. The gates are set out in Section 5 for code, Section 6 for metadata, and Section 7 for secrets and dependencies. Each one is a standard under Quality Gates on the Pipeline Standards page.

In the org itself, four controls hold against whatever the pipeline let through, and against a change that was never in the pipeline:

The pipeline itself is a third place. It holds the credentials of every org. Its secrets, its workflow files, and its dependencies are therefore protected with the same care as the org (Section 7, Supply Chain Security). What an organisation gains from all of this is listed under Expected Outcomes on the Principles page.

Branch Strategy

The branch strategy defines how code moves from a developer's local environment through the pipeline to production. Two models dominate Salesforce delivery projects, each with meaningfully different implications for pipeline design and release governance.

Trunk-based development organises all work around a single long-lived branch - typically main - with developers working in short-lived feature branches that are merged frequently, often daily. The discipline of frequent integration means conflicts are small, the main branch is always in a deployable state, and the feedback loop between writing code and integrating it with the rest of the team's work is tight. Feature flags or scratch org-based isolation handle in-progress work that is not yet ready for production. Trunk-based development pairs naturally with high deployment frequency and on-demand production releases - the continuous delivery model described in Section 2.

GitFlow organises work around multiple long-lived branches - typically develop, one or more release branches, and main - with feature branches merging into develop and release branches acting as a stabilisation layer before production promotion. GitFlow is more familiar to teams coming from sprint-based Salesforce delivery models and maps naturally to fixed release cadences. It introduces more merge complexity and longer integration cycles, but provides explicit staging points that some client change management processes require. For projects where the client's ITSM or CAB process mandates a formal release branch, GitFlow or a GitFlow-adjacent model is the pragmatic choice.

Neither model is universally superior - the right choice depends on the project's release cadence, team size, client governance requirements, and the maturity of the team's conflict resolution practices. What matters more than the choice of model is that the branch topology is defined explicitly at project kickstart, mapped to the environment topology below, and enforced via GitHub Rulesets rather than left to convention.

Regardless of which model is adopted, the following branch conventions apply across both:

  • main always represents the production-deployed state - no direct commits, no exceptions.
  • Environment-mapped branches (staging, qa, int) are long-lived, protected, and represent the deployed state of their respective environments.
  • Feature and bugfix branches are short-lived, branched from the appropriate integration point, and deleted after merge. The name of a feature branch starts with feat/.
  • Hotfix branches follow an explicitly defined path to production that bypasses the normal feature branch flow but not the quality gate chain - emergency deployments are never a reason to skip gates, only a reason to expedite approvals.

Environment Topology and Org Mapping

The GitHub branch topology should be an explicit, documented reflection of the Salesforce environment topology. Every environment that receives deployments should have a corresponding protected branch, and every protected branch should have a corresponding required workflow that validates changes before they are promoted.

A typical Salesforce project environment topology, mapped to branches:

  • Scratch orgs → developer feature branches (feat/); each developer works against their own scratch org, created from a scratch org definition file versioned in the repository; no shared state, maximum isolation.
  • Integration sandbox → int branch; the first shared environment where multiple developers' changes are integrated and tested together. It is a Developer Pro sandbox, for its larger storage.
  • QA sandbox → qa branch; where the QA team tests the functionality and checks it against the requirements; changes promoted here have passed all automated gates and peer review.
  • Staging sandbox → staging branch; where testing at scale happens, with all automations and integrations; usually a full sandbox, the closest approximation of production; the target for sf project deploy in the CI pipeline.
  • Production → main; changes merged here trigger the production deployment workflow.

The pipeline's behaviour changes as changes move up this topology. Early branches - feature branches targeting developer sandboxes or scratch orgs - run lightweight gates: formatting verification, static analysis, Jest tests where applicable. As changes approach production, gates accumulate: full test execution, metadata validation against the full-copy sandbox, elevated reviewer requirements, destructive change scrutiny. The cost of the gate chain increases with proximity to production because the cost of a failure increases correspondingly.

Scratch org management deserves explicit planning in package-based projects. Scratch orgs are ephemeral by design - created for a development task and deleted when it is complete - but they need to be created consistently and reproducibly. The scratch org definition file (project-scratch-def.json) should be versioned in the repository, and the pipeline should include a step that validates scratch org creation as part of the PR gate chain for changes that affect scratch org configuration. A scratch org pool - a set of pre-created scratch orgs maintained by a scheduled workflow and checked out by developers on demand - can significantly reduce the feedback latency that fresh scratch org creation introduces. The design of that provisioning pipeline (pool sizing, org shape strategy, reclaim scheduling, and snapshot management) is a developer environment concern outside the scope of this whitepaper, as noted in the scope boundary above. Creating scratch orgs from a pipeline does constrain how the Dev Hub is authorised, as covered in Section 7.

Three Distinct Pipeline Concerns

Governance in a Salesforce pipeline operates across three fundamentally different workflow triggers that should not be conflated architecturally:

Scheduled workflows run on a defined cadence, independently of any developer activity. Their purpose is continual org surveillance: detecting drift, flagging configuration anomalies, and generating governance reports that inform architectural decisions. They are set out under Scheduled: Metadata Drift Detection in Section 6.

PR-time workflows run synchronously against every pull request and enforce hard quality gates. Their purpose is change control: ensuring that every proposed change to the org meets defined standards before it can be merged and promoted. The gates themselves are set out in Section 5 for code and in Section 6 for metadata.

Push workflows run in response to every branch push and serve as lightweight quality checks, functioning as a redundant check to VS Code auto-formatting and problem presentation with tools such as Code Analyzer or CodeScan. This is part of a shift-left mentality: a push workflow cannot stop a commit from being made, but by flagging a commit that does not meet standards as soon as it is pushed, the organisation saves time and money versus catching these only when the PR is created. Their design principles are set out under Push-Time: Lightweight Local Feedback and Prevention.

Workflow Trigger Strategy

GitHub Actions workflows are triggered by events. Choosing the right trigger for each workflow is an architectural decision that affects pipeline speed, resource consumption, and the reliability of the feedback signal.

Pull request triggers (on: pull_request) are the primary trigger for quality gate workflows. They fire when a PR is opened, updated, or marked ready for review, giving developers immediate feedback on every push. The paths: filter should be used to scope expensive gates - Jest execution, metadata validation - to PRs that actually modify the relevant file types, avoiding unnecessary execution on changes that cannot affect the gate's outcome.

Push triggers (on: push) are appropriate for deployment workflows that execute after a PR is merged to a protected branch, and for the push quality baseline workflow that runs lightweight checks on every branch push. Push triggers on protected branches should be restricted to the deployment action itself - not a repeat of the full PR gate chain, which has already passed before the merge was allowed. Push-triggered workflows on all branches serve a qualitatively different purpose from PR gates: where PR gates are enforcement mechanisms that block merge on failure, push-triggered workflows provide early feedback on a branch regardless of whether a PR has been opened - reinforcing the shift-left model by surfacing formatting violations, static analysis findings, and Jest failures before a developer requests review. On a feature branch, where work in progress is pushed, only the secret scan and the formatting check run: a pushed secret stays on GitHub even after a squash merge, and the static analysis and Jest tests wait for the pull request. The design principles are under Push-Time: Lightweight Local Feedback and Prevention below, and the full implementation of this pattern is provided in Appendix A.1.1.

A second category of push-to-main workflow is automated changelog and release management. Because every merge to main carries a well-formed Conventional Commits message - enforced by amannn/action-semantic-pull-request as a required status check - a push trigger on main can drive googleapis/release-please-action to maintain a rolling Release PR: a continuously updated pull request that accumulates all unreleased changes into a changelog, determines the next SemVer version from the commit type prefixes (fix: → patch, feat: → minor, feat!: or BREAKING CHANGE: footer → major), and creates a tagged GitHub Release when that PR is merged. Release Please ships a native sfdx release type that understands sfdx-project.json versioning, making this the right release-type for all Salesforce projects. This is a push trigger on main with a distinct, narrow purpose - it does not re-run quality gates; it reads the already-validated commit history and manages release bookkeeping. The full annotated workflow is provided in Appendix A.2.13. Its files and its settings are in Version Control, Release Please.

Scheduled triggers (on: schedule) drive the surveillance workflows described in Section 6: the weekly project scan (drift detection and a full Code Analyzer scan), full-org Apex test runs, Apex compilation checks, and CPD scans. Scheduled workflows run independently of developer activity and should be treated as a distinct workflow category with their own notification and failure handling patterns - a scheduled workflow failure at 2am should not page the on-call developer; it should create a GitHub issue or post to a monitoring channel for review at the start of the next working day.

Manual dispatch triggers (on: workflow_dispatch) provide an escape valve for workflows that need to be run on demand - a full org refresh, an emergency validation, a one-time metadata retrieve for drift baseline establishment. Manual dispatch workflows should require input parameters that make the intent explicit - which org, which branch, what operation - and should be restricted to authorised GitHub Teams via environment protection rules rather than available to all repository contributors.

Concurrency controls should be defined explicitly on all org-connected workflows. A GitHub Actions concurrency group scoped to the target org and branch prevents simultaneous workflow runs from competing for the same org's test execution lock - a common source of flaky CI behaviour in Salesforce pipelines where two simultaneous validations against the same sandbox produce conflicting results. The cancel-in-progress option should be set to true for PR-triggered workflows - a new push to an open PR should cancel the in-progress run and start fresh rather than completing a validation against a stale commit.

Push-Time: Lightweight Local Feedback and Prevention

Push workflows operate at commit time, before a PR is created, and serve as a shift-left mechanism to catch issues early in the development cycle. Unlike PR-time workflows which enforce hard blocking gates, push workflows provide fast feedback and lightweight prevention, functioning as a redundant check to local development tooling.

Push workflow design principles:

  • Fast execution: Some organisations recommend frequent commit and push to version control when working on a development branch, so push workflows must complete in seconds, not minutes. Heavy static analysis, full test execution, and metadata validation belong in PR-time gates; push-time checks should focus on formatting, linting, and basic syntax validation with static analysis.
  • The changed files only: Static analysis on a push scans changed files, and not the whole repository. A scan of everything is slow in a large repository, and it reports the violations of old code on every push: the summary is never short, and a team that is cleaning up old code cannot see what a push added. A scan of the whole repository is a separate workflow that a person starts (Appendix A.1.7).
  • Problem presentation: Rather than blocking commits unconditionally, push workflows typically post findings as structured annotations on a failed workflow, allowing developers to review and commit with awareness.
  • Redundancy to local tooling: Push workflows reinforce checks already available locally via VS Code extensions (Code Analyzer, CodeScan) and pre-commit hooks. They catch developers who skip local checks and provide organisational consistency across the team. The local tools, and the gate each one pre-empts, are listed under Local Checks Before the Push.
  • Formatting and style consistency: Push workflows commonly enforce code formatting and linting (Prettier, ESLint) and flagrant style violations (unused variables, incorrect naming conventions).
  • Reusable workflow integration: Push workflows should be implemented as reusable workflow_call targets (Appendix A.1.1) invoked from the baseline push gate, allowing centralised maintenance across all team repositories.
  • Scope limitation: Push workflows should run on all branches, as they do lightweight checks and are the first line of defence.

Push workflows are not a substitute for PR-time gates or for robust local checks; they are a complement, catching low-hanging fruit at an early point in the development cycle and reinforcing organisational standards without the cognitive load of mandatory blocking gates on every feature branch commit.

Local Checks Before the Push

Push workflows can stay fast only if most problems never reach them. The more checks run on the developer's own machine, the fewer violations a push workflow has to find, and the fewer still survive to PR time. Each tool below pre-empts a specific gate, which is the point of shifting left: the same standard, enforced earlier and at lower cost. Only Visual Studio Code tooling is considered here; other editors have their equivalents.

Local tool What it checks on the developer's machine Gate it pre-empts
Salesforce Code Analyzer extension Runs the Code Analyzer engines (PMD, ESLint, RetireJS and the rest) when a file is opened or saved, from the same code-analyzer.yml the pipeline uses, so the editor shows what the gate will report. Its codeAnalyzer.ruleSelectors setting chooses the rules it runs, and the gate's two tags go there (Appendix E) Static Code Analysis (A.2.9)
Apex PMD extension Runs PMD on Apex as files are opened, saved or changed, from a ruleset file such as the custom ruleset in Appendix F.2 The PMD findings in A.2.9
ESLint extension Reports lint findings as the developer types, from the repository's eslint.config.js The ESLint findings in A.2.9, and the push baseline (A.1.1)
Prettier extension Formats each file on save, from the repository's Prettier configuration file Prettier Formatting Verification (A.2.1)
Husky running lint-staged As the commit is made, whichever editor made the change: runs Prettier on the staged files, ESLint on the staged JavaScript of Aura components and Lightning Web Components, and the Jest tests that relate to a changed Lightning Web Component (Version Control: Husky and lint-staged) A.2.1, the ESLint findings in A.2.9, and A.2.10
Salesforce Extension Pack test runners Run LWC Jest tests and Apex tests from the editor before pushing LWC Jest Tests (A.2.10) and Metadata Validation and Apex Tests (A.2.11)
CodeScan and SonarQube for IDE extensions Where a commercial analyser is the chosen tool (Section 5), its extension shows that analyser's findings in the editor. Both depend on a connection to the organisation's server or cloud project; SonarQube for IDE analyses Apex only in connected mode The equivalent static analysis gate
XML (Red Hat) Flags malformed metadata XML as it is edited Loosely, Metadata Validation and Apex Tests (A.2.12), which a malformed file would fail

The gates that these tools pre-empt are listed in Appendix A, by trigger: the tool behind each gate, what the gate does, and what happens when it finds a violation.

Salesforce's Expanded extension pack no longer bundles the ESLint and Apex PMD extensions: it recommends the Code Analyzer extension, which runs both engines. Both remain available on their own, and a team that runs PMD or ESLint standalone can keep using them. These standards recommend the Code Analyzer extension in place of the ESLint extension, so that the editor and the gate apply the same rules at the same severities (Version Control: ESLint). The CodeScan extension does not run alongside SonarQube for IDE or Apex PMD, so a team picks one analyser's extension rather than stacking them.

Some gates have no counterpart in the editor. Secret scanning can run in the same pre-commit hook (Section 7), but destructive change detection, metadata risk classification and deployment validation run only in the pipeline.

PR and Commit Message Conventions

Consistent, machine-readable PR titles and commit messages are the foundation of automated changelog generation, release tagging, and deployment traceability. Without them, the audit trail from Section 8 becomes a chronological list of opaque commit SHAs rather than a readable history of what changed and why.

Conventional Commits is the required format for PR titles, and an optional one for the individual commits on a branch (see below). The format is simple: a type prefix (feat:, fix:, chore:, refactor:, docs:, test:), an optional scope in parentheses, and a concise description. The type prefix carries semantic meaning that downstream tooling - semantic-release (GitHub), release-please (GitHub) - uses to determine version bumps and populate changelogs automatically. A feat: commit increments the minor version; a fix: commit increments the patch; a commit with a BREAKING CHANGE: footer increments the major version.

Enforcement is via the amannn/action-semantic-pull-request (GitHub) GitHub Action, one of the gates behind the Quality Gates check that every protected branch requires. A PR whose title does not conform to the Conventional Commits format is blocked from merge before a reviewer ever sees it - keeping the enforcement in the pipeline rather than in code review feedback.

Squash merging should be the only permitted merge strategy on all protected branches. When a PR is squash-merged, the PR title becomes the commit message on the target branch - which is the reason that PR title enforcement is the critical control point rather than individual commit message enforcement during development. The resulting main branch history is linear and meaningful: each commit represents one logical unit of work, carries a well-formed Conventional Commits message, and is directly traceable to the PR, the reviewer approvals, and the gate results that preceded the merge.

Commit messages on a branch are a different matter, and organisations differ on them. The standards here enforce Conventional Commits on PR titles and leave individual commit messages to each organisation. An organisation that wants them enforced should use a Husky commit-msg hook running commitlint or an equivalent check, which rejects a non-conforming message before the commit is made. These standards do not recommend enforcing it at scale, for two reasons. A local hook only sees commits made on a machine where it is installed: a commit made through the GitHub web interface never passes through it, so the control has a gap that no local tooling can close. Under squash merging the individual commit messages are discarded at merge, so they are far less useful than the PR title; enforcing them makes extra work for developers with little payoff.

The combination of Conventional Commits enforcement and squash merging enables fully automated release management: when a merge to main occurs, a release workflow reads the commit messages since the last tag, determines the appropriate version bump, generates the changelog, creates the GitHub Release, and tags the commit - all without human intervention. The push baseline workflow in Appendix A.1.1 calls the Release Please reusable workflow on every push to main, so the release infrastructure is in place from day one of the engagement. The full annotated release workflow is in Appendix A.2.13.

PR Review Governance and Auto-Assignment

Reviewer requirements should be calibrated to the risk profile of the target branch, not applied uniformly across all branches. A flat policy that requires two approvals on every PR - including a developer's own feature branch targeting a personal scratch org - creates friction with no corresponding risk reduction. The architectural principle is: protection increases as branches approach production.

A practical graduated model mapped to the environment topology above. The levels are named rather than numbered, because the word "tier" is reserved for the metadata risk tiers in Section 6:

Unprotected branches (feat/*, scratch org branches): Developers can open, approve, and merge their own PRs freely. The quality gate chain still runs - static analysis, Jest tests, formatting verification - but no human reviewer is required. This enables rapid iteration against personal scratch orgs without process overhead.

Lightly protected branch (int, the integration sandbox branch): At least one peer reviewer required. Auto-assignment via GitHub Teams ensures a reviewer is always designated without requiring the PR author to manually select one. Quality gates remain mandatory.

Fully protected branches (qa, staging, and main): Two or more reviewers required, at least one from a designated senior reviewer GitHub Team. Auto-merge is disabled unconditionally. Destructive changes and Tier 1 metadata type modifications (Section 6) trigger mandatory assignment of an additional specialist reviewer.

GitHub Teams as reviewer groups: Reviewer requirements should reference GitHub Teams rather than named individuals - this decouples the policy from personnel changes, survives team reorganisations, and applies consistently across all repositories in the GitHub organization without per-repository maintenance overhead.

CODEOWNERS file: A versioned CODEOWNERS file in the repository's .github/ folder maps path patterns to GitHub Teams, enabling automatic reviewer assignment scoped to the specific files changed in a PR. Governance configuration files - static analysis configuration, eslint.config.js, jest.config.js, formatting standards, secret scanning configuration, reusable workflow definitions - should be explicitly mapped to the designated governance team (referenced as @<my-org>/admin-team in the examples throughout this whitepaper). Every other file is mapped to the team that reviews what will be deployed to the org (@<my-org>/architect-team in the examples), by a default line at the top of the file, so that no change is without a code owner. A sample project CODEOWNERS governance block is in Appendix D. Any PR modifying these files requires an approving review from that team before merge, regardless of how many other approvals the PR has accumulated. This is enforced by configuring the GitHub Ruleset to require Code Owner review as a distinct condition, separate from the general reviewer count requirement - a PR with sufficient general approvals but no governance team approval is still blocked.

Auto-assign GitHub Action: For reviewer assignment logic that goes beyond what CODEOWNERS supports - round-robin assignment within a team, conditional assignment based on PR labels - dedicated auto-assign actions such as kentaro-m/auto-assign-action (GitHub) and pozil/auto-assign-issue (GitHub) provide finer-grained control without requiring manual reviewer selection on every PR.

GitHub Rulesets for enforcement: The branch protection model should be defined as GitHub Rulesets at the organization level rather than as per-repository branch protection rules. Organization-level Rulesets apply across all repositories by branch name pattern, ensuring that the protection topology is consistent without requiring each repository to configure its own branch protection independently. The full Rulesets configuration reference is in Appendix C.

Developer Feedback Quality and Shift-Left Visibility

Automated quality gates are only as effective as the feedback they produce. A pipeline that fails with a raw exit code and a wall of log output shifts the cognitive burden of triage onto the developer - defeating much of the productivity value of automation. Designing feedback quality with the same intentionality as gate logic is an architectural concern, not an afterthought.

The draft PR as a feedback mechanism: GitHub's draft PR status provides a first-class mechanism for developers to solicit automated feedback before a change is ready for human review. A draft PR triggers the full quality gate chain without entering the review queue or notifying required reviewers. Developers should be explicitly encouraged to open draft PRs early and iterate against gate feedback before marking the PR ready for review. This pattern reduces the number of review cycles consumed by issues that automation could have caught, and keeps reviewer attention focused on logic, design, and intent rather than mechanical violations.

Structured static analysis output - the two-table pattern: When static analysis produces violations, whether the tool is Salesforce Code Analyzer, CodeScan, or another, the raw output should be post-processed and surfaced as two distinct tables in the PR:

  • Blocking violations (severity 1 - Critical, severity 2 - High): violations that fail the workflow and must be resolved before merge; presented with rule name, file, line number, engine, and a link to the rule documentation.
  • Non-blocking violations (severity 3 - Moderate, severity 4 - Low, severity 5 - Info): violations that do not fail the workflow but are surfaced for developer awareness and continuous improvement tracking; presented as a collapsible section to avoid overwhelming the primary feedback signal.

This separation is critical: a developer looking at a failed PR needs to immediately distinguish what is blocking them from what is advisory. Mixing severities into a single undifferentiated list produces alert fatigue and encourages blanket suppression. Both tables should be posted as a structured PR comment - updated on each push rather than accumulating multiple comments - and the SARIF output should simultaneously be uploaded to GitHub Code Scanning for centralised tracking across the repository.

Test execution feedback: Test failures and coverage gaps should be surfaced with equivalent structure:

  • Failed tests: All failing test classes and methods listed explicitly with failure messages - developers should be able to identify and navigate to failures directly from the PR comment without parsing raw logs.
  • Low coverage classes: All Apex classes falling below the organisational coverage threshold listed by class with their current coverage percentage - a prioritised remediation list rather than a single aggregate number.
  • Jest failures: Failing LWC test files and specific failing test cases listed in a table with the message of each, built from the JSON output of Jest, consistent with Apex test failure presentation. The same comment names each tested component that is below its coverage target, and each component that has no test.

Workflow job summaries: GitHub Actions job summaries ($GITHUB_STEP_SUMMARY) should be populated with the same structured output - providing a persistent, formatted view of gate results accessible directly from the Actions run without navigating to PR comments. This is particularly useful for scheduled workflow runs not associated with a PR.

VS Code integration - catching violations before the PR: The feedback loop can be shifted even further left than the draft PR through IDE-level tooling. The Salesforce Code Analyzer extension for VS Code surfaces PMD, ESLint, RetireJS, and Flow Analyzer violations inline as the developer writes code - before a commit is made, before a push, before a PR exists. Configuring the extension to use the same code-analyzer.yml and ruleset files that govern the CI pipeline ensures that what the developer sees in the IDE is consistent with what the pipeline will enforce - eliminating the category of surprises where code that looked clean locally fails in CI due to different rule configuration. The VS Code extension should be listed as a recommended extension in the repository's .vscode/extensions.json so that all contributors are prompted to install it when they open the project.