bmsdna-devtools 0.41.0__tar.gz → 0.42.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/PKG-INFO +8 -2
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/README.md +7 -1
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/cli.py +13 -3
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/cli_tools.py +43 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/gh_pr.py +18 -3
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/pr_build.py +17 -3
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/pyproject.toml +1 -1
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/skills/bmsdna-devtools/SKILL.md +5 -1
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_cli_pr_id.py +2 -2
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pr_choice.py +2 -2
- bmsdna_devtools-0.42.0/tests/test_wait_timeout.py +187 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/uv.lock +1 -1
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/.github/workflows/auto-release.yml +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/.github/workflows/python-publish.yml +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/.github/workflows/python-test.yml +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/.gitignore +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/.python-version +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/__init__.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/_api_dump.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/ado_auth.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/ado_issue.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/agent_mentions.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/api_usage.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/app_service_logs.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/bdt_config.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/commit.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/dead_code.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/env_config.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/env_files.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/find_injection.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/find_repo.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/gh_issue.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/gitrepo.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/injection_frontend.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/injection_python.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/issue_do.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/issue_show.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/issue_watch.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/lint.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/lint_findings.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/lint_models.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/lint_sql.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/lint_sql_files.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/lint_tooling.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/lint_typescript.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/logs.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/pr_choice.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/pr_info.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/pr_issue_link.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/pr_labels.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/pr_markdown.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/pull.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/testdb.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/translate.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/bmsdna/devtools/worktree.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/justfile +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/conftest.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_ado_auth.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_ado_issue.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_ado_issue_flow.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_agent_mentions.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_api_usage.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_app_service_logs.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_bdt_config.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_cli_cleanup.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_cli_commit.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_cli_issue.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_cli_pr_create.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_cli_pull.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_cli_tools.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_commit.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_env_config.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_env_files.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_find_injection.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_find_repo.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_gh_issue.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_gh_pr.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_gitrepo.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_issue_do.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_issue_for_me.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_issue_show.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_issue_watch.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_lint.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_lint_models.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_lint_sql.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_lint_sql_files.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_lint_tooling.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_lint_typescript.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pr_build.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pr_build_flow.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pr_description_not_truncated.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pr_info.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pr_issue_link.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pr_labels.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pr_markdown.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pull.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_testdb.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_translate.py +0 -0
- {bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_worktree_clean.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bmsdna-devtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.42.0
|
|
4
4
|
Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Requires-Dist: pgdevkit[db]>=0.7.1
|
|
@@ -50,7 +50,7 @@ Find the PR opened from the current branch and report build/check status
|
|
|
50
50
|
GitHub — whichever `origin` points at.
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
bdt pr status [--target-branch BRANCH] [--wait]
|
|
53
|
+
bdt pr status [--target-branch BRANCH] [--pr-id N] [--wait [--timeout SECONDS]]
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
Every command that acts on an existing PR (`pr status/info/retry/publish/update/comment`, `issue take` without
|
|
@@ -75,6 +75,12 @@ stage/check needs a reviewer plus a link to act on it, and exits **3**
|
|
|
75
75
|
build/check has already failed, that's reported instead (exit 1) even when
|
|
76
76
|
one is also waiting on approval.
|
|
77
77
|
|
|
78
|
+
`--wait --timeout SECONDS` gives up after that long when the checks are still running: it prints the current
|
|
79
|
+
state of every pipeline/check, ends with `Still running after N s. Re-run ... to keep waiting.` (the command pins `--pr-id` to the PR it watched) and exits **4**
|
|
80
|
+
(like `issue watch`). Use it where the caller's own limit is shorter than CI, e.g. an agent's shell tool, which
|
|
81
|
+
kills the command after a few minutes and discards its output; re-run the same command to keep waiting.
|
|
82
|
+
Without `--timeout` (or with `0`) it waits until everything is done, as before. `--timeout` needs `--wait`.
|
|
83
|
+
|
|
78
84
|
On Azure DevOps, `--wait` on a PR with no builds yet also checks that the target branch has an enabled
|
|
79
85
|
Build validation policy; if it doesn't, no build will ever run, so it exits 1 with an error instead of
|
|
80
86
|
polling forever (a failed policy lookup is ignored and the wait continues).
|
|
@@ -36,7 +36,7 @@ Find the PR opened from the current branch and report build/check status
|
|
|
36
36
|
GitHub — whichever `origin` points at.
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
bdt pr status [--target-branch BRANCH] [--wait]
|
|
39
|
+
bdt pr status [--target-branch BRANCH] [--pr-id N] [--wait [--timeout SECONDS]]
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Every command that acts on an existing PR (`pr status/info/retry/publish/update/comment`, `issue take` without
|
|
@@ -61,6 +61,12 @@ stage/check needs a reviewer plus a link to act on it, and exits **3**
|
|
|
61
61
|
build/check has already failed, that's reported instead (exit 1) even when
|
|
62
62
|
one is also waiting on approval.
|
|
63
63
|
|
|
64
|
+
`--wait --timeout SECONDS` gives up after that long when the checks are still running: it prints the current
|
|
65
|
+
state of every pipeline/check, ends with `Still running after N s. Re-run ... to keep waiting.` (the command pins `--pr-id` to the PR it watched) and exits **4**
|
|
66
|
+
(like `issue watch`). Use it where the caller's own limit is shorter than CI, e.g. an agent's shell tool, which
|
|
67
|
+
kills the command after a few minutes and discards its output; re-run the same command to keep waiting.
|
|
68
|
+
Without `--timeout` (or with `0`) it waits until everything is done, as before. `--timeout` needs `--wait`.
|
|
69
|
+
|
|
64
70
|
On Azure DevOps, `--wait` on a PR with no builds yet also checks that the target branch has an enabled
|
|
65
71
|
Build validation policy; if it doesn't, no build will ever run, so it exits 1 with an error instead of
|
|
66
72
|
polling forever (a failed policy lookup is ignored and the wait continues).
|
|
@@ -34,7 +34,7 @@ from . import translate as translate_mod
|
|
|
34
34
|
from . import pr_build, pr_info as pr_info_mod, pr_issue_link, pr_labels, pull as pull_mod, worktree as worktree_mod
|
|
35
35
|
from .ado_auth import auth_header
|
|
36
36
|
from .bdt_config import find_pyproject, load_bdt_table
|
|
37
|
-
from .cli_tools import CLI_TIMEOUT_SECS, detect_agent_session, require_az, require_gh
|
|
37
|
+
from .cli_tools import CLI_TIMEOUT_SECS, EXIT_WAIT_TIMEOUT, detect_agent_session, require_az, require_gh
|
|
38
38
|
from .gitrepo import (
|
|
39
39
|
AdoRemote,
|
|
40
40
|
GitHubRemote,
|
|
@@ -468,18 +468,28 @@ def pr_publish(
|
|
|
468
468
|
def pr_status(
|
|
469
469
|
target_branch: str | None = typer.Option(None, "--target-branch", help=_LOOKUP_TARGET_HELP),
|
|
470
470
|
wait: bool = typer.Option(False, "--wait", help="Poll until all pipelines/checks are completed; stops early and reports status if one needs manual approval"),
|
|
471
|
+
timeout: float = typer.Option(
|
|
472
|
+
0,
|
|
473
|
+
"--timeout",
|
|
474
|
+
help="With --wait: stop after this many seconds if the checks are still running, print their current state and exit "
|
|
475
|
+
f"{EXIT_WAIT_TIMEOUT} (0 = wait forever, the default). Use it where the caller's own time limit is shorter than CI, "
|
|
476
|
+
"e.g. an agent's shell tool, then re-run the same command to keep waiting",
|
|
477
|
+
),
|
|
471
478
|
pr_id: int | None = typer.Option(None, "--pr-id", help=_PR_ID_HELP),
|
|
472
479
|
pat: str | None = typer.Option(None, "--pat", envvar=["AZURE_DEVOPS_EXT_PAT", "AZURE_DEVOPS_PAT"], help="Azure DevOps PAT (else falls back to `az` login)"),
|
|
473
480
|
) -> None:
|
|
474
481
|
"""Show build/check status for the PR opened from the current branch, or for --pr-id
|
|
475
482
|
directly (Azure DevOps or GitHub, auto-detected).
|
|
476
483
|
"""
|
|
484
|
+
if timeout and not wait:
|
|
485
|
+
raise typer.BadParameter("--timeout only works together with --wait", param_hint="--timeout")
|
|
486
|
+
_check_poll_options(None, timeout)
|
|
477
487
|
remote = current_remote()
|
|
478
488
|
if isinstance(remote, GitHubRemote):
|
|
479
489
|
gh = require_gh()
|
|
480
|
-
gh_pr.run(gh, wait, pr_id=gh_pr.resolve_pr_id(gh, pr_id, target_branch))
|
|
490
|
+
gh_pr.run(gh, wait, pr_id=gh_pr.resolve_pr_id(gh, pr_id, target_branch), timeout=timeout)
|
|
481
491
|
return
|
|
482
|
-
pr_build.run(remote, pat, target_branch, wait, pr_id=pr_id)
|
|
492
|
+
pr_build.run(remote, pat, target_branch, wait, pr_id=pr_id, timeout=timeout)
|
|
483
493
|
|
|
484
494
|
|
|
485
495
|
@pr_app.command("info")
|
|
@@ -13,7 +13,9 @@ import os
|
|
|
13
13
|
import shutil
|
|
14
14
|
import sys
|
|
15
15
|
import time
|
|
16
|
+
from collections.abc import Callable
|
|
16
17
|
from dataclasses import dataclass
|
|
18
|
+
from typing import NoReturn
|
|
17
19
|
|
|
18
20
|
AZ_INSTALL_HINT = "Install the Azure CLI: https://learn.microsoft.com/cli/azure/install-azure-cli"
|
|
19
21
|
GH_INSTALL_HINT = "Install the GitHub CLI: https://cli.github.com"
|
|
@@ -26,6 +28,11 @@ PSQL_INSTALL_HINT = "Install the PostgreSQL client tools (psql): https://www.pos
|
|
|
26
28
|
# this tool), and callers branching on exit code shouldn't confuse the two.
|
|
27
29
|
EXIT_NEEDS_APPROVAL = 3
|
|
28
30
|
|
|
31
|
+
# `pr status --wait --timeout N` exits with this code when the checks are still running after N seconds
|
|
32
|
+
# (same code as `issue watch` / `issue search --wait` use for "gave up waiting"). The caller learns the
|
|
33
|
+
# current state from the output and can simply run the same command again to keep waiting.
|
|
34
|
+
EXIT_WAIT_TIMEOUT = 4
|
|
35
|
+
|
|
29
36
|
# How long a `--wait` poll loop may go without printing anything before `PollHeartbeat`
|
|
30
37
|
# forces a line out anyway. Every `--wait` loop in this tool polls every 30s but used to
|
|
31
38
|
# print only when the status text changed -- for a check/build that sits in the same
|
|
@@ -73,6 +80,42 @@ class PollHeartbeat:
|
|
|
73
80
|
self._last_print = now
|
|
74
81
|
|
|
75
82
|
|
|
83
|
+
class WaitDeadline:
|
|
84
|
+
"""Optional overall time limit of a `--wait` poll loop (`timeout_secs` of 0 / None = wait forever).
|
|
85
|
+
|
|
86
|
+
Exists because a caller often can't wait as long as CI takes: an agent's shell tool kills a command
|
|
87
|
+
after a few minutes and throws its output away. Giving up on our own terms lets us report where things
|
|
88
|
+
stand and exit with `EXIT_WAIT_TIMEOUT` so the caller knows to re-run.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, timeout_secs: float | None) -> None:
|
|
92
|
+
self.timeout_secs = timeout_secs or 0.0
|
|
93
|
+
self._end = time.monotonic() + self.timeout_secs if self.timeout_secs else None
|
|
94
|
+
|
|
95
|
+
def expired(self) -> bool:
|
|
96
|
+
return self._end is not None and time.monotonic() >= self._end
|
|
97
|
+
|
|
98
|
+
def sleep(self, secs: float) -> None:
|
|
99
|
+
"""`time.sleep(secs)`, cut short so the loop wakes up right at the deadline."""
|
|
100
|
+
time.sleep(secs if self._end is None else max(0.0, min(secs, self._end - time.monotonic())))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def status_rerun_cmd(pr_id: int | None, timeout_secs: float) -> str:
|
|
104
|
+
"""The command that keeps waiting where a timed-out `pr status --wait` stopped. Pins `--pr-id` to the PR that
|
|
105
|
+
was being watched, so re-running can't resolve to a different PR (e.g. another branch checked out since)."""
|
|
106
|
+
return f"bdt pr status --pr-id {pr_id} --wait --timeout {timeout_secs:g}" if pr_id else f"bdt pr status --wait --timeout {timeout_secs:g}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def exit_wait_timeout(msg: str, print_details: Callable[[], object], deadline: WaitDeadline, rerun_cmd: str) -> NoReturn:
|
|
110
|
+
"""Report where a `--wait` loop stands when its `deadline` ran out (status line, per-item details), then
|
|
111
|
+
exit `EXIT_WAIT_TIMEOUT`. The last line tells the caller what to run to keep waiting."""
|
|
112
|
+
print(msg)
|
|
113
|
+
print("\nDetails:")
|
|
114
|
+
print_details()
|
|
115
|
+
print(f"\nStill running after {deadline.timeout_secs:g} s. Re-run `{rerun_cmd}` to keep waiting.")
|
|
116
|
+
sys.exit(EXIT_WAIT_TIMEOUT)
|
|
117
|
+
|
|
118
|
+
|
|
76
119
|
def is_claude_code() -> bool:
|
|
77
120
|
"""True if this process is running as a subprocess of Claude Code.
|
|
78
121
|
|
|
@@ -20,7 +20,17 @@ from pathlib import Path
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
22
|
from . import pr_issue_link
|
|
23
|
-
from .cli_tools import
|
|
23
|
+
from .cli_tools import (
|
|
24
|
+
CLI_TIMEOUT_SECS,
|
|
25
|
+
EXIT_NEEDS_APPROVAL,
|
|
26
|
+
PollHeartbeat,
|
|
27
|
+
WaitDeadline,
|
|
28
|
+
detect_agent_session,
|
|
29
|
+
ensure_agent_session_note,
|
|
30
|
+
exit_wait_timeout,
|
|
31
|
+
is_claude_code,
|
|
32
|
+
status_rerun_cmd,
|
|
33
|
+
)
|
|
24
34
|
from .gitrepo import current_branch
|
|
25
35
|
from .pr_choice import PrCandidate, choose_pr
|
|
26
36
|
from .pr_markdown import build_attachments_section, build_comment_content, build_screenshots_section
|
|
@@ -308,8 +318,11 @@ def exit_needs_approval(msg: str, item_lines: list[str], rerun_cmd: str) -> None
|
|
|
308
318
|
sys.exit(EXIT_NEEDS_APPROVAL)
|
|
309
319
|
|
|
310
320
|
|
|
311
|
-
def run(gh: str, wait: bool, pr_id: int | None = None) -> None:
|
|
321
|
+
def run(gh: str, wait: bool, pr_id: int | None = None, timeout: float = 0) -> None:
|
|
322
|
+
"""`timeout` (seconds, only with `wait`): stop polling after that long and exit `EXIT_WAIT_TIMEOUT`
|
|
323
|
+
with the current state; 0 = wait until every check is done."""
|
|
312
324
|
heartbeat = PollHeartbeat()
|
|
325
|
+
deadline = WaitDeadline(timeout)
|
|
313
326
|
draft_notice_shown = False
|
|
314
327
|
while True:
|
|
315
328
|
pr = get_pr(gh, pr_id)
|
|
@@ -368,7 +381,9 @@ def run(gh: str, wait: bool, pr_id: int | None = None) -> None:
|
|
|
368
381
|
# must not keep polling forever waiting for it to become unstuck.
|
|
369
382
|
if "pending" in buckets and wait and not waiting_approval:
|
|
370
383
|
heartbeat.show(msg)
|
|
371
|
-
|
|
384
|
+
if deadline.expired():
|
|
385
|
+
exit_wait_timeout(msg, lambda: [print_check(c) for c in checks], deadline, status_rerun_cmd(pr_number, timeout))
|
|
386
|
+
deadline.sleep(30)
|
|
372
387
|
continue
|
|
373
388
|
|
|
374
389
|
print(msg)
|
|
@@ -6,12 +6,13 @@ import re
|
|
|
6
6
|
import sys
|
|
7
7
|
import time
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from typing import NoReturn
|
|
9
10
|
from urllib.parse import quote
|
|
10
11
|
|
|
11
12
|
import requests
|
|
12
13
|
|
|
13
14
|
from .ado_auth import auth_header
|
|
14
|
-
from .cli_tools import EXIT_NEEDS_APPROVAL, HTTP_TIMEOUT_SECS, PollHeartbeat, detect_agent_session, ensure_agent_session_note, is_claude_code
|
|
15
|
+
from .cli_tools import EXIT_NEEDS_APPROVAL, HTTP_TIMEOUT_SECS, PollHeartbeat, WaitDeadline, detect_agent_session, ensure_agent_session_note, exit_wait_timeout, is_claude_code, status_rerun_cmd
|
|
15
16
|
from .gitrepo import AdoRemote, current_branch
|
|
16
17
|
from .pr_choice import PrCandidate, choose_pr
|
|
17
18
|
from .pr_markdown import build_attachments_section, build_comment_content, build_screenshots_section
|
|
@@ -766,7 +767,10 @@ def run(
|
|
|
766
767
|
wait: bool,
|
|
767
768
|
source_branch: str | None = None,
|
|
768
769
|
pr_id: int | None = None,
|
|
770
|
+
timeout: float = 0,
|
|
769
771
|
) -> None:
|
|
772
|
+
"""`timeout` (seconds, only with `wait`): stop polling after that long and exit `EXIT_WAIT_TIMEOUT`
|
|
773
|
+
with the current state; 0 = wait until every pipeline is done."""
|
|
770
774
|
session = requests.Session()
|
|
771
775
|
session.headers.update(auth_header(pat))
|
|
772
776
|
|
|
@@ -804,8 +808,14 @@ def run(
|
|
|
804
808
|
draft_notice_shown = False
|
|
805
809
|
policy_checked = False
|
|
806
810
|
heartbeat = PollHeartbeat()
|
|
811
|
+
deadline = WaitDeadline(timeout)
|
|
812
|
+
|
|
813
|
+
def time_out(msg: str, builds: list, pr_number: int) -> NoReturn:
|
|
814
|
+
exit_wait_timeout(msg, lambda: [print_build(session, remote, b) for b in builds], deadline, status_rerun_cmd(pr_number, timeout))
|
|
815
|
+
|
|
807
816
|
while True:
|
|
808
817
|
pr = _poll_or_exit(fetch_pr)
|
|
818
|
+
pipeline_builds: list = []
|
|
809
819
|
if not draft_notice_shown:
|
|
810
820
|
draft_msg = draft_notice(pr)
|
|
811
821
|
if draft_msg:
|
|
@@ -827,7 +837,9 @@ def run(
|
|
|
827
837
|
b.get("definition", {}).get("name", "?") for b in stale
|
|
828
838
|
)
|
|
829
839
|
heartbeat.show(msg)
|
|
830
|
-
|
|
840
|
+
if deadline.expired():
|
|
841
|
+
time_out(msg, [b for b in pipeline_builds if b not in stale], pr_number)
|
|
842
|
+
deadline.sleep(30)
|
|
831
843
|
continue
|
|
832
844
|
msg += " | " + ", ".join(
|
|
833
845
|
f"{b.get('definition', {}).get('name', '?')} #{b['id']} {b.get('status')} ({b.get('result') or '—'})"
|
|
@@ -889,7 +901,9 @@ def run(
|
|
|
889
901
|
heartbeat.show(msg)
|
|
890
902
|
|
|
891
903
|
if wait:
|
|
892
|
-
|
|
904
|
+
if deadline.expired():
|
|
905
|
+
time_out(msg, pipeline_builds, pr_number)
|
|
906
|
+
deadline.sleep(30)
|
|
893
907
|
|
|
894
908
|
|
|
895
909
|
def run_watch_deploy(remote: AdoRemote, pat: str | None, target_branch: str, wait: bool) -> None:
|
|
@@ -10,7 +10,7 @@ packages = ["bmsdna"]
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "bmsdna-devtools"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.42.0"
|
|
14
14
|
description = "Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects"
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.14" # pgdevkit>=0.7.1 requires 3.14; was >=3.11 before adding it as a dependency
|
|
@@ -35,7 +35,7 @@ CLI to install rather than trying to work around it.
|
|
|
35
35
|
## Checking PR build/check status
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
bdt pr status [--target-branch BRANCH] [--pr-id N] [--wait]
|
|
38
|
+
bdt pr status [--target-branch BRANCH] [--pr-id N] [--wait [--timeout SECONDS]]
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Finds the PR opened from the current branch and prints build/check status,
|
|
@@ -48,6 +48,10 @@ error message for `mergeStatus=conflicts` (ADO) or `mergeable=CONFLICTING`
|
|
|
48
48
|
(GitHub) and tell the user to resolve conflicts rather than assuming CI is
|
|
49
49
|
just slow.
|
|
50
50
|
|
|
51
|
+
Where your shell tool has a time limit shorter than CI (it kills the command and drops its output), run
|
|
52
|
+
`bdt pr status --wait --timeout 540` (seconds, below the limit): after that long it prints the current state and
|
|
53
|
+
exits 4 — just run the same command again to keep waiting. Exit 3 = a stage needs a human's approval.
|
|
54
|
+
|
|
51
55
|
`pr status`, `info`, `retry`, `publish`, `update`, `comment` (and `issue take`
|
|
52
56
|
without a number) find the PR opened from the current branch into **any**
|
|
53
57
|
target; `--target`/`--target-branch` only narrows it. With several open PRs
|
|
@@ -32,7 +32,7 @@ def test_pr_status_with_pr_id_passes_it_to_gh_pr_run(monkeypatch) -> None:
|
|
|
32
32
|
monkeypatch.setattr("bmsdna.devtools.cli.current_remote", lambda: GITHUB_REMOTE)
|
|
33
33
|
monkeypatch.setattr("bmsdna.devtools.cli.require_gh", lambda: "gh")
|
|
34
34
|
captured = {}
|
|
35
|
-
monkeypatch.setattr("bmsdna.devtools.cli.gh_pr.run", lambda gh, wait, pr_id=None: captured.update(gh=gh, wait=wait, pr_id=pr_id))
|
|
35
|
+
monkeypatch.setattr("bmsdna.devtools.cli.gh_pr.run", lambda gh, wait, pr_id=None, timeout=0: captured.update(gh=gh, wait=wait, pr_id=pr_id))
|
|
36
36
|
|
|
37
37
|
result = runner.invoke(app, ["pr", "status", "--pr-id", "99"])
|
|
38
38
|
|
|
@@ -144,7 +144,7 @@ def test_pr_status_with_pr_id_skips_branch_resolution_for_ado(monkeypatch) -> No
|
|
|
144
144
|
captured = {}
|
|
145
145
|
monkeypatch.setattr(
|
|
146
146
|
"bmsdna.devtools.cli.pr_build.run",
|
|
147
|
-
lambda remote, pat, target_branch, wait, pr_id=None: captured.update(remote=remote, pr_id=pr_id),
|
|
147
|
+
lambda remote, pat, target_branch, wait, pr_id=None, timeout=0: captured.update(remote=remote, pr_id=pr_id),
|
|
148
148
|
)
|
|
149
149
|
|
|
150
150
|
result = runner.invoke(app, ["pr", "status", "--pr-id", "99"])
|
|
@@ -230,7 +230,7 @@ def test_cli_pr_status_interactive_with_several_prs_acts_on_the_chosen_one(monke
|
|
|
230
230
|
_github_cli(monkeypatch, [GH_INTO_MAIN, GH_INTO_DEV])
|
|
231
231
|
monkeypatch.setattr(pr_choice, "_interactive", lambda: True)
|
|
232
232
|
seen = {}
|
|
233
|
-
monkeypatch.setattr("bmsdna.devtools.cli.gh_pr.run", lambda gh, wait, pr_id=None: seen.update(pr_id=pr_id))
|
|
233
|
+
monkeypatch.setattr("bmsdna.devtools.cli.gh_pr.run", lambda gh, wait, pr_id=None, timeout=0: seen.update(pr_id=pr_id))
|
|
234
234
|
|
|
235
235
|
result = runner.invoke(app, ["pr", "status"], input="1\n")
|
|
236
236
|
|
|
@@ -241,7 +241,7 @@ def test_cli_pr_status_interactive_with_several_prs_acts_on_the_chosen_one(monke
|
|
|
241
241
|
def test_cli_pr_status_with_a_single_pr_into_main_does_not_ask(monkeypatch, never_asks) -> None:
|
|
242
242
|
_github_cli(monkeypatch, [GH_INTO_MAIN])
|
|
243
243
|
seen = {}
|
|
244
|
-
monkeypatch.setattr("bmsdna.devtools.cli.gh_pr.run", lambda gh, wait, pr_id=None: seen.update(pr_id=pr_id))
|
|
244
|
+
monkeypatch.setattr("bmsdna.devtools.cli.gh_pr.run", lambda gh, wait, pr_id=None, timeout=0: seen.update(pr_id=pr_id))
|
|
245
245
|
|
|
246
246
|
result = runner.invoke(app, ["pr", "status"])
|
|
247
247
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""`pr status --wait --timeout N`: give up after N seconds with the current state and exit code 4, so a caller
|
|
2
|
+
whose own time limit is shorter than CI (an agent's shell tool, say) gets a result instead of being cut off."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from unittest.mock import MagicMock
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from typer.testing import CliRunner
|
|
12
|
+
|
|
13
|
+
from bmsdna.devtools.cli import app
|
|
14
|
+
from bmsdna.devtools.cli_tools import EXIT_WAIT_TIMEOUT, WaitDeadline
|
|
15
|
+
from bmsdna.devtools.gh_pr import run as gh_run
|
|
16
|
+
from bmsdna.devtools.gitrepo import AdoRemote, GitHubRemote
|
|
17
|
+
from bmsdna.devtools.issue_watch import EXIT_TIMEOUT
|
|
18
|
+
from bmsdna.devtools.pr_build import run as ado_run
|
|
19
|
+
|
|
20
|
+
runner = CliRunner()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def sleeps(monkeypatch) -> list[float]:
|
|
25
|
+
"""A fake clock: `time.sleep` only advances `time.monotonic`, and records what it was asked for."""
|
|
26
|
+
now = [1000.0]
|
|
27
|
+
slept: list[float] = []
|
|
28
|
+
|
|
29
|
+
def fake_sleep(secs: float) -> None:
|
|
30
|
+
slept.append(secs)
|
|
31
|
+
now[0] += secs
|
|
32
|
+
if len(slept) > 500:
|
|
33
|
+
pytest.fail("polled forever: the --timeout deadline never fired")
|
|
34
|
+
|
|
35
|
+
monkeypatch.setattr(time, "monotonic", lambda: now[0])
|
|
36
|
+
monkeypatch.setattr(time, "sleep", fake_sleep)
|
|
37
|
+
return slept
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_exit_code_matches_the_other_wait_commands() -> None:
|
|
41
|
+
assert EXIT_WAIT_TIMEOUT == EXIT_TIMEOUT == 4
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_deadline_without_timeout_never_expires_and_sleeps_in_full(sleeps) -> None:
|
|
45
|
+
deadline = WaitDeadline(0)
|
|
46
|
+
deadline.sleep(30)
|
|
47
|
+
deadline.sleep(30)
|
|
48
|
+
|
|
49
|
+
assert sleeps == [30, 30]
|
|
50
|
+
assert not deadline.expired()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_deadline_cuts_the_last_sleep_short(sleeps) -> None:
|
|
54
|
+
deadline = WaitDeadline(70)
|
|
55
|
+
while not deadline.expired():
|
|
56
|
+
deadline.sleep(30)
|
|
57
|
+
|
|
58
|
+
assert sleeps == [30, 30, 10]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# -- GitHub -------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _gh_pr(checks: list[dict]) -> dict:
|
|
65
|
+
return {"number": 42, "title": "feat: widgets", "baseRefName": "main", "mergeable": "MERGEABLE", "isDraft": False, "statusCheckRollup": checks}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
PENDING = {"__typename": "CheckRun", "name": "build", "status": "IN_PROGRESS", "workflowName": "CI"}
|
|
69
|
+
PASSED = {"__typename": "CheckRun", "name": "lint", "status": "COMPLETED", "conclusion": "SUCCESS", "workflowName": "CI"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_gh_wait_times_out_with_exit_4_and_reports_every_check(monkeypatch, sleeps, capsys) -> None:
|
|
73
|
+
monkeypatch.setattr("bmsdna.devtools.gh_pr.get_pr", lambda gh, pr_id=None: _gh_pr([PASSED, PENDING]))
|
|
74
|
+
|
|
75
|
+
with pytest.raises(SystemExit) as exc:
|
|
76
|
+
gh_run("gh", wait=True, timeout=70)
|
|
77
|
+
|
|
78
|
+
out = capsys.readouterr().out
|
|
79
|
+
assert exc.value.code == EXIT_WAIT_TIMEOUT
|
|
80
|
+
assert sleeps == [30, 30, 10]
|
|
81
|
+
assert "[✓ PASS] CI / lint" in out and "[… PENDING] CI / build" in out
|
|
82
|
+
assert out.rstrip().endswith("Still running after 70 s. Re-run `bdt pr status --pr-id 42 --wait --timeout 70` to keep waiting.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_gh_wait_without_timeout_keeps_polling(monkeypatch, sleeps) -> None:
|
|
86
|
+
monkeypatch.setattr("bmsdna.devtools.gh_pr.get_pr", lambda gh, pr_id=None: _gh_pr([PENDING]))
|
|
87
|
+
|
|
88
|
+
class Enough(Exception):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
def sleep_a_few_times(secs: float) -> None:
|
|
92
|
+
sleeps.append(secs)
|
|
93
|
+
if len(sleeps) > 200:
|
|
94
|
+
raise Enough
|
|
95
|
+
|
|
96
|
+
monkeypatch.setattr(time, "sleep", sleep_a_few_times)
|
|
97
|
+
|
|
98
|
+
with pytest.raises(Enough):
|
|
99
|
+
gh_run("gh", wait=True)
|
|
100
|
+
|
|
101
|
+
assert len(sleeps) > 200
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_gh_wait_returns_normally_when_checks_finish_before_the_deadline(monkeypatch, sleeps, capsys) -> None:
|
|
105
|
+
polls = iter([_gh_pr([PENDING]), _gh_pr([{**PENDING, "status": "COMPLETED", "conclusion": "SUCCESS"}])])
|
|
106
|
+
monkeypatch.setattr("bmsdna.devtools.gh_pr.get_pr", lambda gh, pr_id=None: next(polls))
|
|
107
|
+
monkeypatch.setattr("bmsdna.devtools.gh_pr.deploy_run_hint", lambda gh, base: None)
|
|
108
|
+
|
|
109
|
+
gh_run("gh", wait=True, timeout=300)
|
|
110
|
+
|
|
111
|
+
assert sleeps == [30]
|
|
112
|
+
assert "Still running" not in capsys.readouterr().out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# -- Azure DevOps ---------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _ado_env(monkeypatch, builds: list[dict]) -> None:
|
|
119
|
+
monkeypatch.setattr("bmsdna.devtools.pr_build.requests.Session", lambda: MagicMock())
|
|
120
|
+
monkeypatch.setattr("bmsdna.devtools.pr_build.auth_header", lambda pat: {})
|
|
121
|
+
monkeypatch.setattr("bmsdna.devtools.pr_build.get_pr", lambda *a, **k: {"pullRequestId": 42, "title": "feat: widgets", "status": "active", "isDraft": False})
|
|
122
|
+
monkeypatch.setattr("bmsdna.devtools.pr_build.get_builds_for_pr", lambda *a, **k: builds)
|
|
123
|
+
monkeypatch.setattr("bmsdna.devtools.pr_build.find_pending_approvals", lambda *a, **k: [])
|
|
124
|
+
monkeypatch.setattr("bmsdna.devtools.pr_build.print_build", lambda session, remote, b: print(f" build #{b['id']} {b['status']}"))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_ado_wait_times_out_with_exit_4_and_reports_the_running_builds(monkeypatch, sleeps, capsys) -> None:
|
|
128
|
+
_ado_env(monkeypatch, [{"id": 200, "status": "inProgress", "result": None, "definition": {"id": 2, "name": "ci"}}])
|
|
129
|
+
|
|
130
|
+
with pytest.raises(SystemExit) as exc:
|
|
131
|
+
ado_run(AdoRemote("myorg", "MyProj", "myrepo"), None, "main", wait=True, source_branch="feature-x", timeout=45)
|
|
132
|
+
|
|
133
|
+
out = capsys.readouterr().out
|
|
134
|
+
assert exc.value.code == EXIT_WAIT_TIMEOUT
|
|
135
|
+
assert sleeps == [30, 15]
|
|
136
|
+
assert "ci #200 inProgress" in out and "build #200 inProgress" in out
|
|
137
|
+
assert out.rstrip().endswith("Still running after 45 s. Re-run `bdt pr status --pr-id 42 --wait --timeout 45` to keep waiting.")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_ado_wait_times_out_while_no_build_has_been_queued_yet(monkeypatch, sleeps, capsys) -> None:
|
|
141
|
+
_ado_env(monkeypatch, [])
|
|
142
|
+
monkeypatch.setattr("bmsdna.devtools.pr_build.lacks_build_policy", lambda *a: False)
|
|
143
|
+
|
|
144
|
+
with pytest.raises(SystemExit) as exc:
|
|
145
|
+
ado_run(AdoRemote("myorg", "MyProj", "myrepo"), None, "main", wait=True, source_branch="feature-x", timeout=60)
|
|
146
|
+
|
|
147
|
+
assert exc.value.code == EXIT_WAIT_TIMEOUT
|
|
148
|
+
assert "No builds found." in capsys.readouterr().out
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# -- CLI ----------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_cli_passes_timeout_on(monkeypatch) -> None:
|
|
155
|
+
monkeypatch.setattr("bmsdna.devtools.cli.current_remote", lambda: GitHubRemote("owner", "repo"))
|
|
156
|
+
monkeypatch.setattr("bmsdna.devtools.cli.require_gh", lambda: "gh")
|
|
157
|
+
monkeypatch.setattr("bmsdna.devtools.cli.gh_pr.resolve_pr_id", lambda gh, pr_id, target: pr_id)
|
|
158
|
+
seen: dict = {}
|
|
159
|
+
monkeypatch.setattr("bmsdna.devtools.cli.gh_pr.run", lambda gh, wait, pr_id=None, timeout=0: seen.update(wait=wait, timeout=timeout))
|
|
160
|
+
|
|
161
|
+
result = runner.invoke(app, ["pr", "status", "--wait", "--timeout", "540"])
|
|
162
|
+
|
|
163
|
+
assert result.exit_code == 0, result.output
|
|
164
|
+
assert seen == {"wait": True, "timeout": 540}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _plain(output: str) -> str:
|
|
168
|
+
"""Rich colors and boxes the error panel on CI terminals; reduce it to the bare words."""
|
|
169
|
+
return " ".join(re.sub(r"[│╭╮╰╯─]", " ", re.sub(r"\x1b\[[0-9;]*m", "", output)).split())
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_cli_timeout_needs_wait(monkeypatch) -> None:
|
|
173
|
+
monkeypatch.setattr("bmsdna.devtools.cli.current_remote", lambda: pytest.fail("must be rejected before anything runs"))
|
|
174
|
+
|
|
175
|
+
result = runner.invoke(app, ["pr", "status", "--timeout", "540"])
|
|
176
|
+
|
|
177
|
+
assert result.exit_code == 2
|
|
178
|
+
assert "--timeout only works together with --wait" in _plain(result.output)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@pytest.mark.parametrize("bad", ["-1", "nan", "inf"])
|
|
182
|
+
def test_cli_rejects_a_timeout_that_is_not_a_finite_number(monkeypatch, bad) -> None:
|
|
183
|
+
monkeypatch.setattr("bmsdna.devtools.cli.current_remote", lambda: pytest.fail("must be rejected before anything runs"))
|
|
184
|
+
|
|
185
|
+
result = runner.invoke(app, ["pr", "status", "--wait", "--timeout", bad])
|
|
186
|
+
|
|
187
|
+
assert result.exit_code == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bmsdna_devtools-0.41.0 → bmsdna_devtools-0.42.0}/tests/test_pr_description_not_truncated.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|