lib-cli-exit-tools 2.2.4__tar.gz → 2.3.1__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.
Files changed (83) hide show
  1. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.codacy/cli.sh +27 -25
  2. lib_cli_exit_tools-2.3.1/.data_arch_violations.json +38 -0
  3. lib_cli_exit_tools-2.3.1/.github/actions/extract-metadata/action.yml +92 -0
  4. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.github/workflows/default_cicd_public.yml +150 -67
  5. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.github/workflows/default_release_public.yml +2 -2
  6. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.gitignore +3 -0
  7. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/CHANGELOG.md +28 -0
  8. lib_cli_exit_tools-2.3.1/Makefile +260 -0
  9. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/PKG-INFO +18 -14
  10. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/README.md +0 -1
  11. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/pyproject.toml +50 -40
  12. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/__init__.py +1 -0
  13. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/__init__conf__.py +1 -1
  14. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/application/runner.py +21 -29
  15. lib_cli_exit_tools-2.3.1/src/lib_cli_exit_tools/cli/__init__.py +27 -0
  16. lib_cli_exit_tools-2.3.1/src/lib_cli_exit_tools/cli/commands.py +69 -0
  17. lib_cli_exit_tools-2.3.1/src/lib_cli_exit_tools/cli/group.py +123 -0
  18. lib_cli_exit_tools-2.3.1/src/lib_cli_exit_tools/cli/styling.py +111 -0
  19. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/core/configuration.py +22 -6
  20. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/core/exit_codes.py +2 -2
  21. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/lib_cli_exit_tools.py +2 -1
  22. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/conftest.py +2 -1
  23. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/test_cli.py +27 -19
  24. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/test_core_configuration.py +5 -4
  25. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/test_core_exit_codes.py +2 -1
  26. lib_cli_exit_tools-2.2.4/.github/workflows/ci.yml_backup +0 -208
  27. lib_cli_exit_tools-2.2.4/.github/workflows/release.yml_backup +0 -50
  28. lib_cli_exit_tools-2.2.4/CLAUDE.md +0 -164
  29. lib_cli_exit_tools-2.2.4/Makefile +0 -60
  30. lib_cli_exit_tools-2.2.4/scripts/__init__.py +0 -3
  31. lib_cli_exit_tools-2.2.4/scripts/__main__.py +0 -7
  32. lib_cli_exit_tools-2.2.4/scripts/_utils.py +0 -768
  33. lib_cli_exit_tools-2.2.4/scripts/build.py +0 -52
  34. lib_cli_exit_tools-2.2.4/scripts/bump.py +0 -32
  35. lib_cli_exit_tools-2.2.4/scripts/bump_major.py +0 -20
  36. lib_cli_exit_tools-2.2.4/scripts/bump_minor.py +0 -20
  37. lib_cli_exit_tools-2.2.4/scripts/bump_patch.py +0 -20
  38. lib_cli_exit_tools-2.2.4/scripts/bump_version.py +0 -80
  39. lib_cli_exit_tools-2.2.4/scripts/clean.py +0 -75
  40. lib_cli_exit_tools-2.2.4/scripts/cli.py +0 -232
  41. lib_cli_exit_tools-2.2.4/scripts/dependencies.py +0 -590
  42. lib_cli_exit_tools-2.2.4/scripts/dev.py +0 -19
  43. lib_cli_exit_tools-2.2.4/scripts/help.py +0 -41
  44. lib_cli_exit_tools-2.2.4/scripts/install.py +0 -19
  45. lib_cli_exit_tools-2.2.4/scripts/menu.py +0 -582
  46. lib_cli_exit_tools-2.2.4/scripts/push.py +0 -309
  47. lib_cli_exit_tools-2.2.4/scripts/release.py +0 -92
  48. lib_cli_exit_tools-2.2.4/scripts/run_cli.py +0 -154
  49. lib_cli_exit_tools-2.2.4/scripts/target_metadata.py +0 -202
  50. lib_cli_exit_tools-2.2.4/scripts/test.py +0 -1004
  51. lib_cli_exit_tools-2.2.4/scripts/toml_config.py +0 -433
  52. lib_cli_exit_tools-2.2.4/scripts/version_current.py +0 -20
  53. lib_cli_exit_tools-2.2.4/src/lib_cli_exit_tools/cli.py +0 -261
  54. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.codacy/cli-config.yaml +0 -0
  55. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.codacy/codacy.yaml +0 -0
  56. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.codacy/tools-configs/lizard.yaml +0 -0
  57. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.codacy/tools-configs/trivy.yaml +0 -0
  58. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.devcontainer/devcontainer.json +0 -0
  59. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.devcontainer/settings.json +0 -0
  60. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.env.example +0 -0
  61. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.github/dependabot.yml +0 -0
  62. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.github/workflows/codeql.yml +0 -0
  63. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.qlty/qlty.toml +0 -0
  64. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/.snyk +0 -0
  65. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/CONTRIBUTING.md +0 -0
  66. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/DEVELOPMENT.md +0 -0
  67. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/INSTALL.md +0 -0
  68. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/LICENSE +0 -0
  69. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/codecov.yml +0 -0
  70. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/docs/system-design/module_reference.md +0 -0
  71. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/notebooks/Quickstart.ipynb +0 -0
  72. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/__main__.py +0 -0
  73. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/adapters/__init__.py +0 -0
  74. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/adapters/signals.py +0 -0
  75. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/application/__init__.py +0 -0
  76. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/core/__init__.py +0 -0
  77. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/src/lib_cli_exit_tools/py.typed +0 -0
  78. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/test_adapters_signals.py +0 -0
  79. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/test_application_runner.py +0 -0
  80. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/test_behaviors.py +0 -0
  81. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/test_metadata.py +0 -0
  82. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/test_module_entry.py +0 -0
  83. {lib_cli_exit_tools-2.2.4 → lib_cli_exit_tools-2.3.1}/tests/test_signals_integration.py +0 -0
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
-
4
3
  set -e +o pipefail
5
4
 
6
5
  # Set up paths first
@@ -11,15 +10,15 @@ os_name=$(uname)
11
10
  arch=$(uname -m)
12
11
 
13
12
  case "$arch" in
14
- "x86_64")
15
- arch="amd64"
16
- ;;
17
- "x86")
18
- arch="386"
19
- ;;
20
- "aarch64"|"arm64")
21
- arch="arm64"
22
- ;;
13
+ "x86_64")
14
+ arch="amd64"
15
+ ;;
16
+ "x86")
17
+ arch="386"
18
+ ;;
19
+ "aarch64" | "arm64")
20
+ arch="arm64"
21
+ ;;
23
22
  esac
24
23
 
25
24
  if [ -z "$CODACY_CLI_V2_TMP_FOLDER" ]; then
@@ -34,10 +33,10 @@ fi
34
33
 
35
34
  version_file="$CODACY_CLI_V2_TMP_FOLDER/version.yaml"
36
35
 
37
-
38
36
  get_version_from_yaml() {
39
37
  if [ -f "$version_file" ]; then
40
- local version=$(grep -o 'version: *"[^"]*"' "$version_file" | cut -d'"' -f2)
38
+ local version
39
+ version=$(grep -o 'version: *"[^"]*"' "$version_file" | cut -d'"' -f2)
41
40
  if [ -n "$version" ]; then
42
41
  echo "$version"
43
42
  return 0
@@ -49,20 +48,24 @@ get_version_from_yaml() {
49
48
  get_latest_version() {
50
49
  local response
51
50
  if [ -n "$GH_TOKEN" ]; then
52
- response=$(curl -Lq --header "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
51
+ response=$(curl -Lq \
52
+ --header "Authorization: Bearer $GH_TOKEN" \
53
+ "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
53
54
  else
54
- response=$(curl -Lq "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
55
+ response=$(curl -Lq \
56
+ "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
55
57
  fi
56
58
 
57
59
  handle_rate_limit "$response"
58
- local version=$(echo "$response" | grep -m 1 tag_name | cut -d'"' -f4)
60
+ local version
61
+ version=$(echo "$response" | grep -m 1 tag_name | cut -d'"' -f4)
59
62
  echo "$version"
60
63
  }
61
64
 
62
65
  handle_rate_limit() {
63
66
  local response="$1"
64
67
  if echo "$response" | grep -q "API rate limit exceeded"; then
65
- fatal "Error: GitHub API rate limit exceeded. Please try again later"
68
+ fatal "Error: GitHub API rate limit exceeded. Please try again later"
66
69
  fi
67
70
  }
68
71
 
@@ -70,9 +73,9 @@ download_file() {
70
73
  local url="$1"
71
74
 
72
75
  echo "Downloading from URL: ${url}"
73
- if command -v curl > /dev/null 2>&1; then
76
+ if command -v curl >/dev/null 2>&1; then
74
77
  curl -# -LS "$url" -O
75
- elif command -v wget > /dev/null 2>&1; then
78
+ elif command -v wget >/dev/null 2>&1; then
76
79
  wget "$url"
77
80
  else
78
81
  fatal "Error: Could not find curl or wget, please install one."
@@ -83,7 +86,7 @@ download() {
83
86
  local url="$1"
84
87
  local output_folder="$2"
85
88
 
86
- ( cd "$output_folder" && download_file "$url" )
89
+ (cd "$output_folder" && download_file "$url")
87
90
  }
88
91
 
89
92
  download_cli() {
@@ -95,7 +98,7 @@ download_cli() {
95
98
  local version="$3"
96
99
 
97
100
  if [ ! -f "$bin_path" ]; then
98
- echo "📥 Downloading CLI version $version..."
101
+ echo "Downloading CLI version $version..."
99
102
 
100
103
  remote_file="codacy-cli-v2_${version}_${suffix}_${arch}.tar.gz"
101
104
  url="https://github.com/codacy/codacy-cli-v2/releases/download/${version}/${remote_file}"
@@ -107,16 +110,16 @@ download_cli() {
107
110
 
108
111
  # Warn if CODACY_CLI_V2_VERSION is set and update is requested
109
112
  if [ -n "$CODACY_CLI_V2_VERSION" ] && [ "$1" = "update" ]; then
110
- echo "⚠️ Warning: Performing update with forced version $CODACY_CLI_V2_VERSION"
113
+ echo "Warning: Performing update with forced version $CODACY_CLI_V2_VERSION"
111
114
  echo " Unset CODACY_CLI_V2_VERSION to use the latest version"
112
115
  fi
113
116
 
114
117
  # Ensure version.yaml exists and is up to date
115
118
  if [ ! -f "$version_file" ] || [ "$1" = "update" ]; then
116
- echo "ℹ️ Fetching latest version..."
119
+ echo "Fetching latest version..."
117
120
  version=$(get_latest_version)
118
121
  mkdir -p "$CODACY_CLI_V2_TMP_FOLDER"
119
- echo "version: \"$version\"" > "$version_file"
122
+ echo "version: \"$version\"" >"$version_file"
120
123
  fi
121
124
 
122
125
  # Set the version to use
@@ -126,7 +129,6 @@ else
126
129
  version=$(get_version_from_yaml)
127
130
  fi
128
131
 
129
-
130
132
  # Set up version-specific paths
131
133
  bin_folder="${CODACY_CLI_V2_TMP_FOLDER}/${version}"
132
134
 
@@ -146,4 +148,4 @@ if [ "$#" -eq 1 ] && [ "$1" = "download" ]; then
146
148
  echo "Codacy cli v2 download succeeded"
147
149
  else
148
150
  eval "$run_command $*"
149
- fi
151
+ fi
@@ -0,0 +1,38 @@
1
+ {
2
+ "pass": 1,
3
+ "total_violations": 0,
4
+ "files": {
5
+ "src/lib_cli_exit_tools/core/configuration.py": {
6
+ "violations": [],
7
+ "status": "clean"
8
+ },
9
+ "src/lib_cli_exit_tools/core/exit_codes.py": {
10
+ "violations": [],
11
+ "status": "clean"
12
+ },
13
+ "src/lib_cli_exit_tools/application/runner.py": {
14
+ "violations": [],
15
+ "status": "clean"
16
+ },
17
+ "src/lib_cli_exit_tools/__init__.py": {
18
+ "violations": [],
19
+ "status": "clean"
20
+ },
21
+ "src/lib_cli_exit_tools/__init__conf__.py": {
22
+ "violations": [],
23
+ "status": "clean"
24
+ },
25
+ "src/lib_cli_exit_tools/cli.py": {
26
+ "violations": [],
27
+ "status": "clean"
28
+ },
29
+ "src/lib_cli_exit_tools/lib_cli_exit_tools.py": {
30
+ "violations": [],
31
+ "status": "clean"
32
+ },
33
+ "src/lib_cli_exit_tools/adapters/signals.py": {
34
+ "violations": [],
35
+ "status": "clean"
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,92 @@
1
+ name: Extract project metadata
2
+ description: >-
3
+ Parse pyproject.toml and expose project metadata as step outputs.
4
+ Requires Python to be available in the runner environment.
5
+ Auto-installs rtoml if not already present.
6
+
7
+ outputs:
8
+ PACKAGE_MODULE:
9
+ description: Python-importable module name (underscored)
10
+ value: ${{ steps.meta.outputs.PACKAGE_MODULE }}
11
+ PROJECT_NAME:
12
+ description: Project name from pyproject.toml
13
+ value: ${{ steps.meta.outputs.PROJECT_NAME }}
14
+ CLI_BIN:
15
+ description: CLI entry-point binary name
16
+ value: ${{ steps.meta.outputs.CLI_BIN }}
17
+ SRC_PATH:
18
+ description: Source directory path (e.g. "src")
19
+ value: ${{ steps.meta.outputs.SRC_PATH }}
20
+ COV_FAIL_UNDER:
21
+ description: Minimum coverage percentage from tool.coverage.report.fail_under
22
+ value: ${{ steps.meta.outputs.COV_FAIL_UNDER }}
23
+ COV_REPORT_FILE:
24
+ description: Coverage report filename (e.g. "coverage.xml")
25
+ value: ${{ steps.meta.outputs.COV_REPORT_FILE }}
26
+ PYTEST_VERBOSITY:
27
+ description: Pytest verbosity flag (e.g. "-vv")
28
+ value: ${{ steps.meta.outputs.PYTEST_VERBOSITY }}
29
+ BANDIT_SKIP_FLAG:
30
+ description: Bandit skip flag string (e.g. "-s B101,B601") or empty
31
+ value: ${{ steps.meta.outputs.BANDIT_SKIP_FLAG }}
32
+
33
+ runs:
34
+ using: composite
35
+ steps:
36
+ - name: Extract project metadata from pyproject.toml
37
+ id: meta
38
+ shell: python
39
+ run: |
40
+ import os, subprocess, sys
41
+ try:
42
+ import rtoml
43
+ except ModuleNotFoundError:
44
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "rtoml", "-q"])
45
+ import rtoml
46
+
47
+ from pathlib import Path
48
+ data = rtoml.load(Path('pyproject.toml'))
49
+
50
+ # --- [project] section ---
51
+ project = data['project']['name']
52
+ module = project.replace('-', '_')
53
+ dash = project.replace('_', '-')
54
+ scripts = list(data['project'].get('scripts', {}).keys())
55
+ cli_bin = scripts[0] if scripts else dash
56
+
57
+ # --- [tool.scripts.test] section ---
58
+ scripts_test = data.get('tool', {}).get('scripts', {}).get('test', {})
59
+ src_path = scripts_test.get('src-path', 'src')
60
+ pytest_verbosity = scripts_test.get('pytest-verbosity', '-vv')
61
+ cov_report_file = scripts_test.get('coverage-report-file', 'coverage.xml')
62
+
63
+ # --- [tool.coverage.report] section ---
64
+ cov_fail_under = (
65
+ data.get('tool', {}).get('coverage', {}).get('report', {}).get('fail_under', 90)
66
+ )
67
+
68
+ # --- [tool.bandit] section ---
69
+ bandit_skips = data.get('tool', {}).get('bandit', {}).get('skips', [])
70
+ bandit_skip_flag = f"-s {','.join(bandit_skips)}" if bandit_skips else ""
71
+
72
+ # Write to GITHUB_OUTPUT for composite action outputs
73
+ with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as out:
74
+ out.write(f"PROJECT_NAME={project}\n")
75
+ out.write(f"PACKAGE_MODULE={module}\n")
76
+ out.write(f"CLI_BIN={cli_bin}\n")
77
+ out.write(f"SRC_PATH={src_path}\n")
78
+ out.write(f"PYTEST_VERBOSITY={pytest_verbosity}\n")
79
+ out.write(f"COV_REPORT_FILE={cov_report_file}\n")
80
+ out.write(f"COV_FAIL_UNDER={cov_fail_under}\n")
81
+ out.write(f"BANDIT_SKIP_FLAG={bandit_skip_flag}\n")
82
+
83
+ # Also write to GITHUB_ENV so subsequent steps can use ${{ env.VAR }}
84
+ with open(os.environ['GITHUB_ENV'], 'a', encoding='utf-8') as env:
85
+ env.write(f"PROJECT_NAME={project}\n")
86
+ env.write(f"PACKAGE_MODULE={module}\n")
87
+ env.write(f"CLI_BIN={cli_bin}\n")
88
+ env.write(f"SRC_PATH={src_path}\n")
89
+ env.write(f"PYTEST_VERBOSITY={pytest_verbosity}\n")
90
+ env.write(f"COV_REPORT_FILE={cov_report_file}\n")
91
+ env.write(f"COV_FAIL_UNDER={cov_fail_under}\n")
92
+ env.write(f"BANDIT_SKIP_FLAG={bandit_skip_flag}\n")
@@ -10,19 +10,61 @@ on:
10
10
  - cron: '17 3 * * *'
11
11
 
12
12
  jobs:
13
+ setup:
14
+ name: Extract Python versions and OS matrix from pyproject.toml
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ python-versions: ${{ steps.metadata.outputs.python-versions }}
18
+ python-latest: ${{ steps.metadata.outputs.python-latest }}
19
+ os-matrix: ${{ steps.metadata.outputs.os-matrix }}
20
+ steps:
21
+ - uses: actions/checkout@v6
22
+ - uses: actions/setup-python@v6
23
+ with:
24
+ python-version: "3.x"
25
+ - name: Parse pyproject.toml for Python versions and OS matrix
26
+ id: metadata
27
+ shell: python
28
+ run: |
29
+ import json, os, tomllib
30
+
31
+ with open("pyproject.toml", "rb") as f:
32
+ data = tomllib.load(f)
33
+
34
+ # Python versions from classifiers
35
+ classifiers = data.get("project", {}).get("classifiers", [])
36
+ prefix = "Programming Language :: Python :: "
37
+ versions = [
38
+ c[len(prefix):]
39
+ for c in classifiers
40
+ if c.startswith(prefix) and "." in c[len(prefix):]
41
+ ]
42
+ if not versions:
43
+ raise SystemExit("No Python X.Y versions found in classifiers")
44
+
45
+ # OS matrix from [tool.ci]
46
+ ci_config = data.get("tool", {}).get("ci", {})
47
+ os_list = ci_config.get("os", ["ubuntu-latest"])
48
+
49
+ with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
50
+ fh.write(f"python-versions={json.dumps(versions)}\n")
51
+ fh.write(f"python-latest={versions[-1]}\n")
52
+ fh.write(f"os-matrix={json.dumps(os_list)}\n")
53
+
13
54
  test:
14
55
  name: Tests (Python ${{ matrix.python }}, ${{ matrix.os }})
56
+ needs: setup
15
57
  runs-on: ${{ matrix.os }}
16
58
  strategy:
17
59
  fail-fast: false
18
60
  matrix:
19
- os: [ubuntu-latest, macos-latest, windows-latest]
20
- # Test against all supported Python versions (3.10+)
21
- python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
61
+ os: ${{ fromJSON(needs.setup.outputs.os-matrix) }}
62
+ python: ${{ fromJSON(needs.setup.outputs.python-versions) }}
22
63
  env:
23
64
  PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1"
24
65
  PYTHONIOENCODING: "utf-8"
25
66
  UV_LINK_MODE: "copy"
67
+ UV_BREAK_SYSTEM_PACKAGES: "1"
26
68
  steps:
27
69
  - uses: actions/checkout@v6
28
70
  - uses: actions/setup-python@v6
@@ -34,51 +76,98 @@ jobs:
34
76
  enable-cache: true
35
77
  prune-cache: false
36
78
 
37
- - name: Install tomli (Python < 3.11)
38
- shell: bash
39
- run: python -c "import tomllib" 2>/dev/null || uv pip install tomli --system
79
+ - name: Install bash 4+ on macOS
80
+ if: runner.os == 'macOS'
81
+ run: |
82
+ brew install bash
83
+ echo "$(brew --prefix)/bin" >> "$GITHUB_PATH"
40
84
 
41
85
  - name: Extract project metadata
86
+ id: metadata
87
+ uses: ./.github/actions/extract-metadata
88
+
89
+ - name: Install CI system dependencies (Ubuntu)
90
+ if: runner.os == 'Linux'
42
91
  shell: python
43
92
  run: |
44
- import os
93
+ import subprocess
45
94
  try:
46
95
  import tomllib
47
96
  except ModuleNotFoundError:
48
- import tomli as tomllib
49
-
50
- with open('pyproject.toml', 'rb') as f:
97
+ import pip._vendor.tomli as tomllib
98
+ with open("pyproject.toml", "rb") as f:
51
99
  data = tomllib.load(f)
52
- project = data['project']['name']
53
- module = project.replace('-', '_')
54
- dash = project.replace('_', '-')
55
- scripts = list(data['project'].get('scripts', {}).keys())
56
- cli_bin = scripts[0] if scripts else dash
57
-
58
- with open(os.environ['GITHUB_ENV'], 'a', encoding='utf-8') as env:
59
- env.write(f"PROJECT_NAME={project}\n")
60
- env.write(f"PACKAGE_MODULE={module}\n")
61
- env.write(f"CLI_BIN={cli_bin}\n")
62
-
63
- - name: Install journald prerequisites
64
- if: runner.os == 'Linux'
65
- shell: bash
100
+ deps = data.get("tool", {}).get("ci", {}).get("system-dependencies-ubuntu", [])
101
+ if deps:
102
+ print(f"Installing system packages: {' '.join(deps)}")
103
+ subprocess.check_call(["sudo", "apt-get", "update"])
104
+ subprocess.check_call(["sudo", "apt-get", "install", "-y", *deps])
105
+ else:
106
+ print("No CI system dependencies configured for Ubuntu.")
107
+
108
+ - name: Install CI system dependencies (macOS)
109
+ if: runner.os == 'macOS'
110
+ shell: python
66
111
  run: |
67
- sudo apt-get update
68
- sudo apt-get install -y python3-systemd
112
+ import subprocess
113
+ try:
114
+ import tomllib
115
+ except ModuleNotFoundError:
116
+ import pip._vendor.tomli as tomllib
117
+ with open("pyproject.toml", "rb") as f:
118
+ data = tomllib.load(f)
119
+ deps = data.get("tool", {}).get("ci", {}).get("system-dependencies-macos", [])
120
+ if deps:
121
+ print(f"Installing system packages: {' '.join(deps)}")
122
+ subprocess.check_call(["brew", "install", *deps])
123
+ else:
124
+ print("No CI system dependencies configured for macOS.")
69
125
 
70
126
  - name: Install dev deps
71
127
  shell: bash
72
128
  run: |
73
129
  uv pip install -e .[dev] --system
74
130
 
75
- - name: Install Windows Event Log prerequisites
131
+ - name: Install CI system dependencies (Windows/choco)
76
132
  if: runner.os == 'Windows'
77
- shell: bash
133
+ shell: python
78
134
  run: |
79
- uv pip install pywin32 --system
80
-
135
+ import subprocess
136
+ try:
137
+ import tomllib
138
+ except ModuleNotFoundError:
139
+ import pip._vendor.tomli as tomllib
140
+ with open("pyproject.toml", "rb") as f:
141
+ data = tomllib.load(f)
142
+ deps = data.get("tool", {}).get("ci", {}).get("system-dependencies-windows-choco", [])
143
+ if deps:
144
+ print(f"Installing choco packages: {' '.join(deps)}")
145
+ subprocess.check_call(["choco", "install", "-y", *deps])
146
+ else:
147
+ print("No CI choco dependencies configured for Windows.")
148
+
149
+ - name: Install CI pip dependencies (Windows)
150
+ if: runner.os == 'Windows'
151
+ shell: python
152
+ run: |
153
+ import subprocess
154
+ try:
155
+ import tomllib
156
+ except ModuleNotFoundError:
157
+ import pip._vendor.tomli as tomllib
158
+ with open("pyproject.toml", "rb") as f:
159
+ data = tomllib.load(f)
160
+ deps = data.get("tool", {}).get("ci", {}).get("system-dependencies-windows-pip", [])
161
+ if deps:
162
+ print(f"Installing pip packages: {' '.join(deps)}")
163
+ subprocess.check_call(["uv", "pip", "install", *deps, "--system"])
164
+ else:
165
+ print("No CI pip dependencies configured for Windows.")
166
+
167
+ # Self-hosted runners persist ruff cache via RUFF_CACHE_DIR volume mount,
168
+ # so actions/cache is only needed on GitHub-hosted runners.
81
169
  - name: Restore ruff cache
170
+ if: runner.environment == 'github-hosted'
82
171
  id: ruff-cache
83
172
  uses: actions/cache/restore@v5
84
173
  with:
@@ -93,8 +182,8 @@ jobs:
93
182
  continue-on-error: true
94
183
  shell: bash
95
184
  run: |
96
- ruff check src tests notebooks
97
- ruff format --check src tests notebooks
185
+ ruff check .
186
+ ruff format --check .
98
187
 
99
188
  - name: Run pyright (type check)
100
189
  id: pyright
@@ -108,7 +197,27 @@ jobs:
108
197
  shell: bash
109
198
  env:
110
199
  TEST_VERBOSE: "1"
111
- run: pytest --cov=src --cov-report=term-missing --cov-report=xml
200
+ # Write coverage SQLite database to local disk — network mounts
201
+ # and stale journal files cause "database is locked" errors.
202
+ COVERAGE_FILE: "${{ runner.temp }}/.coverage"
203
+ run: >-
204
+ pytest -m "not local_only"
205
+ --cov=${{ env.SRC_PATH }}/${{ env.PACKAGE_MODULE }}
206
+ --cov-report=term-missing
207
+ --cov-report=xml:${{ env.COV_REPORT_FILE }}
208
+ --cov-fail-under=${{ env.COV_FAIL_UNDER }}
209
+ ${{ env.PYTEST_VERBOSITY }}
210
+
211
+ - name: Upload coverage to Codecov
212
+ if: >-
213
+ always()
214
+ && steps.pytest.outcome == 'success'
215
+ && matrix.os == 'ubuntu-latest'
216
+ && matrix.python == needs.setup.outputs.python-latest
217
+ uses: codecov/codecov-action@v6
218
+ with:
219
+ files: ${{ env.COV_REPORT_FILE }}
220
+ fail_ci_if_error: false
112
221
 
113
222
  - name: Run import-linter (architecture)
114
223
  id: importlinter
@@ -120,7 +229,7 @@ jobs:
120
229
  id: bandit
121
230
  continue-on-error: true
122
231
  shell: bash
123
- run: bandit -q -r src/${{ env.PACKAGE_MODULE }}
232
+ run: bandit -q -c pyproject.toml -r ${{ env.SRC_PATH }}/${{ env.PACKAGE_MODULE }} ${{ env.BANDIT_SKIP_FLAG }}
124
233
 
125
234
  - name: Run pip-audit (vulnerability scan)
126
235
  id: pipaudit
@@ -128,12 +237,9 @@ jobs:
128
237
  shell: python
129
238
  run: |
130
239
  import subprocess, sys
131
- try:
132
- import tomllib
133
- except ModuleNotFoundError:
134
- import tomli as tomllib
135
- with open("pyproject.toml", "rb") as f:
136
- data = tomllib.load(f)
240
+ import rtoml
241
+ from pathlib import Path
242
+ data = rtoml.load(Path("pyproject.toml"))
137
243
  vulns = data.get("tool", {}).get("pip-audit", {}).get("ignore-vulns", [])
138
244
  cmd = ["pip-audit", "--skip-editable"]
139
245
  for v in vulns:
@@ -164,8 +270,7 @@ jobs:
164
270
  exit 1
165
271
  fi
166
272
  if [[ "${{ steps.pipaudit.outcome }}" != "success" ]]; then
167
- echo "::error::pip-audit vulnerability scan failed"
168
- exit 1
273
+ echo "::warning::pip-audit vulnerability scan failed"
169
274
  fi
170
275
  echo "All checks passed!"
171
276
 
@@ -187,7 +292,7 @@ jobs:
187
292
  run: uv cache prune --ci
188
293
 
189
294
  - name: Save ruff cache
190
- if: always() && steps.ruff-cache.outputs.cache-hit != 'true'
295
+ if: always() && runner.environment == 'github-hosted' && steps.ruff-cache.outputs.cache-hit != 'true'
191
296
  uses: actions/cache/save@v5
192
297
  with:
193
298
  path: .ruff_cache
@@ -208,31 +313,9 @@ jobs:
208
313
  enable-cache: true
209
314
  prune-cache: false
210
315
 
211
- - name: Install tomli (Python < 3.11)
212
- shell: bash
213
- run: python -c "import tomllib" 2>/dev/null || uv pip install tomli --system
214
-
215
316
  - name: Extract project metadata
216
- shell: python
217
- run: |
218
- import os
219
- try:
220
- import tomllib
221
- except ModuleNotFoundError:
222
- import tomli as tomllib
223
-
224
- with open('pyproject.toml', 'rb') as f:
225
- data = tomllib.load(f)
226
- project = data['project']['name']
227
- module = project.replace('-', '_')
228
- dash = project.replace('_', '-')
229
- scripts = list(data['project'].get('scripts', {}).keys())
230
- cli_bin = scripts[0] if scripts else dash
231
-
232
- with open(os.environ['GITHUB_ENV'], 'a', encoding='utf-8') as env:
233
- env.write(f"PROJECT_NAME={project}\n")
234
- env.write(f"PACKAGE_MODULE={module}\n")
235
- env.write(f"CLI_BIN={cli_bin}\n")
317
+ id: metadata
318
+ uses: ./.github/actions/extract-metadata
236
319
 
237
320
  - name: Build wheel
238
321
  run: uv build
@@ -31,7 +31,7 @@ jobs:
31
31
  run: uv build
32
32
 
33
33
  - name: Upload artifacts
34
- uses: actions/upload-artifact@v6
34
+ uses: actions/upload-artifact@v7
35
35
  with:
36
36
  name: dist
37
37
  path: dist/*
@@ -56,7 +56,7 @@ jobs:
56
56
  fi
57
57
 
58
58
  - name: Download artifacts
59
- uses: actions/download-artifact@v6
59
+ uses: actions/download-artifact@v8
60
60
  with:
61
61
  name: dist
62
62
  path: dist/
@@ -179,3 +179,6 @@ poetry.lock
179
179
  pdm.lock
180
180
  uv.lock
181
181
  /.github_bak/
182
+ prompts.md
183
+ CLAUDE.md
184
+ .claude/
@@ -2,12 +2,40 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [2.3.1] - 2026-04-24
8
+
9
+ ### Fixed
10
+ - Replaced `Iterator[...]` with `Generator[...]` in return annotations of `@contextmanager`-decorated functions (`config_overrides`, `cli_session`, `_apply_overrides_without_restore`, `_temporary_rich_click_configuration`) to satisfy pyright 1.1.409's new deprecation check that was breaking CI.
11
+
12
+ ### Security
13
+ - Refreshed pip-audit `ignore-vulns`: dropped entries whose packages are now at fixed versions; added dev/env-only exclusions for authlib (GHSA-jj8c-mmj3-mmgv), cryptography (CVE-2026-39892), lxml (CVE-2026-41066), Pillow (CVE-2026-40192), python-multipart (CVE-2026-40347), and uv (GHSA-pjjw-68hj-v9mw). None of these packages are runtime dependencies of `lib_cli_exit_tools`.
14
+
15
+ ### Changed
16
+ - Bumped dev dependency minimums in `pyproject.toml` and refreshed the BMK Makefile wrapper to 2.9.1 (persistent `uv tool install`).
17
+
18
+ ## [2.3.0] - 2026-02-13
19
+
20
+ ### Added
21
+ - `ExitCodeStyle` enum (`str, Enum`) replacing `Literal["errno", "sysexits"]` string literals, re-exported through the public API facade.
22
+ - New `cli/` package structure splitting the monolithic `cli.py` into `group.py`, `commands.py`, and `styling.py` with backward-compatible re-exports.
23
+
24
+ ### Changed
25
+ - Replaced `Mapping[str, object]` with typed `SessionOverrides` in `cli_session` and internal helpers for stricter type safety.
26
+ - Removed `_CONFIG_TRACEBACK` / `_CONFIG_TRACEBACK_FORCE_COLOR` string constants in favour of direct `SessionOverrides` key access.
27
+ - Migrated `scripts/` to external submodule; build automation now uses `uvx bmk`.
28
+ - Updated GitHub Actions CI/CD workflows and removed legacy backup workflow files.
29
+
30
+ ### Security
31
+ - Added CVE-2026-1703, CVE-2026-25990, and CVE-2026-26007 to pip-audit ignore list (transitive dev dependencies, not runtime).
5
32
 
6
33
  ## [2.2.4] - 2026-01-26
7
34
 
8
35
  ### Changed
9
36
  - Updated dependencies.
10
37
  - Excluded `scripts/` from pyright, pytest, and CI/CD test collection.
38
+ - Ctrl-C in scripts now exits with code 130 (POSIX SIGINT convention) instead of Click's default exit code 1.
11
39
 
12
40
  ## [2.2.3] - 2025-12-17
13
41