Code coverage is often treated as a reporting metric, but it can also become part of a repository's merge policy.

For example, a team may decide that a pull request should not be merged when its test coverage falls below a defined threshold. The challenge is that coverage is usually generated by the CI pipeline, while GitHub repository rules determine whether a pull request can be merged.

GitHub's REST API can be used to work with repository rulesets programmatically, including rules that enforce code coverage requirements. This is useful when organizations need to create, update, or manage similar policies across multiple repositories.

This article explains how to approach GitHub code coverage rulesets through the REST API, how the configuration fits into a GitHub Actions workflow, and what to check before applying the rule to production repositories.

What Is a GitHub Ruleset?

A ruleset defines conditions that must be satisfied before certain repository actions are allowed.

Rulesets can be used to control areas such as:

Instead of configuring every repository independently through the GitHub user interface, an organization can manage rulesets through APIs.

A simplified flow looks like this:

Developer opens Pull Request
            |
            v
     GitHub Actions runs
            |
            v
      Tests + Coverage
            |
            v
      Coverage Result
            |
            v
    GitHub Ruleset Check
            |
      +-----+-----+
      |           |
    Pass         Fail
      |           |
      v           v
   Merge       Block merge

The important point is that a ruleset does not generate coverage by itself. Your CI workflow still needs to build the application, run tests, and produce the required coverage information.

Why Manage Coverage Rules Through the REST API?

For a single repository, configuring a ruleset manually may be enough.

For an organization with many repositories, manual configuration quickly becomes difficult to maintain.

Consider an organization with:

Repository A
Repository B
Repository C
Repository D
Repository E
...
Repository 100

If every repository should follow the same coverage policy, manually creating and maintaining 100 rulesets introduces unnecessary administrative work.

An API-based approach allows you to automate the process.

For example:

Configuration
     |
     v
Automation Script
     |
     +---- Repository A
     +---- Repository B
     +---- Repository C
     +---- Repository D
     |
     v
Consistent Coverage Policy

This is particularly useful for platform engineering and GitHub administration teams.

How Code Coverage Fits Into GitHub Actions

Before creating a coverage rule, you need a workflow that produces coverage information.

For a .NET application, a basic workflow could look like this:

name: Test

on:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.x'

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --no-restore --configuration Release

      - name: Test
        run: >
          dotnet test
          --no-build
          --configuration Release
          --collect:"XPlat Code Coverage"

The important part is the test command.

The workflow needs to generate a coverage result that GitHub or the associated coverage tooling can consume.

The exact command depends on the programming language, test framework, and coverage tooling used by the project.

Coverage and Merge Protection Are Different

A common misunderstanding is that enabling a coverage-related ruleset automatically calculates test coverage.

It does not.

Think of the system as two separate components:

Component

Responsibility

Test framework

Executes tests

Coverage tool

Calculates coverage

GitHub Actions

Runs the CI process

Coverage reporting

Publishes or exposes the result

Ruleset

Applies the repository policy

This separation is important when troubleshooting.

If the coverage value is missing, changing the ruleset may not solve the problem.

You first need to verify that the CI pipeline is correctly producing the coverage result.

Working With Rulesets Through the REST API

GitHub provides REST API endpoints for repository rulesets.

The API can be used to:

A request generally identifies the repository and the ruleset being managed.

For example, a request to list repository rulesets can be made with:

GET /repos/OWNER/REPOSITORY/rulesets

A request to retrieve a specific ruleset uses its ruleset ID:

GET /repos/OWNER/REPOSITORY/rulesets/RULESET_ID

The exact endpoint and request structure should always be checked against the current GitHub REST API documentation because GitHub continues to expand its ruleset capabilities.

Authentication

API calls need appropriate authentication.

For automation, avoid putting a personal access token directly into source code.

For example, this is a bad pattern:

var token = "github_pat_example";

Instead, store credentials securely and provide them to the automation process through an environment variable or GitHub Actions secret.

For example:

export GITHUB_TOKEN="your-token"

Then your application can read it from the environment:

var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");

if (string.IsNullOrWhiteSpace(token))
{
    throw new InvalidOperationException(
        "GITHUB_TOKEN is not configured.");
}

This keeps credentials outside the source code.

Creating a Ruleset Request

When creating a ruleset, the request contains information describing the ruleset.

A simplified API request might look like this:

{
  "name": "Code Coverage",
  "target": "branch",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "include": [
        "refs/heads/main"
      ]
    }
  },
  "rules": []
}

The exact rules included in the request depend on the ruleset functionality you are configuring.

The important idea is that a ruleset is represented as structured configuration rather than a collection of UI selections.

That makes it possible to store your policy definition alongside infrastructure or automation code.

Example: Managing Rulesets With C#

If you are building an internal administration tool, you can use HttpClient to call the GitHub REST API.

A simplified example:

using System.Net.Http.Headers;
using System.Text.Json;

var owner = "my-organization";
var repository = "sample-api";
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");

if (string.IsNullOrWhiteSpace(token))
{
    throw new InvalidOperationException(
        "GITHUB_TOKEN is not configured.");
}

using var client = new HttpClient();

client.DefaultRequestHeaders.UserAgent.ParseAdd(
    "CoverageRulesetManager");

client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);

client.DefaultRequestHeaders.Accept.ParseAdd(
    "application/vnd.github+json");

var endpoint =
    $"https://api.github.com/repos/{owner}/{repository}/rulesets";

using var response = await client.GetAsync(endpoint);

response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync();

Console.WriteLine(json);

This example retrieves the rulesets for a repository.

There are several details worth noticing.

User-Agent Header

GitHub expects API clients to identify themselves.

The example uses:

client.DefaultRequestHeaders.UserAgent.ParseAdd(
    "CoverageRulesetManager");

A meaningful application name makes API requests easier to identify.

Bearer Authentication

The token is passed using:

client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);

The token itself remains outside the source code.

GitHub API Media Type

The request specifies:

client.DefaultRequestHeaders.Accept.ParseAdd(
    "application/vnd.github+json");

This is the recommended media type for current GitHub API requests.

Reading Existing Rulesets Before Creating New Ones

One of the most important production practices is to inspect existing configuration before creating another ruleset.

Otherwise, an automation script can accidentally create duplicate or conflicting policies.

For example:

var endpoint =
    $"https://api.github.com/repos/{owner}/{repository}/rulesets";

using var response = await client.GetAsync(endpoint);

response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync();

using var document = JsonDocument.Parse(json);

foreach (var ruleset in document.RootElement.EnumerateArray())
{
    var name = ruleset.GetProperty("name").GetString();

    Console.WriteLine(name);
}

You can then search for an existing coverage ruleset before deciding whether to create or update one.

A safer automation process is:

List rulesets
      |
      v
Find coverage policy
      |
 +----+----+
 |         |
Found    Not Found
 |         |
 v         v
Update    Create

This makes the operation much more predictable.

Designing a Coverage Policy

A coverage policy should be based on how the repository actually works.

For example, a team might require:

Main branch
    |
    +-- Pull request
          |
          +-- Tests must pass
          |
          +-- Coverage requirement must pass
          |
          +-- Required review
          |
          v
        Merge

Avoid choosing an arbitrary coverage threshold simply because a higher number looks better.

Coverage is useful, but a high percentage does not automatically mean that the tests provide strong protection.

A small amount of well-designed testing can sometimes be more valuable than a large number of low-value tests.

What Should the Coverage Rule Protect?

The policy should answer a clear question.

For example:

Should a pull request be prevented from merging if its coverage requirement is not satisfied?

That is different from asking:

What percentage of the repository is covered?

A good policy should also consider:

These decisions should be made before automating the ruleset.

Testing the Policy Before Enforcement

Do not immediately activate a coverage rule across every repository.

Start with a test repository or a limited set of projects.

For example:

Coverage Policy
      |
      +-- Repository A
      |
      +-- Repository B

Run pull requests with:

  1. Coverage above the threshold.

  2. Coverage below the threshold.

  3. No coverage result.

  4. Failed tests.

  5. Coverage generated for the wrong project.

Observe how each case behaves.

This helps distinguish between a problem in the coverage pipeline and a problem in the ruleset configuration.

Handling Missing Coverage

One common failure scenario is:

Tests passed
Coverage report missing

This should not automatically be treated as equivalent to:

Coverage = 0%

Those are different situations.

A missing report may indicate:

When troubleshooting, inspect the GitHub Actions logs before changing the ruleset.

Managing Rulesets Across Multiple Repositories

For organizations with many repositories, you can create a central configuration.

For example:

{
  "name": "Code Coverage",
  "target": "branch",
  "enforcement": "active"
}

Your automation can then iterate through repositories:

Repository list
      |
      v
For each repository
      |
      +-- Read existing rulesets
      |
      +-- Find coverage policy
      |
      +-- Create or update
      |
      v
Next repository

This approach can help maintain consistency.

However, do not assume every repository should receive exactly the same policy.

A library, web application, infrastructure repository, and documentation repository may have very different testing models.

Best Practices

Keep the Policy Version Controlled

If rulesets are created from code, store the intended configuration in version control.

For example:

.github/
    policies/
        coverage-ruleset.json

This gives the team a history of policy changes.

Make Automation Idempotent

Running the script twice should not create two identical rulesets.

Use this pattern:

Check existing configuration
        |
        v
Create if missing
Update if present

Use Least-Privilege Authentication

The automation identity should have only the permissions needed to manage the intended repositories and rulesets.

Avoid using a highly privileged personal account when a more restricted automation identity can perform the task.

Test Before Enterprise-Wide Rollout

Start with a small number of repositories.

Once the behavior is understood, expand the rollout.

Monitor API Limits and Errors

Large organizations may manage hundreds or thousands of repositories.

Your automation should handle:

Do not assume every API request will succeed.

Common Mistakes

Creating Duplicate Rulesets

Always check existing rulesets before creating a new one.

Putting Tokens in Source Code

Never commit API credentials into the repository.

Treating Coverage as the Only Quality Signal

Coverage does not measure every aspect of software quality.

Combine it with meaningful tests, code review, static analysis, and other repository controls.

Applying One Policy Everywhere

A centralized policy is useful, but different repositories may need different thresholds or coverage strategies.

Ignoring Existing Technical Debt

If an existing repository has low coverage, suddenly enforcing a strict requirement can prevent normal development.

Consider how the team will transition existing code.

Advantages and Disadvantages

Advantages

Disadvantages

Automates ruleset management

Requires API automation knowledge

Useful across many repositories

API permissions must be configured correctly

Supports repeatable configuration

Policies can become complicated

Reduces manual administration

Coverage itself does not guarantee test quality

Can be integrated into platform tooling

Existing repositories may need migration work

Makes policy changes easier to audit

API failures must be handled properly

Troubleshooting Checklist

When a coverage rule does not behave as expected, check the problem in this order:

[ ] Is the GitHub Actions workflow running?
[ ] Are the tests passing?
[ ] Is coverage actually being generated?
[ ] Is the coverage result available where expected?
[ ] Is the correct repository ruleset being applied?
[ ] Is the ruleset targeting the correct branch?
[ ] Is the ruleset active?
[ ] Does the automation identity have the required permissions?
[ ] Are there duplicate or conflicting rulesets?
[ ] Is the API request returning an error?
[ ] Are API rate limits affecting automation?

This sequence helps prevent spending time changing the ruleset when the real issue is in the CI workflow.

Summary

GitHub rulesets provide a way to turn repository policies into enforceable controls, while the REST API makes those policies easier to manage programmatically.

For code coverage, the important distinction is that GitHub Actions generates the coverage result, while the repository policy determines how that result participates in the development workflow.

For a small project, manually managing a ruleset may be sufficient. For organizations with many repositories, API-driven management can make policy deployment more consistent and repeatable.

The safest implementation starts by understanding the existing test and coverage workflow, defining a realistic coverage policy, checking existing rulesets, testing the configuration with a limited scope, and only then expanding the automation.

Coverage should be treated as one part of a broader engineering quality strategy rather than as a standalone measure of software quality.