Skip to content

Appendix C: GitHub Rulesets Configuration Reference

This appendix provides a practical reference for configuring GitHub Rulesets to enforce the governance model described throughout this whitepaper. Rulesets are the enforcement mechanism that transforms documented standards into non-negotiable pipeline controls - applied at the organization level so that protection policies are consistent across all repositories without requiring per-repository configuration.

Full GitHub Rulesets documentation: About rulesets

Organization-Level vs. Repository-Level Rulesets

Organization-level Rulesets apply across all repositories in the GitHub organization by repository and branch name pattern - they are the correct scope for the governance controls described in this whitepaper. Repository-level Rulesets apply only to a single repository and are appropriate for project-specific rules. Repository maintainers may add repository-level Rulesets for project-specific requirements, but they cannot override or weaken organization-level Rulesets.

Branch Protection Rulesets

Each branch ruleset requires one status check, Quality Gates. The reasoning behind one check per gate is explained in One Required Check per PR Workflow below.

Integration Branch Ruleset (lightly protected) - expand to view

{
  "name": "int-branch-protection",
  "target": "branch",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "include": ["refs/heads/int"],
      "exclude": []
    }
  },
  "rules": [
    {
      "type": "pull_request",
      "parameters": {
        "required_approving_review_count": 1,
        "dismiss_stale_reviews_on_push": true,
        "require_code_owner_review": false,
        "require_last_push_approval": true,
        "required_review_thread_resolution": true,
        "allowed_merge_methods": ["squash"]
      }
    },
    {
      "type": "required_status_checks",
      "parameters": {
        "strict_required_status_checks_policy": false,
        "required_status_checks": [
          { "context": "Quality Gates", "integration_id": 15368 }
        ]
      }
    },
    { "type": "non_fast_forward" },
    { "type": "deletion" },
    { "type": "required_signatures" }
  ]
}

QA and Staging Branch Ruleset (fully protected) - expand to view

{
  "name": "qa-staging-branch-protection",
  "target": "branch",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "include": ["refs/heads/qa", "refs/heads/staging"],
      "exclude": []
    }
  },
  "rules": [
    {
      "type": "pull_request",
      "parameters": {
        "required_approving_review_count": 2,
        "dismiss_stale_reviews_on_push": true,
        "require_code_owner_review": true,
        "require_last_push_approval": true,
        "required_review_thread_resolution": true,
        "allowed_merge_methods": ["squash"]
      }
    },
    {
      "type": "required_status_checks",
      "parameters": {
        "strict_required_status_checks_policy": true,
        "required_status_checks": [
          { "context": "Quality Gates", "integration_id": 15368 }
        ]
      }
    },
    { "type": "non_fast_forward" },
    { "type": "deletion" },
    { "type": "required_signatures" }
  ]
}

Production Branch Ruleset (main) (fully protected) - expand to view

{
  "name": "production-branch-protection",
  "target": "branch",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "include": ["refs/heads/main"],
      "exclude": []
    }
  },
  "rules": [
    {
      "type": "pull_request",
      "parameters": {
        "required_approving_review_count": 2,
        "dismiss_stale_reviews_on_push": true,
        "require_code_owner_review": true,
        "require_last_push_approval": true,
        "required_review_thread_resolution": true,
        "allowed_merge_methods": ["squash"]
      }
    },
    {
      "type": "required_status_checks",
      "parameters": {
        "strict_required_status_checks_policy": true,
        "required_status_checks": [
          { "context": "Quality Gates", "integration_id": 15368 }
        ]
      }
    },
    { "type": "non_fast_forward" },
    { "type": "deletion" },
    { "type": "required_signatures" }
  ]
}

The differences between the branches, from these rulesets and from the pull request workflows (Version Control: Protections):

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

One Required Check per PR Workflow

A required status check is matched by the exact name of the check run, and two behaviours of GitHub Actions make one check per gate fragile.

Check names come from the job, not the workflow file. When a caller workflow runs a reusable workflow, the check run is named <caller job name> / <reusable workflow job name>, such as Formatting / Formatting Verification for the prettier-verify job in A.1.3. A required check named after the workflow file (sf-prettier-verify) never matches, and the PR waits for it forever.

Skipped work is reported inconsistently. GitHub's Troubleshooting required status checks page, in the section "Handling skipped but required checks", describes two cases:

  • When "a workflow is skipped by path filtering, branch filtering, or a commit message", its "associated checks stay in a 'Pending' state and block merging".
  • When "a job is skipped by a conditional", "the job reports 'Success'". For a caller job that runs a reusable workflow, the skipped check carries only the caller job's name (Code Analysis), not the full name a ruleset would require (Code Analysis / Static Code Analysis), so the required check never appears.

GitHub's guidance on the same page is to "avoid requiring workflows that can be skipped" and to "use always() with needs for required checks that depend on other jobs". The PR workflows in Appendix A follow both:

  • They have no paths: filter, so they run on every PR. Gates with nothing to check, such as the tests when the PR contains no deployable metadata, skip themselves inside the workflow.
  • Each ends with a Quality Gates job that needs every other job, runs with if: always(), and fails only if a gate failed or was cancelled. A skipped gate counts as passing.

The rulesets require only Quality Gates. Adding, removing, or renaming a gate then changes only the workflow, never the rulesets, and every gate's own result still shows on the PR. The integration_id of 15368 is the GitHub Actions app: it ties the required check to GitHub Actions, so a commit status of the same name posted through the API by anyone else cannot satisfy it.

Tier 0 Push Ruleset

A push ruleset rejects any push containing a Tier 0 path, so prohibited metadata never reaches the remote repository on any branch. Unlike branch rulesets, push rulesets need no branch condition: they apply to every push to the repository and its entire fork network. Push rulesets are available for private and internal repositories, and not on every GitHub plan; where they are unavailable, the Prohibited Metadata Check required status check is the enforcing layer. Patterns use fnmatch syntax, the same syntax as the tier0 list in metadata-risk-tiers.yml, and must match that list and the other enforcement layers. The paths in restricted_file_paths below are examples only: include every item on your organisation's own Tier 0 list, not just these. (JSON does not allow comments, so this note cannot go in the ruleset file itself.)

This ruleset is redundant on purpose. The other layers already stop Tier 0 metadata from being merged.

But both of them act on a pull request, after the file has already reached a branch of the remote repository. The push ruleset acts one step earlier, on the push itself, so the file never reaches GitHub, and no workflow has to run for it to hold. A credential or a certificate that reaches the remote repository is exposed even if it is never merged, so this one layer is worth keeping beside the others.

Tier 0 Prohibited Metadata Ruleset

{
  "name": "tier-0-prohibited-metadata",
  "target": "push",
  "enforcement": "active",
  "conditions": {
    "repository_name": {
      "include": ["~ALL"],
      "exclude": []
    }
  },
  "rules": [
    {
      "type": "file_path_restriction",
      "parameters": {
        "restricted_file_paths": [
          "**/flowDefinitions/**",
          "**/*.flowDefinition-meta.xml",
          "**/certs/**",
          "**/*.crt",
          "**/*.crt-meta.xml",
          "**/extlClntAppGlobalOauthSets/**",
          "**/*.ecaGlblOauth-meta.xml",
          "**/scontrols/**",
          "**/*.scf",
          "**/*.scf-meta.xml"
        ]
      }
    }
  ]
}

Leave the bypass list for this ruleset empty. Tier 0 metadata has no legitimate route into version control, so no actor needs to push it.

Required Workflows Ruleset

Required Workflows Ruleset

{
  "name": "required-workflows",
  "target": "branch",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "include": ["refs/heads/main", "refs/heads/staging", "refs/heads/qa"],
      "exclude": []
    }
  },
  "rules": [
    {
      "type": "workflows",
      "parameters": {
        "workflows": [
          {
            "repository_id": "<template-repo-id>",
            "path": ".github/workflows/sf-code-analyzer.yml",
            "ref": "refs/heads/main"
          },
          {
            "repository_id": "<template-repo-id>",
            "path": ".github/workflows/sf-apex-tests.yml",
            "ref": "refs/heads/main"
          },
          {
            "repository_id": "<template-repo-id>",
            "path": ".github/workflows/sf-secret-scan.yml",
            "ref": "refs/heads/main"
          }
        ]
      }
    }
  ]
}

Replace <template-repo-id> with the numeric ID of the configuration repository, retrievable via the GitHub API or the repository's Settings page. If the consuming repositories pin to versioned template references, the ref should point to the appropriate version tag rather than refs/heads/main.

Bypass Lists

Bypass Actors

{
  "bypass_actors": [
    {
      "actor_id": "<release-please-app-id>",
      "actor_type": "Integration",
      "bypass_mode": "always"
    },
    {
      "actor_id": "<deployment-service-account-id>",
      "actor_type": "Team",
      "bypass_mode": "pull_request"
    }
  ]
}

The bypass_mode of pull_request allows the actor to bypass pull request requirements but not status check requirements. The always bypass mode should be reserved for the Release Please integration and similar automation operating on already-validated commits. All bypass actor assignments should be documented in the configuration repository's docs/ directory and reviewed periodically.

Importing and Exporting Rulesets

GitHub Rulesets can be exported from the organization settings UI as JSON and committed to the configuration repository's rulesets/ directory for version control. The GitHub CLI can apply Ruleset configuration programmatically from these exported files, enabling Ruleset configuration to be part of a new organization bootstrap script:

gh api \
  --method POST \
  -H "Accept: application/vnd.github+json" \
  /orgs/{org}/rulesets \
  --input rulesets/production-branch-protection.json