diff --git a/.ci/mypy.sh b/.ci/mypy.sh
index 792529f3af748be96dc1d0feb548cfa11e5f533f..dc01fac92bfd2351b55380ee10cb63914c04ee3f 100755
--- a/.ci/mypy.sh
+++ b/.ci/mypy.sh
@@ -4,7 +4,7 @@
 
 if [ "$(id -u)" = 0 ]; then
 	set -x
-	apk -q add py3-argcomplete py3-mypy
+	apk -q add py3-argcomplete py3-gitlab py3-mypy
 	exec su "${TESTUSER:-build}" -c "sh -e $0"
 fi
 
diff --git a/mrhlpr/frontend.py b/mrhlpr/frontend.py
index c3b47fd84e869ce6b5914ee72b4aab70d5237d64..20b7d488ea186baf5573aa1984a1453e9dcdd38f 100644
--- a/mrhlpr/frontend.py
+++ b/mrhlpr/frontend.py
@@ -170,19 +170,8 @@ def print_status(mr_id: Optional[int], no_cache: bool = False) -> None:
     if commits_follow_format is None:
         print("* Manually check if the commit subjects are correct")
 
-    origin = gitlab.parse_git_origin()
-
-    remote_local = status.source.split("/", 1)[0]
-    if remote_local == origin.project:
-        remote_local = "origin"
-
     print("* Pretty 'git log -" + str(len(commits)) + " --pretty'?" + " (consider copying MR desc)")
-    print(
-        f"* Push your changes ('git push --force {remote_local} HEAD:" f"{status.source_branch}')"
-    )
-    print("* Web UI: comment about your reviewing and testing")
-    print("* Web UI: approve MR")
-    print("* Web UI: do (automatic) merge")
+    print("* Finish the MR: push, approve, merge, comment ('mrhlpr finish')")
 
 
 def parse_args() -> argparse.Namespace:
@@ -241,6 +230,16 @@ def parse_args() -> argparse.Namespace:
         help=f"add message to last commit: {ci_labels['skip_vercheck']}",
     )
 
+    # finish
+    finish = sub.add_parser("finish", help="help finishing the MR (push, approve, merge...)")
+    finish.add_argument(
+        "-c",
+        "--comment",
+        type=str,
+        nargs="?",
+        help="comment to post before merging to thank author",
+    )
+
     if "argcomplete" in sys.modules:
         argcomplete.autocomplete(parser, always_complete_options="long")
     return parser.parse_args()
@@ -260,3 +259,11 @@ def main():
         mr_id = mr.checked_out()
         mr.fixmsg(mr_id, args.skip_build, args.ignore_count, args.skip_vercheck)
         print_status(mr_id)
+    elif args.action == "finish":
+        mr_id = mr.checked_out()
+        # TODO: Check that everything passed
+        comment = args.comment
+        if not comment:
+            comment = input("Provide a thank you comment (discouraged, but can be empty): ")
+        mr.finish(mr_id, comment, args.no_cache)
+        print_status(mr_id)
diff --git a/mrhlpr/git.py b/mrhlpr/git.py
index 40a206463f7a67edd0dc8f70fe796d9b3d5cf83f..c77a6f51ced732d39c7336bc0ee457432c3ee186 100644
--- a/mrhlpr/git.py
+++ b/mrhlpr/git.py
@@ -115,3 +115,21 @@ def topdir() -> str:
         msg = "Something went wrong when getting current branch name"
         raise RuntimeError(msg)
     return ret
+
+
+def sha() -> str:
+    """:returns: current SHA"""
+    ret = run(["rev-parse"])
+    if ret is None:
+        msg = "Something went wrong when getting current commit SHA"
+        raise RuntimeError(msg)
+    return ret
+
+
+def push_head(remote: str, branch: str) -> None:
+    """Push HEAD to branch in remote"""
+    ret = run(["push", "--force-with-lease", remote, "HEAD:" + branch])
+    if ret is None:
+        msg = f"Something went wrong when pushing HEAD to branch '{branch}' in remote '{remote}'"
+        raise RuntimeError(msg)
+    print(ret)
diff --git a/mrhlpr/mr.py b/mrhlpr/mr.py
index a5d645eecc75201484308b3ac4de91ba56e3c6f6..54a08b0af685caee4ef8a2d712a6e32218368cf6 100644
--- a/mrhlpr/mr.py
+++ b/mrhlpr/mr.py
@@ -8,9 +8,15 @@ import os
 import re
 import subprocess
 import sys
+import time
 from dataclasses import dataclass
 from typing import Any, Optional
 
+try:
+    import gitlab as gitlab_api
+except ImportError:
+    pass
+
 from . import ci_labels  # type: ignore[attr-defined]
 from . import git
 from . import gitlab
@@ -436,3 +442,41 @@ def fixmsg(
     if skip_vercheck:
         print("Appending [ci:skip-vercheck] to last commit...")
         run_git_filter_branch("msg_filter_add_label.py", "HEAD~1", label="skip_vercheck")
+
+
+def finish(mr_id: int, comment: str | None, no_cache: bool = False):
+    if "gitlab" not in sys.modules:
+        logging.error("python-gitlab is needed to run 'finish'")
+        exit(1)
+
+    print("Pushing changes...")
+    status = get_status(mr_id, no_cache)
+    origin = gitlab.parse_git_origin()
+    remote_local = status.source.split("/", 1)[0]
+    if remote_local == origin.project:
+        remote_local = "origin"
+    git.push_head(remote_local, status.source_branch)
+
+    gl = gitlab_api.Gitlab(url="https://gitlab.com", private_token=os.environ["GITLAB_TOKEN"])
+    project = gl.projects.get(origin.project_id)
+    mr = project.mergerequests.get(mr_id)
+    print("Approving MR...")
+    try:
+        mr.approve(git.sha())
+    # For some reason, approve fails with auth error if already approved...
+    except gitlab_api.GitlabAuthenticationError as e:
+        approved_by_ids = [ap["id"] for ap in mr.approval_state.get().rules[0]["approved_by"]]
+        gl.auth()
+        if gl.user is None:
+            raise e
+        if gl.user.id not in approved_by_ids:
+            raise RuntimeError("Approving failed, and MR not yet approved") from e
+
+    if comment:
+        print("Adding the comment...")
+        mr.notes.create({"body": comment})
+
+    # Give gitlab some time to process the push, it sometimes struggles
+    time.sleep(5)
+    print("Setting to auto-merge...")
+    mr.merge(merge_when_pipeline_succeeds=True)
diff --git a/pyproject.toml b/pyproject.toml
index 6fb5ae726ce694fe12ec16272290cd5449adf1aa..cbabc8921e30ad9dbe8638a559e3d082b9662f7b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,6 +30,7 @@ mrtest = "mrtest.frontend:main"
 
 [project.optional-dependencies]
 completion = ["argcomplete"]
+api = ["python-gitlab>=4.0.0"]
 
 [project.urls]
 Homepage = "https://www.postmarketos.org"