Repository: relekang/python-semantic-release
Pull request #419 (2022-02-22 17:26:47)

Allow using ssh-key to push version while using token to publish to hvcs
# Scenario

In GH Actions I want to use a ssh Deploy Key to push changes to a _protected_ branch because not every user's GITHUB_TOKEN is allowed to push to _protected_ branches but Deploy Keys are.

# Problematic

If I specify a `GH_TOKEN` such as `GITHUB_TOKEN` in a GitHub Action with enough permission to publish a GitHub Release, python-semantic-releases will also use that user's token to push version and tag changes to the repository.
The user running the action though might not have enough permission to push to a _protected_ branch even if grating some degree of permission to its `GITHUB_TOKEN`. Only Deploy Keys or Admin user's `GITHUB_TOKEN`(or Personal Access Tokens) can.

# Solution

Add an option that will allow to use the token to submit GitHub Releases while will avoid using that token for pushing changes to the repository, relying on the underlying git authentication mechanism. In the case of GitHub Actions it can be combined with [ssh-agent action](https://github.com/webfactory/ssh-agent) to allow using Deploy Keys for this purpose.

sp-ricard-valverde: Anyone ?
danth: Looks like some tests need to be fixed.
sp-ricard-valverde: I'll look into it, thanks @danth 
sp-ricard-valverde: Sorry for the late fix @danth. I've figured out the problem which had to do with the GITHUB_ACTOR env var set by the GH runners altering the test outcome(thus why locally the tests were ok)
danth: One of the tests is still broken 🤔 Perhaps there is still some interference from the test environment.
sp-ricard-valverde: Hope this one works. I explicitly added the GITHUB_ACTOR env var to my test execution to emulate the runner behaviour

Patch:
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 7789053..314d169 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -538,3 +538,14 @@ Variable          Contents
 ================  ========
 
 Default: ``v{version}``
+
+.. _config-ignore_token_for_push:
+
+``ignore_token_for_push``
+-------------------------
+Do not use the default auth token to push changes to the repository. Use the system
+configured method.
+This is useful if the auth token does not have permission to push, but the system method
+(an ssh deploy key for instance) does.
+
+Default: `false`
diff --git a/semantic_release/defaults.cfg b/semantic_release/defaults.cfg
index 05b1533..642c977 100644
--- a/semantic_release/defaults.cfg
+++ b/semantic_release/defaults.cfg
@@ -23,6 +23,7 @@ gitlab_token_var=GL_TOKEN
 gitea_token_var=GITEA_TOKEN
 
 hvcs=github
+ignore_token_for_push=false
 major_emoji=:boom:
 major_on_zero=true
 minor_emoji=:sparkles:,:children_crossing:,:lipstick:,:iphone:,:egg:,:chart_with_upwards_trend:
diff --git a/semantic_release/settings.py b/semantic_release/settings.py
index e58b446..7f57114 100644
--- a/semantic_release/settings.py
+++ b/semantic_release/settings.py
@@ -41,6 +41,7 @@ def _config_from_ini(paths):
         "changelog_scope",
         "check_build_status",
         "commit_version_number",
+        "ignore_token_for_push",
         "patch_without_tag",
         "major_on_zero",
         "remove_dist",
diff --git a/semantic_release/vcs_helpers.py b/semantic_release/vcs_helpers.py
index 9a6a724..cdfb1ae 100644
--- a/semantic_release/vcs_helpers.py
+++ b/semantic_release/vcs_helpers.py
@@ -261,14 +261,17 @@ def push_new_version(
     """
     server = "origin"
     if auth_token:
-        token = auth_token
-        if config.get("hvcs") == "gitlab":
-            token = "gitlab-ci-token:" + token
-        actor = os.environ.get("GITHUB_ACTOR")
-        if actor:
-            server = f"https://{actor}:{token}@{domain}/{owner}/{name}.git"
+        if not config.get("ignore_token_for_push"):
+            token = auth_token
+            if config.get("hvcs") == "gitlab":
+                token = "gitlab-ci-token:" + token
+            actor = os.environ.get("GITHUB_ACTOR")
+            if actor:
+                server = f"https://{actor}:{token}@{domain}/{owner}/{name}.git"
+            else:
+                server = f"https://{token}@{domain}/{owner}/{name}.git"
         else:
-            server = f"https://{token}@{domain}/{owner}/{name}.git"
+            logger.debug("Ignoring token for pushing to the repository.")
 
     try:
         repo.git.push(server, branch)


Test patch:
diff --git a/tests/test_vcs_helpers.py b/tests/test_vcs_helpers.py
index aa2e028..01ed6b0 100644
--- a/tests/test_vcs_helpers.py
+++ b/tests/test_vcs_helpers.py
@@ -134,6 +134,44 @@ def test_push_new_version_with_custom_branch(mock_git):
     )
 
 
+@pytest.mark.parametrize("actor", (None, "GITHUB_ACTOR_TOKEN"))
+def test_push_using_token(mock_git, mocker, actor):
+    mocker.patch.dict(os.environ, {"GITHUB_ACTOR": actor} if actor else {}, clear=True)
+    token = "auth--token"
+    domain = "domain"
+    owner = "owner"
+    name = "name"
+    branch = "main"
+    push_new_version(auth_token=token, domain=domain, owner=owner, name=name, branch=branch)
+    server = f"https://{actor + ':' if actor else ''}{token}@{domain}/{owner}/{name}.git"
+    mock_git.push.assert_has_calls(
+        [
+            mock.call(server, branch),
+            mock.call("--tags", server, branch),
+        ]
+    )
+
+
+def test_push_ignoring_token(mock_git, mocker):
+    mocker.patch(
+        "semantic_release.vcs_helpers.config.get",
+        wrapped_config_get(**{"ignore_token_for_push": True}),
+    )
+    token = "auth--token"
+    domain = "domain"
+    owner = "owner"
+    name = "name"
+    branch = "main"
+    push_new_version(auth_token=token, domain=domain, owner=owner, name=name, branch=branch)
+    server = "origin"
+    mock_git.push.assert_has_calls(
+        [
+            mock.call(server, branch),
+            mock.call("--tags", server, branch),
+        ]
+    )
+
+
 @mock.patch.dict(
     os.environ,
     {
@@ -290,8 +328,7 @@ def test_get_current_head_hash(mocker):
     assert get_current_head_hash() == "commit-hash"
 
 
-@mock.patch("semantic_release.vcs_helpers.config.get", return_value="gitlab")
-def test_push_should_not_print_auth_token(mock_gitlab, mock_git):
+def test_push_should_not_print_auth_token(mock_git, mocker):
     mock_git.configure_mock(
         **{
             "push.side_effect": GitCommandError(
@@ -299,6 +336,10 @@ def test_push_should_not_print_auth_token(mock_gitlab, mock_git):
             )
         }
     )
+    mocker.patch(
+        "semantic_release.vcs_helpers.config.get",
+        wrapped_config_get(**{"hvcs": "gitlab"}),
+    )
     with pytest.raises(GitError) as excinfo:
         push_new_version(auth_token="auth--token")
     assert "auth--token" not in str(excinfo)