Skip to content

Commit

Permalink
feat(cli): rollback the repository state in case of a failed publish
Browse files Browse the repository at this point in the history
Additionally improves error messages slightly.
  • Loading branch information
Niko Heikkilä committed Sep 26, 2022
1 parent ee38976 commit f069c53
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 24 deletions.
50 changes: 30 additions & 20 deletions publicator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def cli(

if publish:
build_package()
publish_package(repository)
publish_package(repository, new_version)

if push:
push_changes()
Expand Down Expand Up @@ -114,7 +114,7 @@ def fatal(message: str, exit_code: int = 1) -> NoReturn:


def draft_new_release(tag: Semver) -> None:
info("Opening GitHub release draft in browser")
info("Opening GitHub release draft in browser...")

if preview:
return
Expand All @@ -129,25 +129,35 @@ def draft_new_release(tag: Semver) -> None:


def push_changes() -> None:
info("Pushing changes to Git")
if not preview:
info("Pushing changes to Git...")

if preview:
return

try:
git.push()
except CalledProcessError as e:
error(e.stderr)
fatal("Failed to push changes to Git!", exit_code=e.returncode)


def publish_package(repository: Optional[str], version: Semver) -> None:
info("Publishing the package to repository...")

def publish_package(repository: Optional[str]) -> None:
info("Publishing the package to repository")
if preview:
return

try:
poetry.publish(repository)
except CalledProcessError as e:
git.delete_tag(version)
git.reset()
error(e.stderr)
fatal("Failed to publish the package", exit_code=e.returncode)
fatal("Failed to publish the package! Repository state has been reset.", exit_code=e.returncode)


def build_package() -> None:
info("Building the package")
info("Building the package...")

if preview:
return
Expand All @@ -156,11 +166,11 @@ def build_package() -> None:
poetry.build()
except CalledProcessError as e:
error(e.stderr)
fatal("Failed to build the package", exit_code=e.returncode)
fatal("Failed to build the package!", exit_code=e.returncode)


def create_tag(version: Semver) -> None:
info(f"Creating a new tag {version} from HEAD")
info(f"Creating a new tag {version} from HEAD...")

if preview:
return
Expand All @@ -169,7 +179,7 @@ def create_tag(version: Semver) -> None:
git.create_tag(version, message=f"Version {version}")
except CalledProcessError as e:
error(e.stderr)
fatal("Failed to create a new tag", exit_code=e.returncode)
fatal("Failed to create a new tag!", exit_code=e.returncode)


def commit_changes(semver: Semver, template: str) -> None:
Expand All @@ -185,7 +195,7 @@ def commit_changes(semver: Semver, template: str) -> None:
git.commit(message)
except CalledProcessError as e:
error(e.stderr)
fatal("Failed to commit changes", exit_code=e.returncode)
fatal("Failed to commit changes!", exit_code=e.returncode)


def bump_version(version: str) -> Semver:
Expand All @@ -198,15 +208,15 @@ def bump_version(version: str) -> Semver:
next_version = poetry.bump(version)
except CalledProcessError as e:
error(e.stderr)
fatal("Failed to bump the version", exit_code=e.returncode)
fatal("Failed to bump the version!", exit_code=e.returncode)

info(f"Bumped version from {current_version} to {next_version}")

return next_version


def run_tests(script: str) -> None:
info(f"Running tests with '{script}'")
info(f"Running tests with '{script}' ...")

if preview:
return
Expand All @@ -215,11 +225,11 @@ def run_tests(script: str) -> None:
poetry.run(script)
except CalledProcessError as e:
error(e.stderr)
fatal("Test run failed", exit_code=e.returncode)
fatal("The test runner reported an error!", exit_code=e.returncode)


def install_dependencies() -> None:
info("Reinstalling project dependencies from pyproject.toml")
info("Reinstalling project dependencies from pyproject.toml...")

if preview:
return
Expand All @@ -228,27 +238,27 @@ def install_dependencies() -> None:
poetry.install()
except CalledProcessError as e:
error(e.stderr)
fatal("Failed reinstalling the project dependencies", exit_code=e.returncode)
fatal("Failed reinstalling the project dependencies!", exit_code=e.returncode)


def clean_up() -> None:
if preview or git.is_working_directory_clean():
return

info("Resetting working directory to a clean state")
info("Resetting working directory to a clean state...")

try:
git.stash()
git.pull()
git.pop()
except CalledProcessError as e:
error(e.stderr)
fatal("Failed to reset the working directory to a clean state", exit_code=e.returncode)
fatal("Failed to reset the working directory to a clean state!", exit_code=e.returncode)


def verify_branch() -> None:
current_branch = git.current_branch()
release_branches = git.release_branches()

if current_branch not in release_branches:
fatal(f"Current checked out branch {current_branch} is not a release branch {release_branches}")
fatal(f"Current checked out branch {current_branch} is not one of allowed release branches: {release_branches}")
13 changes: 11 additions & 2 deletions publicator/git.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Git operations"""
from __future__ import annotations

from dataclasses import dataclass
from typing import List, Mapping, Tuple

from parse import compile, Parser, Result
from parse import Parser, Result, compile
from semmy import Semver

from publicator import shell
from semmy import Semver


class RemoteParser:
Expand Down Expand Up @@ -84,5 +85,13 @@ def create_tag(version: Semver, message: str) -> List[str]:
return shell.run(f'git tag -a {version} -m "{message.strip()}"')


def delete_tag(version: Semver) -> List[str]:
return shell.run(f"git tag -d {version}")


def push() -> List[str]:
return shell.run("git push --follow-tags")


def reset() -> List[str]:
return shell.run("git reset --hard")
18 changes: 16 additions & 2 deletions tests/test_git.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from unittest.mock import MagicMock
from hypothesis import given
from hypothesis.strategies import builds, text, just

from hypothesis import given
from hypothesis.strategies import builds, just, text
from publicator import git
from semmy import Semver

Expand Down Expand Up @@ -72,13 +72,27 @@ def test_creating_tag(mock_shell: MagicMock) -> None:
assert git.create_tag(version=Semver(1, 2, 3), message="Version 1.2.3")


def test_deleting_tag(mock_shell: MagicMock) -> None:
effects = {"git tag -d 1.2.3": ["Deleted tag '1.2.3'"]}
mock_shell.side_effect = lambda cmd: effects.get(cmd, [])

assert git.delete_tag(version=Semver(1, 2, 3))


def test_pushing_changes(mock_shell: MagicMock) -> None:
effects = {"git push --follow-tags": ["Everything up-to-date"]}
mock_shell.side_effect = lambda cmd: effects.get(cmd, [])

assert git.push()


def test_reset(mock_shell: MagicMock) -> None:
effects = {"git reset --hard": ["HEAD is now at abc1234 chore: initial commit"]}
mock_shell.side_effect = lambda cmd: effects.get(cmd, [])

assert git.reset()


def test_extract_repo_from_invalid_remote(mock_shell: MagicMock) -> None:
effects = {"git remote get-url --push origin": ["no remote"]}
mock_shell.side_effect = lambda cmd: effects.get(cmd, [])
Expand Down

0 comments on commit f069c53

Please sign in to comment.