4. GitHub Workflow Architecture for Salesforce¶
Repository Structure - Organizing 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.
Automated package version creation - triggering sf package version create as part of a merge workflow, producing beta package versions on merges to integration or staging branches and finalized 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 centralized scripts, consumed by all package repositories, and protected so that no individual package team can modify them unilaterally. Package-based development and centralized 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 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.jsonauthoring, scratch org pool architecture, org snapshot strategy, and the tooling used to provision and reclaim ephemeral environments — are 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.Organizations adopting package-based development for their production org should treat both topics as required companion standards to this whitepaper, not as covered by it.
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 organizes 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 organizes 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 stabilization 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:
mainalways represents the production-deployed state - no direct commits, no exceptions.- Environment-mapped branches (
staging,uat,integration) 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.
- 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; each developer works against their own scratch org, created from a scratch org definition file versioned in the repository; no shared state, maximum isolation.
- Developer sandbox → integration branch; the first shared environment where multiple developers' changes are integrated and tested together.
- QA / UAT sandbox →
stagingoruatbranch; client-facing testing environment; changes promoted here have passed all automated gates and peer review. - Full-copy / partial-copy sandbox →
integrationbranch where used; the closest approximation of production; the target forsf project deployin the CI pipeline. - Production →
main; changes merged here trigger the production deployment workflow.
The pipeline's behavior 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.
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 every 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. 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.14.
Scheduled triggers (on: schedule) drive the surveillance workflows described in Section 6: nightly drift detection, 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 authorized 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 behavior 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.
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 recommended format for both PR titles and commit messages. 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, configured as a required status check on all protected branches. 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 why 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.
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.14.
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 tiered model mapped to the environment topology above:
Tier 1 - Unprotected (feature/*, 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.
Tier 2 - Lightly protected (integration, developer sandbox branches): 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.
Tier 3 - Fully protected (staging, production branches): 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 reorganizations, and applies consistently across all repositories in the GitHub organization without per-repository maintenance overhead.
CODEOWNERS file: A versioned CODEOWNERS file in the repository root 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). 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 - a dedicated auto-assign action 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 tiered 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 Code Analyzer output - the two-table pattern: When sf code-analyzer run produces violations, 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 centralized 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 organizational coverage threshold listed by class with their current coverage percentage - a prioritized remediation list rather than a single aggregate number.
- Jest failures: Failing LWC test files and specific failing test cases surfaced via JUnit XML reporter output, consistent with Apex test failure presentation.
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.