gha-utils 4.21.0__tar.gz → 4.23.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.
Potentially problematic release.
This version of gha-utils might be problematic. Click here for more details.
- {gha_utils-4.21.0 → gha_utils-4.23.0}/PKG-INFO +11 -11
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils/__init__.py +1 -1
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils/cli.py +13 -8
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils/matrix.py +5 -1
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils/metadata.py +93 -14
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils/test_plan.py +62 -17
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils.egg-info/PKG-INFO +11 -11
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils.egg-info/requires.txt +10 -10
- {gha_utils-4.21.0 → gha_utils-4.23.0}/pyproject.toml +32 -18
- {gha_utils-4.21.0 → gha_utils-4.23.0}/tests/test_metadata.py +3 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils/__main__.py +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils/changelog.py +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils/mailmap.py +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils/py.typed +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils.egg-info/SOURCES.txt +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils.egg-info/dependency_links.txt +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils.egg-info/entry_points.txt +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/gha_utils.egg-info/top_level.txt +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/readme.md +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/setup.cfg +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/tests/test_changelog.py +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/tests/test_mailmap.py +0 -0
- {gha_utils-4.21.0 → gha_utils-4.23.0}/tests/test_matrix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gha-utils
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.23.0
|
|
4
4
|
Summary: ⚙️ CLI helpers for GitHub Actions + reuseable workflows
|
|
5
5
|
Author-email: Kevin Deldycke <kevin@deldycke.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/kdeldycke/workflows
|
|
@@ -48,18 +48,18 @@ Classifier: Topic :: Utilities
|
|
|
48
48
|
Classifier: Typing :: Typed
|
|
49
49
|
Requires-Python: >=3.11
|
|
50
50
|
Description-Content-Type: text/markdown
|
|
51
|
-
Requires-Dist: boltons>=
|
|
51
|
+
Requires-Dist: boltons>=25.0.0
|
|
52
52
|
Requires-Dist: bump-my-version<1.1.1,>=0.32.2
|
|
53
|
-
Requires-Dist: click-extra
|
|
54
|
-
Requires-Dist: extra-platforms
|
|
55
|
-
Requires-Dist: packaging
|
|
56
|
-
Requires-Dist: py-walk
|
|
57
|
-
Requires-Dist: PyDriller
|
|
58
|
-
Requires-Dist: pyproject-metadata
|
|
59
|
-
Requires-Dist: pyyaml
|
|
60
|
-
Requires-Dist: wcmatch>=
|
|
53
|
+
Requires-Dist: click-extra>=6.0.3
|
|
54
|
+
Requires-Dist: extra-platforms>=4.0.0
|
|
55
|
+
Requires-Dist: packaging>=25.0
|
|
56
|
+
Requires-Dist: py-walk>=0.3.3
|
|
57
|
+
Requires-Dist: PyDriller>=2.6
|
|
58
|
+
Requires-Dist: pyproject-metadata>=0.9.0
|
|
59
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
60
|
+
Requires-Dist: wcmatch>=10.0
|
|
61
61
|
Provides-Extra: test
|
|
62
|
-
Requires-Dist: coverage[toml]~=7.
|
|
62
|
+
Requires-Dist: coverage[toml]~=7.11.0; extra == "test"
|
|
63
63
|
Requires-Dist: pytest~=8.4.0; extra == "test"
|
|
64
64
|
Requires-Dist: pytest-cases~=3.9.1; extra == "test"
|
|
65
65
|
Requires-Dist: pytest-cov~=7.0.0; extra == "test"
|
|
@@ -23,7 +23,6 @@ import sys
|
|
|
23
23
|
from collections import Counter
|
|
24
24
|
from datetime import datetime
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import IO
|
|
27
26
|
|
|
28
27
|
from boltons.iterutils import unique
|
|
29
28
|
from click_extra import (
|
|
@@ -47,6 +46,10 @@ from .mailmap import Mailmap
|
|
|
47
46
|
from .metadata import NUITKA_BUILD_TARGETS, Dialects, Metadata
|
|
48
47
|
from .test_plan import DEFAULT_TEST_PLAN, SkippedTest, parse_test_plan
|
|
49
48
|
|
|
49
|
+
TYPE_CHECKING = False
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from typing import IO
|
|
52
|
+
|
|
50
53
|
|
|
51
54
|
def is_stdout(filepath: Path) -> bool:
|
|
52
55
|
"""Check if a file path is set to stdout.
|
|
@@ -303,11 +306,11 @@ def mailmap_sync(ctx, source, create_if_missing, destination_mailmap):
|
|
|
303
306
|
|
|
304
307
|
@gha_utils.command(short_help="Run a test plan from a file against a binary")
|
|
305
308
|
@option(
|
|
309
|
+
"--command",
|
|
306
310
|
"--binary",
|
|
307
|
-
type=file_path(exists=True, executable=True, resolve_path=True),
|
|
308
311
|
required=True,
|
|
309
|
-
metavar="
|
|
310
|
-
help="Path to the binary file to test.",
|
|
312
|
+
metavar="COMMAND",
|
|
313
|
+
help="Path to the binary file to test, or a command line to be executed.",
|
|
311
314
|
)
|
|
312
315
|
@option(
|
|
313
316
|
"-F",
|
|
@@ -375,7 +378,7 @@ def mailmap_sync(ctx, source, create_if_missing, destination_mailmap):
|
|
|
375
378
|
help="Print per-manager package statistics.",
|
|
376
379
|
)
|
|
377
380
|
def test_plan(
|
|
378
|
-
|
|
381
|
+
command: str,
|
|
379
382
|
plan_file: tuple[Path, ...] | None,
|
|
380
383
|
plan_envvar: tuple[str, ...] | None,
|
|
381
384
|
select_test: tuple[int, ...] | None,
|
|
@@ -422,7 +425,9 @@ def test_plan(
|
|
|
422
425
|
try:
|
|
423
426
|
logging.debug(f"Test case parameters: {test_case}")
|
|
424
427
|
test_case.run_cli_test(
|
|
425
|
-
|
|
428
|
+
command,
|
|
429
|
+
additional_skip_platforms=skip_platform,
|
|
430
|
+
default_timeout=timeout,
|
|
426
431
|
)
|
|
427
432
|
except SkippedTest as ex:
|
|
428
433
|
counter["skipped"] += 1
|
|
@@ -430,8 +435,8 @@ def test_plan(
|
|
|
430
435
|
except Exception as ex:
|
|
431
436
|
counter["failed"] += 1
|
|
432
437
|
logging.error(f"Test {test_name} failed: {ex}")
|
|
433
|
-
if show_trace_on_error:
|
|
434
|
-
echo(test_case.execution_trace
|
|
438
|
+
if show_trace_on_error and test_case.execution_trace:
|
|
439
|
+
echo(test_case.execution_trace)
|
|
435
440
|
if exit_on_error:
|
|
436
441
|
logging.debug("Don't continue testing, a failed test was found.")
|
|
437
442
|
sys.exit(1)
|
|
@@ -19,11 +19,15 @@ from __future__ import annotations
|
|
|
19
19
|
import itertools
|
|
20
20
|
import json
|
|
21
21
|
import logging
|
|
22
|
-
from typing import Iterable, Iterator
|
|
23
22
|
|
|
24
23
|
from boltons.dictutils import FrozenDict
|
|
25
24
|
from boltons.iterutils import unique
|
|
26
25
|
|
|
26
|
+
TYPE_CHECKING = False
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Iterable, Iterator
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
RESERVED_MATRIX_KEYWORDS = ["include", "exclude"]
|
|
28
32
|
|
|
29
33
|
|
|
@@ -31,6 +31,7 @@ yaml_files="config.yaml" ".github/workflows/lint.yaml" ".github/workflows/test.y
|
|
|
31
31
|
workflow_files=".github/workflows/lint.yaml" ".github/workflows/test.yaml"
|
|
32
32
|
doc_files="changelog.md" "readme.md" "docs/license.md"
|
|
33
33
|
markdown_files="changelog.md" "readme.md" "docs/license.md"
|
|
34
|
+
image_files=
|
|
34
35
|
zsh_files=
|
|
35
36
|
is_python_project=true
|
|
36
37
|
package_name=click-extra
|
|
@@ -285,12 +286,12 @@ from operator import itemgetter
|
|
|
285
286
|
from pathlib import Path
|
|
286
287
|
from random import randint
|
|
287
288
|
from re import escape
|
|
288
|
-
from typing import Any, Final, cast
|
|
289
289
|
|
|
290
290
|
from bumpversion.config import get_configuration # type: ignore[import-untyped]
|
|
291
291
|
from bumpversion.config.files import find_config_file # type: ignore[import-untyped]
|
|
292
292
|
from bumpversion.show import resolve_name # type: ignore[import-untyped]
|
|
293
293
|
from extra_platforms import is_github_ci
|
|
294
|
+
from gitdb.exc import BadName # type: ignore[import-untyped]
|
|
294
295
|
from packaging.specifiers import SpecifierSet
|
|
295
296
|
from packaging.version import Version
|
|
296
297
|
from py_walk import get_parser_from_file
|
|
@@ -310,6 +311,11 @@ from wcmatch.glob import (
|
|
|
310
311
|
|
|
311
312
|
from .matrix import Matrix
|
|
312
313
|
|
|
314
|
+
TYPE_CHECKING = False
|
|
315
|
+
if TYPE_CHECKING:
|
|
316
|
+
from typing import Any, Final
|
|
317
|
+
|
|
318
|
+
|
|
313
319
|
SHORT_SHA_LENGTH = 7
|
|
314
320
|
"""Default SHA length hard-coded to ``7``.
|
|
315
321
|
|
|
@@ -559,6 +565,59 @@ class Metadata:
|
|
|
559
565
|
logging.debug(f"Number of stashes in repository: {count}")
|
|
560
566
|
return count
|
|
561
567
|
|
|
568
|
+
def git_deepen(
|
|
569
|
+
self, commit_hash: str, max_attempts: int = 10, deepen_increment: int = 50
|
|
570
|
+
) -> bool:
|
|
571
|
+
"""Deepen a shallow clone until the provided ``commit_hash`` is found.
|
|
572
|
+
|
|
573
|
+
Progressively fetches more commits from the current repository until the
|
|
574
|
+
specified commit is found or max attempts is reached.
|
|
575
|
+
|
|
576
|
+
Returns ``True`` if the commit was found, ``False`` otherwise.
|
|
577
|
+
"""
|
|
578
|
+
for attempt in range(max_attempts):
|
|
579
|
+
try:
|
|
580
|
+
_ = self.git.get_commit(commit_hash)
|
|
581
|
+
if attempt > 0:
|
|
582
|
+
logging.info(
|
|
583
|
+
f"Found commit {commit_hash} after {attempt} deepen "
|
|
584
|
+
"operation(s)."
|
|
585
|
+
)
|
|
586
|
+
return True
|
|
587
|
+
except (ValueError, BadName) as ex:
|
|
588
|
+
logging.debug(f"Commit {commit_hash} not found: {ex}")
|
|
589
|
+
|
|
590
|
+
current_depth = self.git.total_commits()
|
|
591
|
+
|
|
592
|
+
if attempt == max_attempts - 1:
|
|
593
|
+
# We've exhausted all attempts
|
|
594
|
+
logging.error(
|
|
595
|
+
f"Cannot find commit {commit_hash} in repository after "
|
|
596
|
+
f"{max_attempts} deepen attempts. "
|
|
597
|
+
f"Final depth is {current_depth} commits."
|
|
598
|
+
)
|
|
599
|
+
return False
|
|
600
|
+
|
|
601
|
+
logging.info(
|
|
602
|
+
f"Commit {commit_hash} not found at depth {current_depth}."
|
|
603
|
+
)
|
|
604
|
+
logging.info(
|
|
605
|
+
f"Deepening by {deepen_increment} commits (attempt "
|
|
606
|
+
f"{attempt + 1}/{max_attempts})..."
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
self.git.repo.git.fetch(f"--deepen={deepen_increment}")
|
|
611
|
+
new_depth = self.git.total_commits()
|
|
612
|
+
logging.debug(
|
|
613
|
+
f"Repository deepened successfully. New depth: {new_depth}"
|
|
614
|
+
)
|
|
615
|
+
except Exception as ex:
|
|
616
|
+
logging.error(f"Failed to deepen repository: {ex}")
|
|
617
|
+
return False
|
|
618
|
+
|
|
619
|
+
return False
|
|
620
|
+
|
|
562
621
|
def commit_matrix(self, commits: Iterable[Commit] | None) -> Matrix | None:
|
|
563
622
|
"""Pre-compute a matrix of commits.
|
|
564
623
|
|
|
@@ -719,7 +778,10 @@ class Metadata:
|
|
|
719
778
|
def event_sender_type(self) -> str | None:
|
|
720
779
|
"""Returns the type of the user that triggered the workflow run."""
|
|
721
780
|
sender_type = self.github_context.get("event", {}).get("sender", {}).get("type")
|
|
722
|
-
|
|
781
|
+
if not sender_type:
|
|
782
|
+
return None
|
|
783
|
+
assert isinstance(sender_type, str)
|
|
784
|
+
return sender_type
|
|
723
785
|
|
|
724
786
|
@cached_property
|
|
725
787
|
def is_bot(self) -> bool:
|
|
@@ -736,7 +798,7 @@ class Metadata:
|
|
|
736
798
|
return False
|
|
737
799
|
|
|
738
800
|
@cached_property
|
|
739
|
-
def commit_range(self) -> tuple[str, str] | None:
|
|
801
|
+
def commit_range(self) -> tuple[str | None, str] | None:
|
|
740
802
|
"""Range of commits bundled within the triggering event.
|
|
741
803
|
|
|
742
804
|
A workflow run is triggered by a singular event, which might encapsulate one or
|
|
@@ -777,13 +839,18 @@ class Metadata:
|
|
|
777
839
|
):
|
|
778
840
|
base_ref = os.environ["GITHUB_BASE_REF"]
|
|
779
841
|
assert base_ref
|
|
780
|
-
|
|
842
|
+
assert (
|
|
843
|
+
self.github_context["event"]["pull_request"]["base"]["ref"] == base_ref
|
|
844
|
+
)
|
|
845
|
+
full_base_ref = f"origin/{base_ref}"
|
|
846
|
+
base_ref_sha = self.github_context["event"]["pull_request"]["base"]["sha"]
|
|
847
|
+
start = base_ref_sha
|
|
781
848
|
# We need to checkout the HEAD commit instead of the artificial merge
|
|
782
849
|
# commit introduced by the pull request.
|
|
783
850
|
end = self.github_context["event"]["pull_request"]["head"]["sha"]
|
|
784
851
|
# Push event.
|
|
785
852
|
else:
|
|
786
|
-
start = self.github_context["event"]
|
|
853
|
+
start = self.github_context["event"].get("before")
|
|
787
854
|
end = os.environ["GITHUB_SHA"]
|
|
788
855
|
assert end
|
|
789
856
|
logging.debug(f"Commit range: {start} -> {end}")
|
|
@@ -811,19 +878,20 @@ class Metadata:
|
|
|
811
878
|
# inclusive), we still need to make sure it exists: PyDriller stills needs to
|
|
812
879
|
# find it to be able to traverse the commit history.
|
|
813
880
|
for commit_id in (start, end):
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
f"Cannot find commit {commit_id} in repository. "
|
|
819
|
-
"Repository was probably not checked out with enough depth. "
|
|
820
|
-
f"Current depth is {self.git.total_commits()}. "
|
|
821
|
-
)
|
|
881
|
+
if not commit_id:
|
|
882
|
+
continue
|
|
883
|
+
|
|
884
|
+
if not self.git_deepen(commit_id):
|
|
822
885
|
logging.warning(
|
|
823
886
|
"Skipping metadata extraction of the range of new commits."
|
|
824
887
|
)
|
|
825
888
|
return None
|
|
826
889
|
|
|
890
|
+
if not start:
|
|
891
|
+
logging.warning("No start commit found. Only one commit in range.")
|
|
892
|
+
assert end
|
|
893
|
+
return (self.git.get_commit(end),)
|
|
894
|
+
|
|
827
895
|
commit_list = []
|
|
828
896
|
for index, commit in enumerate(
|
|
829
897
|
Repository(".", from_commit=start, to_commit=end).traverse_commits()
|
|
@@ -1003,6 +1071,15 @@ class Metadata:
|
|
|
1003
1071
|
"""Returns a list of Markdown files."""
|
|
1004
1072
|
return self.glob_files("**/*.{markdown,mdown,mkdn,mdwn,mkd,md,mdtxt,mdtext}")
|
|
1005
1073
|
|
|
1074
|
+
@cached_property
|
|
1075
|
+
def image_files(self) -> list[Path]:
|
|
1076
|
+
"""Returns a list of image files.
|
|
1077
|
+
|
|
1078
|
+
Inspired by the list of image extensions supported by calibre's image-actions:
|
|
1079
|
+
https://github.com/calibreapp/image-actions/blob/f325757/src/constants.ts#L32
|
|
1080
|
+
"""
|
|
1081
|
+
return self.glob_files("**/*.{jpeg,jpg,png,webp,avif}")
|
|
1082
|
+
|
|
1006
1083
|
@cached_property
|
|
1007
1084
|
def zsh_files(self) -> list[Path]:
|
|
1008
1085
|
"""Returns a list of Zsh files."""
|
|
@@ -1127,6 +1204,7 @@ class Metadata:
|
|
|
1127
1204
|
- ``--target-version py311``
|
|
1128
1205
|
- ``--target-version py312``
|
|
1129
1206
|
- ``--target-version py313``
|
|
1207
|
+
- ``--target-version py314``
|
|
1130
1208
|
|
|
1131
1209
|
As mentioned in Black usage, you should `include all Python versions that you
|
|
1132
1210
|
want your code to run under
|
|
@@ -1556,7 +1634,7 @@ class Metadata:
|
|
|
1556
1634
|
else:
|
|
1557
1635
|
raise NotImplementedError(f"GitHub formatting for: {value!r}")
|
|
1558
1636
|
|
|
1559
|
-
return
|
|
1637
|
+
return str(value)
|
|
1560
1638
|
|
|
1561
1639
|
def dump(self, dialect: Dialects = Dialects.github) -> str:
|
|
1562
1640
|
"""Returns all metadata in the specified format.
|
|
@@ -1575,6 +1653,7 @@ class Metadata:
|
|
|
1575
1653
|
"workflow_files": self.workflow_files,
|
|
1576
1654
|
"doc_files": self.doc_files,
|
|
1577
1655
|
"markdown_files": self.markdown_files,
|
|
1656
|
+
"image_files": self.image_files,
|
|
1578
1657
|
"zsh_files": self.zsh_files,
|
|
1579
1658
|
"is_python_project": self.is_python_project,
|
|
1580
1659
|
"package_name": self.package_name,
|
|
@@ -17,19 +17,31 @@
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import logging
|
|
20
|
+
import os
|
|
20
21
|
import re
|
|
21
22
|
import shlex
|
|
22
23
|
import sys
|
|
24
|
+
from collections.abc import Sequence
|
|
23
25
|
from dataclasses import asdict, dataclass, field
|
|
24
26
|
from pathlib import Path
|
|
27
|
+
from shutil import which
|
|
25
28
|
from subprocess import TimeoutExpired, run
|
|
26
|
-
from typing import Generator, Sequence
|
|
27
29
|
|
|
28
30
|
import yaml
|
|
29
31
|
from boltons.iterutils import flatten
|
|
30
32
|
from boltons.strutils import strip_ansi
|
|
31
|
-
from click_extra.testing import
|
|
32
|
-
|
|
33
|
+
from click_extra.testing import (
|
|
34
|
+
args_cleanup,
|
|
35
|
+
regex_fullmatch_line_by_line,
|
|
36
|
+
render_cli_run,
|
|
37
|
+
)
|
|
38
|
+
from extra_platforms import Group, current_os
|
|
39
|
+
|
|
40
|
+
TYPE_CHECKING = False
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from collections.abc import Generator
|
|
43
|
+
|
|
44
|
+
from extra_platforms._types import _TNestedReferences
|
|
33
45
|
|
|
34
46
|
|
|
35
47
|
class SkippedTest(Exception):
|
|
@@ -38,6 +50,20 @@ class SkippedTest(Exception):
|
|
|
38
50
|
pass
|
|
39
51
|
|
|
40
52
|
|
|
53
|
+
def _split_args(cli: str) -> list[str]:
|
|
54
|
+
"""Split a string or sequence of strings into a tuple of arguments.
|
|
55
|
+
|
|
56
|
+
.. todo::
|
|
57
|
+
Evaluate better Windows CLI parsing with:
|
|
58
|
+
`w32lex <https://github.com/maxpat78/w32lex>`_.
|
|
59
|
+
"""
|
|
60
|
+
if sys.platform == "win32":
|
|
61
|
+
return cli.split()
|
|
62
|
+
# For Unix platforms, we have the dedicated shlex module.
|
|
63
|
+
else:
|
|
64
|
+
return shlex.split(cli)
|
|
65
|
+
|
|
66
|
+
|
|
41
67
|
@dataclass(order=True)
|
|
42
68
|
class CLITestCase:
|
|
43
69
|
cli_parameters: tuple[str, ...] | str = field(default_factory=tuple)
|
|
@@ -102,13 +128,7 @@ class CLITestCase:
|
|
|
102
128
|
# CLI parameters provided as a long string needs to be split so
|
|
103
129
|
# that each argument is a separate item in the final tuple.
|
|
104
130
|
if field_id == "cli_parameters":
|
|
105
|
-
|
|
106
|
-
# https://github.com/maxpat78/w32lex
|
|
107
|
-
if sys.platform == "win32":
|
|
108
|
-
field_data = field_data.split()
|
|
109
|
-
# For Unix platforms, we have the dedicated shlex module.
|
|
110
|
-
else:
|
|
111
|
-
field_data = shlex.split(field_data)
|
|
131
|
+
field_data = _split_args(field_data)
|
|
112
132
|
else:
|
|
113
133
|
field_data = (field_data,)
|
|
114
134
|
|
|
@@ -147,16 +167,22 @@ class CLITestCase:
|
|
|
147
167
|
|
|
148
168
|
def run_cli_test(
|
|
149
169
|
self,
|
|
150
|
-
|
|
170
|
+
command: Path | str,
|
|
151
171
|
additional_skip_platforms: _TNestedReferences | None,
|
|
152
172
|
default_timeout: float | None,
|
|
153
173
|
):
|
|
154
174
|
"""Run a CLI command and check its output against the test case.
|
|
155
175
|
|
|
156
|
-
|
|
176
|
+
The provided ``command`` can be either:
|
|
177
|
+
|
|
178
|
+
- a path to a binary or script to execute;
|
|
179
|
+
- a command name to be searched in the ``PATH``,
|
|
180
|
+
- a command line with arguments to be parsed and executed by the shell.
|
|
181
|
+
|
|
182
|
+
.. todo::
|
|
157
183
|
Add support for environment variables.
|
|
158
184
|
|
|
159
|
-
..todo::
|
|
185
|
+
.. todo::
|
|
160
186
|
Add support for proper mixed <stdout>/<stderr> stream as a single,
|
|
161
187
|
intertwined output.
|
|
162
188
|
"""
|
|
@@ -173,7 +199,28 @@ class CLITestCase:
|
|
|
173
199
|
logging.info(f"Set default test case timeout to {default_timeout} seconds")
|
|
174
200
|
self.timeout = default_timeout
|
|
175
201
|
|
|
176
|
-
|
|
202
|
+
# Separate the command into binary file path and arguments.
|
|
203
|
+
args = []
|
|
204
|
+
if isinstance(command, str):
|
|
205
|
+
args = _split_args(command)
|
|
206
|
+
command = args[0]
|
|
207
|
+
args = args[1:]
|
|
208
|
+
# Ensure the command to execute is in PATH.
|
|
209
|
+
if not which(command):
|
|
210
|
+
raise FileNotFoundError(f"Command not found in PATH: {command!r}")
|
|
211
|
+
# Resolve the command to an absolute path.
|
|
212
|
+
command = which(command) # type: ignore[assignment]
|
|
213
|
+
assert command is not None
|
|
214
|
+
|
|
215
|
+
# Check the binary exists and is executable.
|
|
216
|
+
binary = Path(command).resolve()
|
|
217
|
+
assert binary.exists()
|
|
218
|
+
assert binary.is_file()
|
|
219
|
+
assert os.access(binary, os.X_OK)
|
|
220
|
+
|
|
221
|
+
clean_args = args_cleanup(binary, args, self.cli_parameters)
|
|
222
|
+
logging.info(f"Run CLI command: {' '.join(clean_args)}")
|
|
223
|
+
|
|
177
224
|
try:
|
|
178
225
|
result = run(
|
|
179
226
|
clean_args,
|
|
@@ -265,9 +312,7 @@ class CLITestCase:
|
|
|
265
312
|
raise AssertionError(f"{name} does not match regex {regex}")
|
|
266
313
|
|
|
267
314
|
elif field_id.endswith("_regex_fullmatch"):
|
|
268
|
-
|
|
269
|
-
if not regex.fullmatch(output):
|
|
270
|
-
raise AssertionError(f"{name} does not fully match regex {regex}")
|
|
315
|
+
regex_fullmatch_line_by_line(field_data, output)
|
|
271
316
|
|
|
272
317
|
|
|
273
318
|
DEFAULT_TEST_PLAN: list[CLITestCase] = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gha-utils
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.23.0
|
|
4
4
|
Summary: ⚙️ CLI helpers for GitHub Actions + reuseable workflows
|
|
5
5
|
Author-email: Kevin Deldycke <kevin@deldycke.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/kdeldycke/workflows
|
|
@@ -48,18 +48,18 @@ Classifier: Topic :: Utilities
|
|
|
48
48
|
Classifier: Typing :: Typed
|
|
49
49
|
Requires-Python: >=3.11
|
|
50
50
|
Description-Content-Type: text/markdown
|
|
51
|
-
Requires-Dist: boltons>=
|
|
51
|
+
Requires-Dist: boltons>=25.0.0
|
|
52
52
|
Requires-Dist: bump-my-version<1.1.1,>=0.32.2
|
|
53
|
-
Requires-Dist: click-extra
|
|
54
|
-
Requires-Dist: extra-platforms
|
|
55
|
-
Requires-Dist: packaging
|
|
56
|
-
Requires-Dist: py-walk
|
|
57
|
-
Requires-Dist: PyDriller
|
|
58
|
-
Requires-Dist: pyproject-metadata
|
|
59
|
-
Requires-Dist: pyyaml
|
|
60
|
-
Requires-Dist: wcmatch>=
|
|
53
|
+
Requires-Dist: click-extra>=6.0.3
|
|
54
|
+
Requires-Dist: extra-platforms>=4.0.0
|
|
55
|
+
Requires-Dist: packaging>=25.0
|
|
56
|
+
Requires-Dist: py-walk>=0.3.3
|
|
57
|
+
Requires-Dist: PyDriller>=2.6
|
|
58
|
+
Requires-Dist: pyproject-metadata>=0.9.0
|
|
59
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
60
|
+
Requires-Dist: wcmatch>=10.0
|
|
61
61
|
Provides-Extra: test
|
|
62
|
-
Requires-Dist: coverage[toml]~=7.
|
|
62
|
+
Requires-Dist: coverage[toml]~=7.11.0; extra == "test"
|
|
63
63
|
Requires-Dist: pytest~=8.4.0; extra == "test"
|
|
64
64
|
Requires-Dist: pytest-cases~=3.9.1; extra == "test"
|
|
65
65
|
Requires-Dist: pytest-cov~=7.0.0; extra == "test"
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
boltons>=
|
|
1
|
+
boltons>=25.0.0
|
|
2
2
|
bump-my-version<1.1.1,>=0.32.2
|
|
3
|
-
click-extra
|
|
4
|
-
extra-platforms
|
|
5
|
-
packaging
|
|
6
|
-
py-walk
|
|
7
|
-
PyDriller
|
|
8
|
-
pyproject-metadata
|
|
9
|
-
pyyaml
|
|
10
|
-
wcmatch>=
|
|
3
|
+
click-extra>=6.0.3
|
|
4
|
+
extra-platforms>=4.0.0
|
|
5
|
+
packaging>=25.0
|
|
6
|
+
py-walk>=0.3.3
|
|
7
|
+
PyDriller>=2.6
|
|
8
|
+
pyproject-metadata>=0.9.0
|
|
9
|
+
pyyaml>=6.0.3
|
|
10
|
+
wcmatch>=10.0
|
|
11
11
|
|
|
12
12
|
[test]
|
|
13
|
-
coverage[toml]~=7.
|
|
13
|
+
coverage[toml]~=7.11.0
|
|
14
14
|
pytest~=8.4.0
|
|
15
15
|
pytest-cases~=3.9.1
|
|
16
16
|
pytest-cov~=7.0.0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
# Docs: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
|
|
3
3
|
name = "gha-utils"
|
|
4
|
-
version = "4.
|
|
4
|
+
version = "4.23.0"
|
|
5
5
|
# Python versions and their status: https://devguide.python.org/versions/
|
|
6
6
|
requires-python = ">= 3.11"
|
|
7
7
|
description = "⚙️ CLI helpers for GitHub Actions + reuseable workflows"
|
|
@@ -70,31 +70,45 @@ classifiers = [
|
|
|
70
70
|
'Topic :: Utilities',
|
|
71
71
|
'Typing :: Typed',
|
|
72
72
|
]
|
|
73
|
+
# Semantic versioning works in theory. In practice, it is hard to guarantee.
|
|
74
|
+
# That's why we rely on >= specifier instead of ~= to relax constraints. This gives more freedom to packagers to
|
|
75
|
+
# release hotfixes for security vulnerabilities in the future.
|
|
76
|
+
# See also discussion about upper limits at: https://iscinumpy.dev/post/bound-version-constraints/#tldr
|
|
77
|
+
# All minimal version choice are documented. Reasons to bump a minimal version are:
|
|
78
|
+
# - bug fixes,
|
|
79
|
+
# - security fixes,
|
|
80
|
+
# - new code path we depends on,
|
|
81
|
+
# - aligns minimal Python requirements to ours,
|
|
82
|
+
# - adds new explicit support Python version.
|
|
73
83
|
dependencies = [
|
|
74
|
-
#
|
|
75
|
-
"boltons >=
|
|
76
|
-
# Dependency version is more relaxed on bump-my-version to prevent chicken and egg
|
|
77
|
-
# while releasing gha-utils itself.
|
|
84
|
+
# boltons 25.0.0 is the first version to support Python 3.13.
|
|
85
|
+
"boltons >= 25.0.0",
|
|
78
86
|
# v0.32.2 is the first fixing an issue preventing compilation with Nuitka.
|
|
79
87
|
# v1.1.1 and later have some regressions: see https://github.com/callowayproject/bump-my-version/issues/331
|
|
80
88
|
"bump-my-version >= 0.32.2, < 1.1.1",
|
|
81
|
-
# Click Extra 6.0.
|
|
82
|
-
"click-extra
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
#
|
|
89
|
+
# Click Extra 6.0.3 fix the regex_fullmatch_line_by_line() function we rely on.
|
|
90
|
+
"click-extra >= 6.0.3",
|
|
91
|
+
# Extra Platforms 4.0.0 is the first one to support Python 3.14.
|
|
92
|
+
"extra-platforms >= 4.0.0",
|
|
93
|
+
# v25.0 is the first version we used when we last changed the requirement.
|
|
94
|
+
"packaging >= 25.0",
|
|
95
|
+
# v0.3.3 is the first version we used.
|
|
96
|
+
# XXX In the future, replace py-walk with wcmatch once the latter supports gitignore files:
|
|
86
97
|
# https://github.com/facelessuser/wcmatch/issues/226
|
|
87
|
-
"py-walk
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
"py-walk >= 0.3.3",
|
|
99
|
+
# PyDriller 2.6.0 is the first version we used when we last changed the requirement.
|
|
100
|
+
"PyDriller >= 2.6",
|
|
101
|
+
# v0.9.0 supports the latest metadata specifications.
|
|
102
|
+
"pyproject-metadata >= 0.9.0",
|
|
103
|
+
# PyYAML 6.0.3 is the first version to support Python 3.14.
|
|
104
|
+
"pyyaml >= 6.0.3",
|
|
105
|
+
# wcmatch 10.0 fix some inconsistencies in globbing and symlinks.
|
|
106
|
+
"wcmatch >= 10.0",
|
|
93
107
|
]
|
|
94
108
|
|
|
95
109
|
[project.optional-dependencies]
|
|
96
110
|
test = [
|
|
97
|
-
"coverage [toml] ~= 7.
|
|
111
|
+
"coverage [toml] ~= 7.11.0",
|
|
98
112
|
"pytest ~= 8.4.0",
|
|
99
113
|
"pytest-cases ~= 3.9.1",
|
|
100
114
|
"pytest-cov ~= 7.0.0",
|
|
@@ -146,7 +160,7 @@ addopts = [
|
|
|
146
160
|
xfail_strict = true
|
|
147
161
|
|
|
148
162
|
[tool.bumpversion]
|
|
149
|
-
current_version = "4.
|
|
163
|
+
current_version = "4.23.0"
|
|
150
164
|
allow_dirty = true
|
|
151
165
|
ignore_missing_files = true
|
|
152
166
|
|
|
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
|