Skip to content

Using semantic-release with an SSH deploy key in GitHub Actions

We use semantic-release to release new versions of one of our JavaScript-based web applications. semantic-release can help with various release-based activities, such as figuring out the version bump based on the commit history using conventional commits, updating the changelog, pushing a new version tag, and so on.

We recently migrated our repositories to GitHub and have a ruleset enabled for the main (default) branch to protect this branch. Using a ruleset is basically the new way of protecting a branch. For a single developer or a very small team this might be overkill and slow you down. For bigger teams definitely it makes sense to ensure that certain practices are adhered to. For example, you can enforce that force pushes are getting blocked, or that a pull request is required before merging.

This is where we ran into issues where the release commit by semantic-release could not be pushed directly to main due to this rule. GitHub allows you to grant bypass permissions for your ruleset. Unfortunately, you cannot add a single user to this bypass list, and the GITHUB_TOKEN secret is associated with the (special) github-actions[bot] user.

So, how were we able to accomplish this?

There are lots of options. You could use another account, a bot user with their own personal access token (PAT), or, create a dedicated GitHub app. Another option is using a deploy key which we ended up using.

The advantage of using a deploy key is that it is tied to a specific repository whereas a user account might have access to more repositories.

To use and set up a deploy key for a repository, follow GitHub's instructions. Ensure that the deploy key has write access to the repository.

Then, add "Deploy Keys" to the bypass list and set this specific bypass permission to "Always allow".

Now, to make use of this deploy key in your release workflow, you need to check out the repository using the SSH private key.

There are two options: Add the secret to the repository, or add the secret for an environment. You should absolutely use an environment secret.

Protect the deploy key behind an environment

This could be abused if you are not careful and just create a regular actions secret.

Like me, you are probably asking yourself whether someone could abuse the secret. Basically, someone could adjust the workflow in a PR, use the secret and push something directly to main from the workflow 🤔

Definitely.

As the GitHub docs state:

Deploy keys with write access can perform the same actions as an organization member with admin access, or a collaborator on a personal repository.

Unfortunately, GitHub (unlike GitLab) does not make it as easy to protect secrets from the same screen. GitLab allows you to make secrets available only to protected branches or certain environments. GitHub has environment secrets at least.

So please, use an environment secret and protect your environment.

The semantic-release note about pushing to your repository applies here as well.

Essentially, this should only be done in trusted environments and you really need to be aware of the risks and be willing to accept them.

At least for situations where someone raises a PR from a fork it is not possible, even with a regular actions secret. This is because secrets are not passed to workflows triggered from forks.

Add the private key as an environment secret on the repository.

Then, update your workflow as follows:

Release workflow file
[...]
jobs:
  increment-version:
    runs-on: ubuntu-latest
    # protect the deploy key with an environment secret
    environment: semantic-release
    steps:
      - uses: actions/checkout@v6
        with:
          ssh-key: ${{ secrets.DEPLOY_KEY }}
          # Persist credentials so that semantic-release can use them
          persist-credentials: true
      - name: Run semantic-release
        run: npx semantic-release
[...]

And finally, you need to ensure that the semantic-release/git plugin will use the SSH protocol. Add the SSH repository URL to your semantic-release configuration in the repositoryUrl option:

.releaserc
{
  "repositoryUrl": "git@github.com/owner/repo.git"
}

Improving the semantic-release documentation

Part of this was not immediately clear from the semantic-release documentation. I raised a PR to improve the SSH key documentation. Hopefully it will get accepted.

Updates to this blog post

  • 19.02.2026: Added information about using environment secrets to protect the deploy key.