Skip to content

Commit

Permalink
Merge pull request #435 from codemagic-ci-cd/bugfix/altool-retry-by-e…
Browse files Browse the repository at this point in the history
…xit-code

Retry `altool --upload-app` on return codes `-5` and `-11`
  • Loading branch information
fran-tirapu authored Nov 6, 2024
2 parents 77f1cc9 + 0e2de60 commit 11e98a6
Show file tree
Hide file tree
Showing 5 changed files with 37 additions and 7 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
Version 0.54.2
-------------

**Bugfixes**
- Introduce a new retrying condition for `altool` commands as part of `app-store-connect` action when unexpected return codes occurs. [PR #435](https://github.com/codemagic-ci-cd/cli-tools/pull/435)


Version 0.54.1
-------------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "codemagic-cli-tools"
version = "0.54.1"
version = "0.54.2"
description = "CLI tools used in Codemagic builds"
readme = "README.md"
authors = [
Expand Down
2 changes: 1 addition & 1 deletion src/codemagic/__version__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__title__ = "codemagic-cli-tools"
__description__ = "CLI tools used in Codemagic builds"
__version__ = "0.54.1.dev"
__version__ = "0.54.2.dev"
__url__ = "https://github.com/codemagic-ci-cd/cli-tools"
__licence__ = "GNU General Public License v3.0"
14 changes: 9 additions & 5 deletions src/codemagic/models/altool/altool.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@


class AltoolCommandError(Exception):
def __init__(self, error_message: str, process_output: str):
def __init__(self, error_message: str, process_output: str, return_code: int = 1):
super().__init__(error_message)
self.process_output = process_output
self.return_code = return_code


class Altool(RunningCliAppMixin, StringConverterMixin):
Expand Down Expand Up @@ -196,7 +197,7 @@ def _run_retrying_command(
return self._run_command(command, f'Failed to {action_name} archive at "{artifact_path}"', cli_app)
except AltoolCommandError as error:
has_retries = retries > 0
should_retry = self._should_retry_command(error.process_output)
should_retry = self._should_retry_command(error)
if has_retries and should_retry:
if attempt == 1:
print_fn(f"Failed to {action_name} archive, but this might be a temporary issue, retrying...")
Expand Down Expand Up @@ -258,6 +259,7 @@ def _run_command(
raise AltoolCommandError(
error_message,
self._hide_environment_variable_values(cpe.stdout),
cpe.returncode,
)

self._log_process_output(stdout, cli_app)
Expand Down Expand Up @@ -286,14 +288,16 @@ def _hide_environment_variable_values(cls, altool_output: Optional[AnyStr]) -> s

return output

@classmethod
def _should_retry_command(cls, process_output: str):
def _should_retry_command(self, command_error: AltoolCommandError) -> bool:
if command_error.return_code in [-5, -11]:
self.logger.info(f"Unexpected altool exit code {command_error.return_code}, retrying...")
return True
patterns = (
re.compile("Unable to authenticate.*-19209"),
re.compile("server returned an invalid response.*try your request again"),
re.compile("The request timed out."),
)
return any(pattern.search(process_output) for pattern in patterns)
return any(pattern.search(command_error.process_output) for pattern in patterns)

@classmethod
def _get_action_result(cls, action_stdout: AnyStr) -> Optional[AltoolResult]:
Expand Down
19 changes: 19 additions & 0 deletions tests/models/altool/test_altool_retrying.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,25 @@ def test_retrying_command_success(mock_altool, mock_auth_error_stdout, mock_succ
assert mock_kill_xcodes.call_count == 2


@mock.patch.object(PlatformType, "from_path", lambda _artifact_path: PlatformType.IOS)
def test_retrying_command_by_return_code_and_success(caplog, mock_altool, mock_success_result):
raise_errors = mock.Mock(
side_effect=(
AltoolCommandError("my error", "process output", -11),
AltoolCommandError("my error", "process output", -5),
AltoolCommandError("my error", "process output", -5),
mock_success_result,
),
)

with mock.patch.object(mock_altool, "_run_command", side_effect=raise_errors):
result = mock_altool.upload_app(pathlib.Path("app.ipa"), retries=4, retry_wait_seconds=0)

assert result is mock_success_result
assert caplog.text.count("Unexpected altool exit code -11, retrying...") == 1
assert caplog.text.count("Unexpected altool exit code -5, retrying...") == 2


@mock.patch.object(PlatformType, "from_path", lambda _artifact_path: PlatformType.IOS)
def test_retrying_command_immediate_success(mock_altool, mock_success_stdout, mock_success_result):
with mock.patch.object(mock_altool, "_run_command", side_effect=[mock_success_result]):
Expand Down

0 comments on commit 11e98a6

Please sign in to comment.