Avoid workflow loops on GitHub Actions when committing to a protected branch.

Avoid workflow loops on GitHub Actions when committing to a protected branch.

Continuous Deployment (CD) is part of almost every modern application development workflow. Since the software is deployed using an automated CD workflow, it only makes sense to also auto-increment at least the patch version or build number.

This can be easily achieved on GitHub Actions with a workflow that will

  1. Check out the latest code on the current branch.
  2. Increment the patch version or the build number or use any other strategy (like semantic versioning) to update the current version.
  3. Build the application and deploy it to production (or any appropriate environment).
  4. Commit changes made to the version to the same branch.
name: CD
on:
  push:
    branches:
      - main
jobs:
    deploy:
        name: Deploy to PROD
        runs-on: ubuntu-latest
        steps:
            # Checkout current branch
            - uses: actions/checkout@v2

            # Increment the patch version / build number.
            - name: Bump Version
               run: scripts/bump-build-number.sh

            # Build and deploy app to production.
            - name: Build and Deploy
                run: ...

            # After a successful deployment, commit changes made to the version.
            - name: Commit Version Change
              run: |
                    git config user.name "Github Actions CD"
                    git config user.email "<>"
                    git add --all
                    git commit -m "Bump Version to $NEW_VERSION"
                    git push origin main

Here we have a workflow that is triggered by a push on the main branch. In this workflow, we are creating a commit and pushing it to the main branch.

Since this workflow is triggered on push to the main branch and the workflow itself pushed to the main branch, we should have ended with a workflow that continuously triggers itself. Why does that not happen?

It is because GitHub Actions generates a GITHUB_TOKEN for each workflow run. This GITHUB_TOKEN is used to set up git on the workflow. Any changes made using that token do not trigger the workflow.

Committing to Protected Branches

Branches that trigger important workflows like CD usually need to be protected so that we do not end up releasing any unintended changes to the public. We can protect these branches using the Branch Protection Rules and add the Required Pull Request Reviews rule.

  • Running the above workflow on a protected branch will fail with an error saying that pushing to the protected branch failed. Screenshot 2022-02-15 at 12.31.10 PM.png

  • To fix this, we can edit the Branch Protection Rules to allow specific users to commit to the protected branch.

    Screenshot 2022-02-15 at 12.36.19 PM.png

  • Now we can create a Personal Access Token for the user allowed to bypass the rules and save that token to GitHub Secrets.

    Screenshot 2022-02-15 at 12.43.17 PM.png

  • Update the CD workflow to use this new token instead of the default GITHUB_TOKEN.

name: CD
on:
  push:
    branches:
      - main
jobs:
    deploy:
        name: Deploy to PROD
        runs-on: ubuntu-latest
        steps:
            # Checkout current branch
            - uses: actions/checkout@v2
               with:
                   token: ${{ secrets.PROTECTED_BRANCH_PUSH_TOKEN }}
            ...

Using the PROTECTED_BRANCH_PUSH_TOKEN allows us to push to a protected branch for the workflow. But since we are no longer using the GITHUB_TOKEN, any commits made to the branch the workflow is triggered on, will re-trigger this workflow. We end up in an infinite loop.

Preventing Workflow Loops

Similar to the GITHUB_TOKEN, we need a way to stop the re-triggering of the same workflow.

This is where we can leverage the ability of Github Actions to skip workflow runs if it detects specific commands in the commit message.

By adding any of the following commands to our commit message, the workflow triggered on push will not run for that commit.

  • [skip ci]
  • [ci skip]
  • [no ci]
  • [skip actions]
  • [actions skip]

We can update the workflow so that the commit message includes one of these commands and we do not end up in an infinite loop.

name: CD
on:
  push:
    branches:
      - main
jobs:
    deploy:
        name: Deploy to PROD
        runs-on: ubuntu-latest
        steps:
            # Checkout current branch
            - uses: actions/checkout@v2
                with:
                    token: ${{ secrets.PROTECTED_BRANCH_PUSH_TOKEN }}

            ...

            # After successful deployment, commit changes made to version.
            - name: Commit Version Change
               run: |
                    git config user.name "Github Actions CD"
                    git config user.email "<>"
                    git add --all
                    git commit -m "[skip ci] Bump Version to $NEW_VERSION"
                    git push origin main

In Summary

To push to a protected branch from CD