| |

GitHub Actions CI/CD (7/10): Branch Protection — Make CI a Merge Gate

Summary: Enable GitHub branch protection rules so that pull requests cannot be merged unless CI passes. Then learn to store sensitive values like database credentials as GitHub secrets and use them in your workflows.

KeyValue
Repositoryhelloci
Protected branchmain
Required checkslint, unit-tests, integration-tests
Secret name exampleDATABASE_URL

0. Prerequisites

  • The helloci project with the three-job CI workflow from Part 6
  • Admin access to your GitHub repository
  • At least one successful workflow run (so GitHub knows which checks exist)

1. What Are Branch Protection Rules

Branch protection rules tell GitHub: “do not allow changes to this branch unless certain conditions are met.”

Without protection, anyone with push access can commit directly to main — even if CI is failing. Protection rules enforce the workflow you have been building:

  • Code must come through a pull request
  • CI checks must pass before merging
  • Optionally, another person must review the code

This turns CI from “informational” to “enforced.”


2. Enable Branch Protection on main

Open your repository on GitHub and navigate to Settings > Branches.

Click Add branch protection rule (or Add rule depending on your GitHub plan).

Set the Branch name pattern to main.

Enable the following settings:

  • Require a pull request before merging — prevents direct pushes to main
  • Require status checks to pass before merging — this is the key setting
  • Require branches to be up to date before merging — ensures the PR is tested against the latest main

Note: Free GitHub accounts support branch protection on public repositories. Private repositories require a paid plan (GitHub Pro or Team) for branch protection.


3. Require Status Checks

After enabling “Require status checks to pass before merging,” you see a search box labeled Status checks that are required.

Search for and add your three job names:

  • lint
  • unit-tests
  • integration-tests

Note: For matrix jobs like unit-tests, GitHub lists each matrix combination separately (e.g., unit-tests (3.10), unit-tests (3.11), unit-tests (3.12)). Add all of them if you want every Python version to be required.

These names come from the jobs: keys in your workflow file. If you rename a job in your workflow, you must update the branch protection rule to match.

Click Create (or Save changes) to activate the rule.


4. Test the Merge Gate

Create a branch with a deliberate test failure.

git checkout -b test-protectionCode language: Shell Session (shell)

Edit tests/test_greet.py and change one assertion to be wrong.

def test_greet_returns_greeting():
    assert greet("Alice") == "Hi, Alice!"Code language: Python (python)

Commit and push.

git add tests/test_greet.py
git commit -m "Deliberate test failure to verify branch protection"
git push -u origin test-protectionCode language: Shell Session (shell)

Open a pull request from test-protection into main. CI runs and the unit-tests job fails. The pull request page shows:

❌ unit-tests (3.12) — failing 🟡 Merging is blocked

The Merge button is grayed out. You cannot merge until the required checks pass.

Close this PR without merging and switch back to main.

git checkout main
git branch -D test-protectionCode language: Shell Session (shell)

Tip: This is exactly what you want. A broken test now blocks the merge — not a human remembering to check.


5. Store Secrets in GitHub

Your integration test workflow currently has the database password in plain text inside the workflow file.

DATABASE_URL: postgresql://helloci_user:helloci_pass@localhost:5432/helloci_dbCode language: YAML (yaml)

For a service container running in CI, this is acceptable — the database is ephemeral and the credentials are not real. But in production workflows, credentials must be stored as secrets.

Navigate to Settings > Secrets and variables > Actions in your repository.

Click New repository secret.

FieldValue
NameDATABASE_URL
Secretpostgresql://helloci_user:helloci_pass@localhost:5432/helloci_db

Click Add secret.

Warning: Never commit real credentials to your repository. GitHub secrets are encrypted at rest, masked in logs, and only available to workflows running in your repository.


6. Use Secrets in Your Workflow

Update the integration test step in .github/workflows/ci.yml to read from the secret.

      - name: Run integration tests
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: pytest -v -m integrationCode language: YAML (yaml)

The ${{ secrets.DATABASE_URL }} syntax tells GitHub Actions to inject the secret value as an environment variable. The value is never printed in logs — GitHub automatically masks it.


7. Understand Environment Variables in Workflows

GitHub Actions provides several ways to pass data to your steps.

MethodScopeUse case
env: at step levelOne stepPass a value to a single command
env: at job levelAll steps in a jobShare a value across steps
env: at workflow levelAll jobsShare a value across the entire workflow
${{ secrets.NAME }}Wherever referencedInject encrypted secrets
${{ vars.NAME }}Wherever referencedInject non-sensitive repository variables

Step-level example:

- name: Run integration tests
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
  run: pytest -v -m integrationCode language: YAML (yaml)

Job-level example:

jobs:
  integration-tests:
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
    steps:
      - name: Run integration tests
        run: pytest -v -m integrationCode language: YAML (yaml)

Both achieve the same result. Use step-level env when only one step needs the value. Use job-level env when multiple steps share it.

Note: Repository variables (vars.NAME) are for non-sensitive configuration like feature flags or version numbers. Use secrets for anything sensitive — passwords, tokens, API keys.


8. Push and Verify

Commit and push the workflow change.

git add .github/workflows/ci.yml
git commit -m "Use GitHub secret for DATABASE_URL"
git pushCode language: Shell Session (shell)

Open the Actions tab and verify that the integration test still passes. The DATABASE_URL value is now pulled from the repository secret instead of being hardcoded in the workflow file.


Summary

You protected the main branch so that CI must pass before any pull request can be merged. You also learned to store sensitive values as GitHub secrets and reference them in workflows.

  • Branch protection enforces “tests must pass to merge”
  • Required status checks tie specific workflow jobs to the merge gate
  • GitHub secrets store credentials encrypted and masked in logs
  • ${{ secrets.NAME }} injects secrets as environment variables
  • ${{ vars.NAME }} is for non-sensitive repository variables

Your CI pipeline is now an enforced quality gate — not just a notification. In Part 8 you will add artifacts so that CI runs produce downloadable reports and logs.

Similar Posts

Leave a Reply