Enforcing Branch Restrictions in GitHub Using GitHub Actions

Pushpen Dawn
Globant
Published in
10 min readSep 6, 2024
GitHub Branch Protection

In this article, we will explore how to implement GitHub Actions workflows to restrict merges to certain branches in a GitHub repository. The example setups described here will enforce that merges to the “main” branch can only come from branch “QA”, and merges to the “QA” branch can only come from the “Develop” branch. Furthermore, this article will explore how to restrict merges using pattern match. Before continuing this topic, let’s briefly discuss the branching strategy.

What is a Branching Strategy?

A branching strategy is a set of rules and conventions that define how branches are created, named, and merged in a version control system (VCS) like Git. It helps manage the workflow of code changes, ensuring that the development process is organized and that code quality is maintained.

Why is a Branching Strategy Needed?

Below are the points for which we need the Branching Strategy:

  • Organized development: help keep the development process organized by clearly defining the roles of different branches (e.g., feature branches, release branches, main branches).
  • Parallel development: allows multiple developers to work on different features or bug fixes simultaneously without interfering with each other’s work.
  • Code quality: ensure that code is reviewed, tested, and meets certain standards before being merged into the main branches.
  • Continuous Integration and Deployment facilitates automated testing and deployment processes by maintaining a clean and stable codebase.
  • Collaboration: enhance collaboration among team members by providing a clear workflow for integrating changes.

So, by implementing a branching strategy, we can maintain a high level of code quality, ensure that features are developed and integrated smoothly, and manage the release process effectively.

Prerequisites

Before proceeding with the setup, ensure that you have the following:

  • Access to a GitHub repository.
  • Write permission on/for GitHub Actions workflows.
  • Understanding of YAML syntax.

Setting Up GitHub Actions Workflows

Setting up GitHub Actions workflows involves automating tasks such as building, testing, and deploying our code directly from our GitHub repository. GitHub Actions allows us to create custom workflows by defining steps in a YAML file. Each workflow can be triggered by various events, like pushing code to a repository, creating a pull request, or on a schedule.

The key components of a GitHub Actions workflow include:

  • Triggers: It will define what event should start the workflow, such as a push to a specific branch.
  • Jobs: A workflow consists of one or more jobs, each of which runs on a virtual machine.
  • Steps: Each job consists of steps that execute commands or actions.
  • Runners: The environment where the workflow is executed, which can be hosted by GitHub or self-hosted.

By setting up GitHub Actions workflows, we can automate repetitive tasks, ensure consistency across environments, and streamline your development process.

Workflow to restrict merges to the “main” branch

The first thing to restrict merges to the “main” branch is to create a workflow file. To create this file, navigate to your GitHub repository, and ensure you are on the default branch. Create a new file in the following path: .github/workflows/restrictmergemain.yml

You can give any name to the workflow file, but the path must be the same. Now, add the following YAML content to the created file:

name: Restrict.Main.Merges.Branch


on:
pull_request:
branches:
- main


jobs:
restrict-to-merge-branch-main:
runs-on: ubuntu-latest


steps:
- name: Check merge branch
if: github.event.pull_request.base.ref == 'main'
run: |
if [ "${{ github.event.pull_request.head.ref }}" != "release/QA" ]; then
echo "Merges to main are only allowed from the release/QA branch."
exit 1
fi

Commit the file with a meaningful commit message, e.g., “Add workflow to restrict merges to main”. We can use the GitHub shell command below:

git commit -m “Add workflow to restrict merges to main.”

Push the commit to the default branch using the below shell command:

git push -u origin <desired branch>.

Workflow to Restrict Merges to “Release/QA” Branch

Now we are going to navigate to our repository on GitHub and on the default branch again to test the Restrict.QA.Merges.Branch workflow. Replace the contents of the file .github/workflows/restrictmerge.yml with the following:

name: Restrict.QA.Merges.Branch


on:
pull_request:
branches:
- release/QA


jobs:
restrict-to-merge-branch-qa:
runs-on: ubuntu-latest


steps:
- name: Check merge branch
if: github.event.pull_request.base.ref == 'release/QA'
run: |
if [ "${{ github.event.pull_request.head.ref }}" != "develop" ]; then
echo "Merges to release/QA are only allowed from the develop branch."
exit 1
fi

To commit and push the changes we need to follow the same last three steps mentioned above for the “main” branch with a different commit message e.g., “Add workflow to restrict merges to QA”.

Apart from the specific branches, we can use a matching pattern of allowed branches to merge our code. Please find the below example, for merging code to the “Develop” branch from a specific allowed pattern match.

Workflow to restrict merges to the “Develop” branch

For the “Develop” branch, we will follow the same steps mentioned above to create a Restrict.dev.Merges.Branch workflow to test by creating the file .github/workflows/restrictmergedev.yml with the following:

name: Restrict.dev.Merges.Branch


on:
pull_request:
branches:
- develop


jobs:
restrict-to-merge-branch-develop:
runs-on: ubuntu-latest


steps:
- name: Check merge branch
if: github.event.pull_request.base.ref == 'develop'
run: |
ALLOWED_PATTERNS=("feature/*")
BRANCH_ALLOWED=false
for pattern in "${ALLOWED_PATTERNS[@]}"; do
if [[ "${{ github.event.pull_request.head.ref }}" == $pattern ]]; then
BRANCH_ALLOWED=true
break
fi
done
if [ "$BRANCH_ALLOWED" != true ]; then
echo "Merges to develop are only allowed from branches matching patterns: ${ALLOWED_PATTERNS[*]}."
exit 1
fi

To commit and push the changes, we need to follow the same last three steps mentioned above for the “main” branch with a different commit message, e.g., “Add workflow to restrict merges to Develop.”

Configuring Branch Protection Rules

In GitHub there is a way to enforce certain standards and workflows on your repository’s branches. These rules help maintain code quality and ensure changes are properly reviewed before merging. Let’s discuss why we need them:

  • Prevent unintended changes: By restricting who can push to certain branches, we can reduce the risk of accidental or unauthorized changes to critical parts of your code.
  • Enforce code quality: Requiring pull request reviews and status checks ensures that every change is carefully reviewed and passes automated tests before merging. This helps catch bugs and maintain high standards.
  • Ensure collaboration and accountability: Branch protection rules encourage collaboration by requiring peer reviews, fostering better communication and accountability within the team.
  • Maintain a stable codebase: By setting rules that enforce proper testing and review, you minimize the chances of introducing breaking changes or bugs into key branches like main or production.

Comply with security standards: Features like requiring signed commits help verify the authenticity of contributions, protecting your repository from malicious changes.

In this article, we will protect our branches so that no undesired merge can happen. To protect the “main” branch, follow these steps:

Open the GitHub repository & Navigate to Repository Settings, and click on the Settings tab at the top:

GitHub Repository Settings

Access Branch Protection Rules

In the left-hand sidebar, under Code and Automation, click Branches. Then, under Branch protection rules, click the Add rule button:

Add Branches Rule

Configure the Rule

For the branch name pattern, enter the desired branch name e.g., “main”. Then select Require status checks to pass before merging:

Configure Branch Protection Rule

For required status checks, add restrict-to-merge-branch-main to the list of required status checks. We can find the job names in the search list of the status check:

Add Required Status Check

Optionally, we can enforce other protections like requiring pull request reviews or restricting who can push to the branch to save the changes. Lastly, we need to save the rule:

Branch Protection Rule Saving

We should follow the same steps for the “develop” and the “QA” branches. Please ensure we select the appropriate GitHub Actions workflow jobs in the status check. The screenshot below shows the configuration for the “develop” branch:

Required Status Check “develop” Branch

And for the “QA” branch:

Required Status Check “QA” Branch

What Happens if We Try to Merge from a Different Branch?

If we try to merge from a branch that does not match the allowed branches or patterns, the GitHub Actions workflow will automatically fail, and the merge will be blocked. For example:

Merging to “main” from a non “release/QA” branch

The workflow Restrict.Main.Merges.Branch will run and check the branch name. If it doesn’t match release/QA, the workflow will fail and exit with an error message and a failure status:

Merges to main is only allowed from the release/QA branch.

As shown in the screenshot below:

Status of Workflow “Restrict.Main.Merges.Branch”

Merging to “Release/QA” from a non “develop” branch

The workflow Restrict.QA.Merges.Branch will run and check the name of the branch. If it doesn’t match “develop”, the workflow will fail and exit with an error message and a failure status:

Merges to release/QA are only allowed from the develop branch.

As shown in the screenshot below:

Status of Workflow “Restrict.QA.Merges.Branch”

Merging to “develop” from a non “feature/*” branch

The workflow Restrict.dev.Merges.Branch will run and check the name of the branch. If it doesn’t match the allowed pattern “feature/*”, the workflow will fail and exit with an error message and a failure status:

Merges to develop are only allowed from branches matching patterns: feature/*.

As shown in the screenshot below:

Status of Workflow “Restrict.dev.Merges.Branch”

Resolution

If we encounter a failure due to a merge attempt from an incorrect branch, we need to follow these steps to resolve it:

  • Identify the source branch: This will check the branch which we are attempting to merge. Ensure it matches the allowed patterns or names specified in the workflows.
  • Recreate the Pull Request: If the branch does not match the allowed name or pattern, we need to create a new branch that complies with the rules and move our changes to this new branch. Open a new pull request from the compliant branch.
  • Adjust branch patterns (if necessary): If we find that the branch patterns have to be adjusted, update the allowed patterns in the workflow YAML file. Commit and push the changes, ensuring they are merged into the default branch in your repository.
  • Re-run the workflow: Once the branch matches the allowed patterns or names, we should re-run the GitHub Actions workflow by re-opening the pull request or pushing a new commit to trigger the workflow again.

Conclusion

Following this article, we can implement a branching strategy with restrictions on where and how code can be merged. It is a critical step in maintaining the integrity and quality of our codebase. By enforcing these rules through GitHub Actions and branch protection settings, we can ensure that our development process is structured and secured.

The above-described workflows in this guide provide a robust solution for managing merges in a multi-branch environment. By restricting merges to the “main” branch from only specific branch patterns (e.g., Release/QA ) and enforcing that merges to the “Release/QA” branch can only come from the “develop” branch, We are effectively safeguarding our codes against undesired changes. This setup is useful in teams where multiple developers are working on different items of a project simultaneously, as it helps to prevent conflicts and ensures that only thoroughly tested and reviewed code reaches our production branches.

Moreover, by configuring branch protection rules in GitHub, we have added a layer of security that complements the automated checks provided by GitHub Actions Workflows. These protections ensure that critical branches like “main” and “Release/QA” are not compromised by direct pushes or unverified merges, which could introduce bugs in our code repository. We can cover the below-mentioned points as our benefits:

  • Improved Code Quality: By implementing that only code from desired branches can be merged and that is why, we can maintain a high standard of quality in our production and pre-production branches.
  • Enhanced Collaboration: Many developers can work independently on their respective feature branches while still; adhering to the overall project structure. It will make collaboration smoother and more efficient.
  • Reduced Risk of Errors: Automated checks help catch potential issues early, reducing the risk of deploying broken or unstable code to production environments.
  • Consistency Across Environments: By controlling the flow of codes through specific branches, we can ensure that different environments (like, development, QA, production) remain consistent.
  • Scalability: As the team grows, these practices scale with your development process, ensuring that even large teams can maintain a consistent and good quality of codes.

References

--

--