opsscript-gate 0.2.0__tar.gz → 0.2.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.

Potentially problematic release.


This version of opsscript-gate might be problematic. Click here for more details.

Files changed (36) hide show
  1. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/CHANGELOG.md +9 -0
  2. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/PKG-INFO +10 -2
  3. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/README.md +9 -1
  4. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/pyproject.toml +1 -1
  5. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/src/opsscript_gate/__init__.py +1 -1
  6. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/src/opsscript_gate/runner.py +17 -3
  7. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/tests/test_runner.py +57 -7
  8. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.gitattributes +0 -0
  9. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  10. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.github/ISSUE_TEMPLATE/distro_request.yml +0 -0
  11. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  12. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.github/assets/social-preview.png +0 -0
  13. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.github/pull_request_template.md +0 -0
  14. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.github/workflows/demo.yml +0 -0
  15. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.github/workflows/release.yml +0 -0
  16. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.github/workflows/test.yml +0 -0
  17. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/.gitignore +0 -0
  18. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/CONTRIBUTING.md +0 -0
  19. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/LICENSE +0 -0
  20. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/ROADMAP.md +0 -0
  21. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/SECURITY.md +0 -0
  22. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/action.yml +0 -0
  23. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/examples/alpine-incompatibility/README.md +0 -0
  24. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/examples/alpine-incompatibility/apt_dependency.sh +0 -0
  25. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/examples/basic/README.md +0 -0
  26. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/examples/basic/clean_setup.sh +0 -0
  27. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/examples/github-actions/README.md +0 -0
  28. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/examples/github-actions/workflow.yml +0 -0
  29. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/examples/interactive-hang/README.md +0 -0
  30. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/examples/interactive-hang/prompt_confirm.sh +0 -0
  31. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/src/opsscript_gate/cli.py +0 -0
  32. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/src/opsscript_gate/models.py +0 -0
  33. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/src/opsscript_gate/reporter.py +0 -0
  34. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/tests/fixtures/fail_deps.sh +0 -0
  35. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/tests/fixtures/fail_hang.sh +0 -0
  36. {opsscript_gate-0.2.0 → opsscript_gate-0.2.1}/tests/fixtures/pass_basic.sh +0 -0
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2026-09-19
9
+
10
+ ### Fixed
11
+ - Strip complete ANSI CSI and OSC escape sequences from diagnostic text instead of leaving escape-sequence residue.
12
+ - Ensure diagnostic `max_length` includes the trailing ellipsis when truncation occurs.
13
+ - Add regression coverage for ANSI sanitization and truncation edge cases.
14
+
15
+ ---
16
+
8
17
  ## [0.2.0] - 2026-09-18
9
18
 
10
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opsscript-gate
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Run Linux shell scripts across Debian, Ubuntu and Alpine before release to catch runtime compatibility failures
5
5
  Project-URL: Homepage, https://github.com/Mresyzz/opsscript-gate
6
6
  Project-URL: Repository, https://github.com/Mresyzz/opsscript-gate
@@ -55,7 +55,7 @@ Add one step to your pull request workflow (`.github/workflows/gate.yml`):
55
55
 
56
56
  ```yaml
57
57
  - name: Verify Shell Script Portability
58
- uses: Mresyzz/opsscript-gate@v0.2.0
58
+ uses: Mresyzz/opsscript-gate@v0.2.1
59
59
  with:
60
60
  script-path: scripts/setup.sh
61
61
  # shell: posix # optional: 'posix' (default), 'shebang', or 'auto'
@@ -217,6 +217,14 @@ Check out the [examples/](examples/) directory for self-contained, runnable scen
217
217
 
218
218
  ---
219
219
 
220
+ ## Dogfooding
221
+
222
+ OpsScript Gate is used in [`Mresyzz/linux-dev-bootstrap`](https://github.com/Mresyzz/linux-dev-bootstrap) to validate its `install.sh` script across the default Debian, Ubuntu, and Alpine matrix in GitHub Actions. That workflow pins `Mresyzz/opsscript-gate@v0.2.0` with `shell: auto`.
223
+
224
+ See the downstream workflow: [`.github/workflows/test.yml`](https://github.com/Mresyzz/linux-dev-bootstrap/blob/main/.github/workflows/test.yml).
225
+
226
+ ---
227
+
220
228
  ## Development & Testing
221
229
 
222
230
  The test suite uses Docker SDK mocking to ensure fast unit tests without needing a local daemon:
@@ -27,7 +27,7 @@ Add one step to your pull request workflow (`.github/workflows/gate.yml`):
27
27
 
28
28
  ```yaml
29
29
  - name: Verify Shell Script Portability
30
- uses: Mresyzz/opsscript-gate@v0.2.0
30
+ uses: Mresyzz/opsscript-gate@v0.2.1
31
31
  with:
32
32
  script-path: scripts/setup.sh
33
33
  # shell: posix # optional: 'posix' (default), 'shebang', or 'auto'
@@ -189,6 +189,14 @@ Check out the [examples/](examples/) directory for self-contained, runnable scen
189
189
 
190
190
  ---
191
191
 
192
+ ## Dogfooding
193
+
194
+ OpsScript Gate is used in [`Mresyzz/linux-dev-bootstrap`](https://github.com/Mresyzz/linux-dev-bootstrap) to validate its `install.sh` script across the default Debian, Ubuntu, and Alpine matrix in GitHub Actions. That workflow pins `Mresyzz/opsscript-gate@v0.2.0` with `shell: auto`.
195
+
196
+ See the downstream workflow: [`.github/workflows/test.yml`](https://github.com/Mresyzz/linux-dev-bootstrap/blob/main/.github/workflows/test.yml).
197
+
198
+ ---
199
+
192
200
  ## Development & Testing
193
201
 
194
202
  The test suite uses Docker SDK mocking to ensure fast unit tests without needing a local daemon:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "opsscript-gate"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Run Linux shell scripts across Debian, Ubuntu and Alpine before release to catch runtime compatibility failures"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,4 +1,4 @@
1
1
  """OpsScript Gate: Lightweight cross-distro compatibility pre-check for Linux ops scripts."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
4
4
  __all__ = ["__version__"]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import re
4
5
  import tempfile
5
6
  import time
6
7
  from typing import Sequence
@@ -35,21 +36,34 @@ SUPPORTED_SHEBANG_COMMANDS: dict[str, list[str]] = {
35
36
 
36
37
  DEFAULT_POSIX_COMMAND: list[str] = ["/bin/sh", "-c", "/bin/sh /tmp/target_script.sh </dev/null"]
37
38
 
39
+ # Regular expression matching common ANSI escape sequences:
40
+ # 1. CSI (Control Sequence Introducer): ESC [ ... [@-~]
41
+ # 2. OSC (Operating System Command): ESC ] ... (BEL | ST)
42
+ _ANSI_ESCAPE_RE = re.compile(
43
+ r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))"
44
+ )
45
+
38
46
 
39
47
  def sanitize_diagnostic_text(text: str, max_length: int = 200) -> str:
40
48
  """
41
49
  Sanitize raw shebang text for safe display in diagnostics and error messages:
50
+ - Strips complete ANSI escape sequences (CSI, OSC)
42
51
  - Removes control characters and embedded CR/LF
43
- - Truncates to max_length characters with an ellipsis if exceeded
52
+ - Collapses multiple consecutive whitespace
53
+ - Ensures maximum final length <= max_length (including ellipsis if truncated)
44
54
  """
45
- if not text:
55
+ if not text or max_length <= 0:
46
56
  return ""
57
+ # Strip complete ANSI escape sequences first
58
+ text = _ANSI_ESCAPE_RE.sub("", text)
47
59
  # Filter out control characters (ASCII < 32 and 127)
48
60
  sanitized = "".join(ch if (32 <= ord(ch) < 127 or ord(ch) >= 160) else " " for ch in text)
49
61
  # Collapse multiple consecutive whitespace
50
62
  sanitized = " ".join(sanitized.split())
51
63
  if len(sanitized) > max_length:
52
- return sanitized[:max_length] + "..."
64
+ if max_length < 3:
65
+ return sanitized[:max_length]
66
+ return sanitized[: max_length - 3] + "..."
53
67
  return sanitized
54
68
 
55
69
 
@@ -368,7 +368,7 @@ def test_cli_version(capsys):
368
368
  main(["--version"])
369
369
  assert excinfo.value.code == 0
370
370
  captured = capsys.readouterr()
371
- assert "0.2.0" in captured.out
371
+ assert "0.2.1" in captured.out
372
372
 
373
373
 
374
374
  def test_cli_format_markdown_and_table(tmp_path, capsys):
@@ -681,21 +681,71 @@ def test_run_matrix_parses_shebang_once(tmp_path):
681
681
  def test_sanitize_diagnostic_text():
682
682
  # Empty string
683
683
  assert sanitize_diagnostic_text("") == ""
684
- # Control characters, embedded CR/LF, ANSI escapes
684
+
685
+ # Control characters, embedded CR/LF, ANSI escape, BEL
685
686
  raw = "#!/bin/sh\r\n\x00\x1b[31mecho evil\x07"
686
687
  cleaned = sanitize_diagnostic_text(raw)
688
+ # ESC byte is absent
689
+ assert "\x1b" not in cleaned
690
+ # ANSI parameter residue such as "[31m" is also absent
691
+ assert "[31m" not in cleaned
687
692
  assert "\r" not in cleaned
688
693
  assert "\n" not in cleaned
689
694
  assert "\x00" not in cleaned
690
- assert "\x1b" not in cleaned
691
695
  assert "\x07" not in cleaned
692
- # Multiple whitespace collapsed
693
- assert " " not in cleaned
694
- # Truncation to max_length
696
+ assert cleaned == "#!/bin/sh echo evil"
697
+
698
+ # Extended CSI sequence (e.g. 256-color) is removed as a complete sequence
699
+ extended_csi = "prefix \x1b[38;5;196mcolor\x1b[0m suffix"
700
+ cleaned_csi = sanitize_diagnostic_text(extended_csi)
701
+ assert "\x1b" not in cleaned_csi
702
+ assert "[38;5;196m" not in cleaned_csi
703
+ assert "[0m" not in cleaned_csi
704
+ assert cleaned_csi == "prefix color suffix"
705
+
706
+ # Additional CSI sequences (\x1b[1;31m, \x1b[?25h, etc.)
707
+ csi_variations = "\x1b[1;31mboldred\x1b[0m \x1b[?25hcursor"
708
+ cleaned_variations = sanitize_diagnostic_text(csi_variations)
709
+ assert "[1;31m" not in cleaned_variations
710
+ assert "[?25h" not in cleaned_variations
711
+ assert cleaned_variations == "boldred cursor"
712
+
713
+ # OSC title sequences are removed cleanly
714
+ osc_bel = "\x1b]0;terminal title\x07echo hello"
715
+ cleaned_osc_bel = sanitize_diagnostic_text(osc_bel)
716
+ assert "terminal title" not in cleaned_osc_bel
717
+ assert cleaned_osc_bel == "echo hello"
718
+
719
+ osc_st = "\x1b]0;terminal title\x1b\\echo world"
720
+ cleaned_osc_st = sanitize_diagnostic_text(osc_st)
721
+ assert "terminal title" not in cleaned_osc_st
722
+ assert cleaned_osc_st == "echo world"
723
+
724
+ # Ordinary printable text and Unicode are preserved
725
+ normal_text = "echo 'Hello world! 12345 äöü'"
726
+ cleaned_normal = sanitize_diagnostic_text(normal_text)
727
+ assert cleaned_normal == normal_text
728
+
729
+ # Repeated whitespace is collapsed
730
+ whitespace_text = " a b \t c \n\r d "
731
+ assert sanitize_diagnostic_text(whitespace_text) == "a b c d"
732
+
733
+ # max_length=200 returns len(result) <= 200, ends in "..."
695
734
  long_text = "#!" + "a" * 250
696
735
  truncated = sanitize_diagnostic_text(long_text, max_length=200)
697
- assert len(truncated) == 203 # 200 chars + "..."
736
+ assert len(truncated) <= 200
737
+ assert len(truncated) == 200
698
738
  assert truncated.endswith("...")
739
+ assert truncated == "#!" + "a" * 195 + "..."
740
+
741
+ # Small/zero max_length values do not crash and remain defensively sized
742
+ assert sanitize_diagnostic_text("hello world", max_length=0) == ""
743
+ assert sanitize_diagnostic_text("hello world", max_length=-5) == ""
744
+ assert sanitize_diagnostic_text("hello world", max_length=1) == "h"
745
+ assert sanitize_diagnostic_text("hello world", max_length=2) == "he"
746
+ assert sanitize_diagnostic_text("hello world", max_length=3) == "..."
747
+ assert sanitize_diagnostic_text("hello world", max_length=4) == "h..."
748
+ assert len(sanitize_diagnostic_text("hello world", max_length=5)) <= 5
699
749
 
700
750
 
701
751
  def test_missing_interpreter_compatibility_failure(tmp_path):
File without changes