Repository navigation
Bypass protected branches with deploy keys #1343
Description
Activity
- addedfeatureA new feature or a feature requestA new feature or a feature requesttriagewaiting for initial maintainer reviewwaiting for initial maintainer review
on Oct 24, 2025 @dxvidparham, if this works, you are a hero. I've been unable to find a solution myself and as someone who cares about sustainment and security this has been frustrating to say the least.
So it seems like the feature for PSR is really just a change in the docs to provide this method as the instructions, right? As long as Git can authenticate, then PSR will work to publish a commit and tag.
- addeddocsImprovements or additions to documentationImprovements or additions to documentationconfirmedPrevent from becoming stalePrevent from becoming staleand removedfeatureA new feature or a feature requestA new feature or a feature requesttriagewaiting for initial maintainer reviewwaiting for initial maintainer review
on Oct 24, 2025 I tried the example from the repository mentioned above and got it to work, but failed in my own GitHub workflow where I use the python semantic release workflow. And according to this discussion it seems like multiple people succeeded to bypass a protected branch in their own setups by following the workflow with the deploy keys.
My suspicion is that the GITHUB_TOKEN overwrites the deploy key. Could that be why I`m failing?
This is my workflow:
name: Automated Releases on: push: branches: - main # Default permissions (least privilege) permissions: contents: read jobs: release: runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-release-${{ github.ref_name }} cancel-in-progress: false permissions: contents: write id-token: write outputs: released: ${{ steps.release.outputs.released }} version: ${{ steps.release.outputs.version }} tag: ${{ steps.release.outputs.tag }} steps: - name: Checkout Repository uses: actions/checkout@v4 with: ssh-key: ${{ secrets.DEPLOY_KEY }} ref: ${{ github.ref_name }} fetch-depth: 0 - name: Configure git run: | git config --local user.email "[email protected]" git config --local user.name "GitHub Actions" - name: Force branch to workflow SHA run: git reset --hard ${{ github.sha }} - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Setup Poetry uses: snok/install-poetry@v1 with: version: latest virtualenvs-in-project: true - name: Python Semantic Release id: release uses: python-semantic-release/[email protected] with: github_token: ${{ secrets.GITHUB_TOKEN }} git_committer_name: "github-actions" git_committer_email: "[email protected]" # - name: Build package if: steps.release.outputs.released == 'true' run: | echo "Building package with updated version: ${{ steps.release.outputs.version }}" # Install dependencies poetry install --extras GPU-libs --with dev # Build package poetry build --format wheel - name: Verify wheel name match release version if: steps.release.outputs.released == 'true' run: | echo "=== WHEEL VERIFICATION ===" echo "Release version: ${{ steps.release.outputs.version }}" echo "Release tag: ${{ steps.release.outputs.tag }}" echo "" echo "Built wheels:" ls -la dist/*.whl echo "" echo "Checking if wheel names contain correct version..." - name: Upload to GitHub Release Assets if: steps.release.outputs.released == 'true' uses: python-semantic-release/[email protected] with: github_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ steps.release.outputs.tag }} - name: Comment on PR (if applicable) if: steps.release.outputs.released == 'true' && github.event_name == 'pull_request' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `🚀 Release created: ${{ steps.release.outputs.version }}\n\nTag: ${{ steps.release.outputs.tag }}\n\nYou can test this release version before merging.\n\nWheel files have been built with the correct release version.` })
Reacted by Jin Yu Zhang and NewComer00I think you need to set the
semantic_release.remote.ignore_token_for_pushtotruebecause of how the push command is generated (why idk and I didn't touch it if it was working). If you set this totrue, then it will use a genericgit pushwhich will rely on the repo git configuration for auth configuration.Thanks, that was definitely helpful. But where I'm running into an issue now is this:
Environment Variable from which to source the authentication token for the remote VCS. Common examples include "GH_TOKEN", "GITLAB_TOKEN" or "GITEA_TOKEN", however, you may choose to use a custom environment variable if you wish.
By default, this is a mandatory environment variable that must be set before using any functionality that requires authentication with your remote VCS. If you are using this token to enable push access to the repository, it must also be set before attempting to push.
If your push access is enabled via SSH keys instead, then you do not need to set this environment variable in order to push the version increment, changelog and modified source code assets to the remote using semantic-release version. However, you will need to disable release creation using the --vcs-release/--no-vcs-release option, among other options, in order to use Python Semantic Release without configuring the environment variable for your remote VCS authentication token. Link
If I understand it correctly, then the priority order of the authentication process is:
- If .git/config has HTTPS credentials → use those (set by actions/checkout)
- If a token environment variable exists → force HTTPS with that token
- Only if neither exists → try SSH
If we could flip that then I think it would be possible to use a deploy key, but as it stands now the deploy key will be ignored because the GITHUB_TOKEN takes precedence.
Are you actually hitting an error or are you anticipating one based on my previously worded (deploy-keys unaware) guide?
Regardless of what the documentation says, I am telling you that the code (in default mode) performs 3 remote connections.
- git push release commit to remote
- git push release tag to remote
- Remote VCS (ex. GitHub.com) API connection to make a Release (tied to the tag) that contains the release notes.
In remote connections 1 & 2, its a git command
git push $ORIGIN_URL [branch | tag]where$ORIGIN_URLis the full remote url to includes the authentication token unlessremote.ignore_token_for_pushis set totrue. Because I told you to setremote.ignore_token_for_push = true, your$ORIGIN_URLdoes not include any authentication so it will rely on.git/configand the associated SSH configuration which you set to be the deploy key.For remote connection 3, the only option is an API token for authentication. You should pass the
GITHUB_TOKENvalue to enable the ability of PSR to make a VCS release via the API. Branch protections do not hinder this action. If you don't pass a token, then this remote connection will fail unless you disable this action with the--no-vcs-releasecli switch.Please try this out and let me know if you receive an error or if it works.
Yes, so the error message I receive is the following:
GitCommandError: Cmd('git') failed due to: exit code(128) cmdline: git push [email protected]:LEGO/cbm-core.git main stderr: 'Warning: Identity file /home/runner/work/_temp/2bb151c1-8524-416e -a179-9045cb4caa7f not accessible: No such file or directory. No ED25519 host key is known for github.com and you have requested strict checking. Host key verification failed. fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists.'This is due to the fact that the SSH keys are out of scope, and don't have access to the Docker container python semantic releases is using. Which I was able to solve by using the webfactory ssh agent instead:
name: Automated Releases on: push: branches: - main # Default permissions (least privilege) permissions: contents: read jobs: release: runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-release-${{ github.ref_name }} cancel-in-progress: false permissions: contents: write id-token: write outputs: released: ${{ steps.release.outputs.released }} version: ${{ steps.release.outputs.version }} tag: ${{ steps.release.outputs.tag }} steps: - name: Checkout Repository uses: actions/checkout@v4 with: ref: ${{ github.ref_name }} fetch-depth: 0 - name: Setup SSH Agent with Deploy Key uses: webfactory/[email protected] with: ssh-private-key: ${{ secrets.DEPLOY_KEY }} - name: Configure git run: | git config --local user.email "[email protected]" git config --local user.name "GitHub Actions"
But now I'm receiving this error message:
GitCommandError: Cmd('git') failed due to: exit code(1) cmdline: git push https://github.com/LEGO/cbm-core main stderr: 'remote: error: GH013: Repository rule violations found for refs/heads/main. remote: Review all repository rules at https://github.com/LEGO/cbm-core/rules?ref =refs%2Fheads%2Fmain remote: remote: - Changes must be made through the merge queue remote: remote: - Changes must be made through a pull request. remote: To https://github.com/LEGO/cbm-core ! [remote rejected] main -> main (push declined due to repository rule violations) error: failed to push some refs to 'https://github.com/LEGO/cbm-core'' GitPushError: Failed to push branch (main) to remoteWhich suggests that the deploy key is not used within the Docker container, hence we can't bypass the branch protection rules. Unless there are any flaws in my understanding.
Thanks for the error message details. The second error you received I think is because you removed the
ssh-key: ${{ secrets.DEPLOY_KEY }}from the checkout action. When checkout action occurs it uses the GITHUB_TOKEN by default in the git clone process. This leaves you with agit remote -vdefinition that includes the GITHUB_TOKEN over HTTPS and not SSH. So even if your ssh-agent is ready and properly passed to the container (I have no idea if this is accomplished by default), PSR's action ofgit pushdid not use SSH. When GitHub runs a Container action, it passes your repo to the container via a file mount of your repository folder only (hence the first error, no SSH key).You have 2 options:
-
try adding the ssh-key definition back to checkout, and looking into if the SSH_AGENT_SOCK is passed to the PSR container. I have no idea if this will just work and I'm out of ideas beyond this. I wouldn't put too much into making the action work as the action will change to a composite action in the future.
-
Scrap the PSR action, and instead just use a traditional shell action. I can pretty much guarantee this will work without all the silly complexity (and slowness) of containers.
jobs: release: runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-release-${{ github.ref_name }} cancel-in-progress: false permissions: contents: write id-token: write outputs: released: ${{ steps.release.outputs.released }} version: ${{ steps.release.outputs.version }} tag: ${{ steps.release.outputs.tag }} steps: - name: Checkout Repository uses: actions/checkout@v4 with: ssh-key: ${{ secrets.DEPLOY_KEY }} ref: ${{ github.ref_name }} fetch-depth: 0 - name: Configure git # This will be how the release is committed, your current user/emails don't match run: | git config --local user.email "[email protected]" git config --local user.name "GitHub Actions" - name: Force branch to workflow SHA run: git reset --hard ${{ github.sha }} - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Setup Poetry # Add python-semantic-release ~= 10.4 to dependencies uses: snok/install-poetry@v1 with: version: latest virtualenvs-in-project: true - name: Python Semantic Release id: release - uses: python-semantic-release/[email protected] - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - git_committer_name: "github-actions" - git_committer_email: "[email protected]" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + poetry run semantic-release versionYou may have a few changes depending on Poetry and virtual environments and I am no expert on poetry usage so I leave that to you. Either way because you have the repository checked out via SSH, and the ssh key available then refer to my previous message of how PSR will work with its remote connections.
Let me know what you choose and what happens now.
-
Thanks for the assist. Sadly, I wasn't able to get it working, even with your suggested changes. Now, I receive this error message:
No build command specified, skipping [13:56:55] ERROR 401 Client Error: Unauthorized for url: version.py:791 https://api.github.com/repos/.../.../re leases NoneType: None 401 Client Error: Unauthorized for url: https://api.github.com/repos/.../.../releases Failed to create release on Github!I think at this point it's best to discard the approach and fallback to the proven method with using a PAT.
That's very disappointing. I will have to look into this further as now it should just be only an access token issue for the API. I assume that you did see a successful tag populate into the repository?
Yes, the tag has been successfully populated in the repository. It feels like there’s just one small piece missing to make it fully work. But I'm struggling to see what it is.
Ok great. Please just double check that you have granted the token permissions of
contents: writeand that PSR's log did not issue a warning about a missing or empty token. You should also trigger very verbose mode of PSR with the-vvprior to the version subcommand to make sure you have enough logging of your run into errors in the future. Protected branches should not have any effect on the releases API which is what PSR is using under the hood.Reacted by David Anthony ParhamSorry for my late response. I'll be able to test tomorrow and give further feedback.
Thanks for the pointers! You were absolutely right about the token permissions and the API access.
It turns out the root cause of the 401 error was a mismatch in environment variables. PSR defaults to looking for `GH_TOKEN` for the `github` remote type, but my workflow provides `GITHUB_TOKEN`. Because of this, PSR couldn't authenticate with the API to create the release, even though the git push (via SSH) was configured correctly with `ignore_token_for_push = true`.
So basically what I had to do was this:
[tool.semantic_release.remote] name = "origin" type = "github" token = { env = "GITHUB_TOKEN" } # CRITICAL: Add this line ignore_token_for_push = true insecure = falseI’ll stress-test it over the next few days, and if it proves to work reliably, I can provide more information if needed and help extend the documentation.
Reacted by David Anthony Parham and codejedi365Everything works flawlessly. I created this repository to stress-test the setup once more, add detailed instructions, and make it easy to provide a fully reproducible example. I hope this helps!
https://github.com/dxvidparham/psr-deploy-keys-for-protected-branches
Reacted by codejedi365 and NewComer00I'm trying to replicate this in my environment (we're using GHES in my environment, so
domainis set).In my workflow the
verify_upstream_unchangedfunction fails.
My assumption is the git fetch command doesn't seem to be using the ssh-key nor the givengithub_token, becauseignore_token_for_pushis set totrue.cmdline: git fetch -v -- origin
stderr: 'fatal: Could not read from remote repository.'Logs
[...] The next version is: 0.24.0! 🚀 INFO No contents found in changelog_writer.py:210 PosixPath('/github/workspace/templat es'), using default changelog template INFO found 'project.version' in source file contents, toml.py:69 replacing with 0.24.0 Skipping build due to --skip-build flag INFO requested to use token for push but no token github.py:480 set, ignoring... INFO Upstream branch name: origin/main gitproject.py:384 INFO Fetching latest changes from remote gitproject.py:415 'origin' ERROR Cmd('git') failed due to: exit code(128) gitproject.py:458 cmdline: git fetch -v -- origin stderr: 'fatal: Could not read from remote repository.' ╭── Traceback (most recent call last) ───╮ │ /opt/psr/.venv/lib/python3.14/site-pac │ │ kages/semantic_release/gitproject.py:4 │ │ 56 in verify_upstream_unchanged │ │ │ │ 453 │ │ │ │ else: │ │ 454 │ │ │ │ │ # Use the de │ │ 455 │ │ │ │ │ # file:// UR │ │ ❱ 456 │ │ │ │ │ remote_ref_o │ │ 457 │ │ │ except GitCommandErr │ │ 458 │ │ │ │ self.logger.exce │ │ 459 │ │ │ │ err_msg = f"Fail │ │ │ │ /opt/psr/.venv/lib/python3.14/site-pac │ │ kages/git/remote.py:1076 in fetch │ │ │ │ 1073 │ │ proc = self.repo.git.fe │ │ 1074 │ │ │ "--", self, *args, │ │ universal_newlines=True, v=verb │ │ 1075 │ │ ) │ │ ❱ 1076 │ │ res = self._get_fetch_i │ │ kill_after_timeout=kill_after_t │ │ 1077 │ │ if hasattr(self.repo.od │ │ 1078 │ │ │ self.repo.odb.updat │ │ 1079 │ │ return res │ │ │ │ /opt/psr/.venv/lib/python3.14/site-pac │ │ kages/git/remote.py:902 in │ │ _get_fetch_info_from_stderr │ │ │ │ 899 │ │ ) │ │ 900 │ │ │ │ 901 │ │ stderr_text = progress. │ │ ❱ 902 │ │ proc.wait(stderr=stderr │ │ 903 │ │ if stderr_text: │ │ 904 │ │ │ _logger.warning("Er │ │ 905 │ │ │ │ /opt/psr/.venv/lib/python3.14/site-pac │ │ kages/git/cmd.py:419 in wait │ │ │ │ 416 │ │ if status != 0: │ │ 417 │ │ │ errstr = read_all_f │ │ 418 │ │ │ _logger.debug("Auto │ │ ❱ 419 │ │ │ raise GitCommandErr │ │ 420 │ │ return status │ │ 421 │ │ 422 │ ╰────────────────────────────────────────╯ GitCommandError: Cmd('git') failed due to: exit code(128) cmdline: git fetch -v -- origin stderr: 'fatal: Could not read from remote repository.' Failed to fetch from remote 'origin' Unable to verify upstream due to error!jobs: release: runs-on: ubuntu-latest concurrency: release permissions: id-token: write contents: write outputs: released: ${{ steps.release.outputs.released }} version: ${{ steps.release.outputs.version }} tag: ${{ steps.release.outputs.tag }} steps: - uses: actions/checkout@v3 with: ssh-key: ${{ secrets.DEPLOY_KEY }} fetch-depth: 0 - name: Configure Git Credentials run: | git config user.name 'github-actions[bot]' git config user.email 'github-actions[bot]@users.noreply.github.com' - name: Force branch to workflow SHA run: git reset --hard ${{ github.sha }} - name: Python Semantic Release id: python-semantic-release uses: python-semantic-release/python-semantic-release@master with: github_token: ${{ secrets.GITHUB_TOKEN }} build: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Any ideas?
@ragchuck, yeah, you can't use the GitHub action for this activity. You need to run it via the command line. This is because the GitHub action is a Docker Action and the SSH authentication mechanism is lost during the context change. See my more detailed explanation above.
It has been 90 days since the last update on this confirmed issue. @python-semantic-release/team can you provide an update on the status of this issue?
- addedneeds-updateNeeds status update from maintainersNeeds status update from maintainers
on Apr 27, 2026
Feature Request
Description
The current suggested method to bypass protected branches is the following:
Not everyone would like to use PATs (see reasoning below). After researching this topic intensely lately I stumbled across the following solution: https://github.com/sbellone/release-workflow-example. This repository showcases how one can bypass the branch protection on GitHub via deployment keys.
Use cases
Why PATs are problematic for automation:
Why deploy keys are superior:
Possible implementation
This workflow would allow us to bypass the protected branch as follows:
Alternative solutions
Continue using PATs