opsscript-gate 0.2.0__tar.gz → 0.3.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.
Files changed (37) hide show
  1. opsscript_gate-0.3.0/.github/workflows/release.yml +213 -0
  2. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/CHANGELOG.md +19 -0
  3. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/PKG-INFO +36 -19
  4. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/README.md +35 -18
  5. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/SECURITY.md +3 -1
  6. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/examples/github-actions/workflow.yml +2 -1
  7. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/pyproject.toml +1 -1
  8. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/src/opsscript_gate/__init__.py +1 -1
  9. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/src/opsscript_gate/models.py +19 -0
  10. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/src/opsscript_gate/reporter.py +13 -1
  11. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/src/opsscript_gate/runner.py +117 -4
  12. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/tests/test_runner.py +304 -8
  13. opsscript_gate-0.2.0/.github/workflows/release.yml +0 -121
  14. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/.gitattributes +0 -0
  15. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/.github/ISSUE_TEMPLATE/distro_request.yml +0 -0
  17. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  18. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/.github/assets/social-preview.png +0 -0
  19. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/.github/pull_request_template.md +0 -0
  20. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/.github/workflows/demo.yml +0 -0
  21. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/.github/workflows/test.yml +0 -0
  22. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/.gitignore +0 -0
  23. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/CONTRIBUTING.md +0 -0
  24. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/LICENSE +0 -0
  25. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/ROADMAP.md +0 -0
  26. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/action.yml +0 -0
  27. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/examples/alpine-incompatibility/README.md +0 -0
  28. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/examples/alpine-incompatibility/apt_dependency.sh +0 -0
  29. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/examples/basic/README.md +0 -0
  30. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/examples/basic/clean_setup.sh +0 -0
  31. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/examples/github-actions/README.md +0 -0
  32. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/examples/interactive-hang/README.md +0 -0
  33. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/examples/interactive-hang/prompt_confirm.sh +0 -0
  34. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/src/opsscript_gate/cli.py +0 -0
  35. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/tests/fixtures/fail_deps.sh +0 -0
  36. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/tests/fixtures/fail_hang.sh +0 -0
  37. {opsscript_gate-0.2.0 → opsscript_gate-0.3.0}/tests/fixtures/pass_basic.sh +0 -0
@@ -0,0 +1,213 @@
1
+ name: Build and Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ tags:
8
+ - 'v*'
9
+ workflow_dispatch:
10
+
11
+ concurrency:
12
+ group: opsscript-gate-release
13
+ cancel-in-progress: false
14
+ queue: max
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ test:
21
+ name: Run Test Suite
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - name: Checkout Code
25
+ uses: actions/checkout@v7
26
+
27
+ - name: Set up Python
28
+ uses: actions/setup-python@v7
29
+ with:
30
+ python-version: '3.12'
31
+
32
+ - name: Install Dependencies
33
+ run: |
34
+ python -m pip install --upgrade pip
35
+ pip install -e .[test]
36
+
37
+ - name: Run Tests
38
+ run: |
39
+ pytest -v -m "not integration"
40
+
41
+ integration:
42
+ name: Docker Integration Tests
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - name: Checkout Code
46
+ uses: actions/checkout@v7
47
+
48
+ - name: Set up Python
49
+ uses: actions/setup-python@v7
50
+ with:
51
+ python-version: '3.12'
52
+
53
+ - name: Install Dependencies
54
+ run: |
55
+ python -m pip install --upgrade pip
56
+ pip install -e .[test]
57
+
58
+ - name: Verify Docker Available
59
+ run: docker version
60
+
61
+ - name: Run Docker Integration Tests
62
+ run: |
63
+ pytest -v -m integration
64
+
65
+ build:
66
+ name: Build Distribution Packages
67
+ needs: [test, integration]
68
+ runs-on: ubuntu-latest
69
+ permissions:
70
+ contents: write
71
+ outputs:
72
+ is_new_release: ${{ steps.release-check.outputs.is_new_release }}
73
+ should_tag: ${{ steps.release-check.outputs.should_tag }}
74
+ release_tag: ${{ steps.release-check.outputs.release_tag }}
75
+ steps:
76
+ - name: Checkout Code
77
+ uses: actions/checkout@v7
78
+ with:
79
+ fetch-depth: 0
80
+
81
+ - name: Set up Python
82
+ uses: actions/setup-python@v7
83
+ with:
84
+ python-version: '3.12'
85
+
86
+ - name: Determine Release Readiness
87
+ id: release-check
88
+ shell: bash
89
+ run: |
90
+ set -euo pipefail
91
+
92
+ TOML_VERSION=$(python -c "import tomllib; f=open('pyproject.toml', 'rb'); print(tomllib.load(f)['project']['version'])")
93
+ INIT_VERSION=$(python -c "import sys; sys.path.insert(0, 'src'); import opsscript_gate; print(opsscript_gate.__version__)")
94
+
95
+ echo "pyproject.toml version: $TOML_VERSION"
96
+ echo "opsscript_gate version: $INIT_VERSION"
97
+
98
+ if [ "$TOML_VERSION" != "$INIT_VERSION" ]; then
99
+ echo "::error::Version mismatch: pyproject.toml ($TOML_VERSION) != __init__.py ($INIT_VERSION)"
100
+ exit 1
101
+ fi
102
+
103
+ TARGET_TAG="v$TOML_VERSION"
104
+ echo "Target release tag: $TARGET_TAG"
105
+ echo "Triggering ref: ${{ github.ref }}"
106
+ echo "Triggering SHA: ${{ github.sha }}"
107
+ echo "Triggering event: ${{ github.event_name }}"
108
+
109
+ IS_NEW_RELEASE="false"
110
+ SHOULD_TAG="false"
111
+
112
+ if [[ "${{ github.ref }}" =~ ^refs/tags/v ]]; then
113
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
114
+ echo "Triggered via explicit Git tag: $GITHUB_REF_NAME (version: $TAG_VERSION)"
115
+ if [ "$TAG_VERSION" != "$TOML_VERSION" ]; then
116
+ echo "::error::Tag mismatch: Git tag $GITHUB_REF_NAME does not match project version $TOML_VERSION"
117
+ exit 1
118
+ fi
119
+ IS_NEW_RELEASE="true"
120
+ SHOULD_TAG="false"
121
+ elif [ "${{ github.ref }}" = "refs/heads/main" ]; then
122
+ if git rev-parse -q --verify "refs/tags/$TARGET_TAG" >/dev/null; then
123
+ TAG_SHA=$(git rev-list -n 1 "$TARGET_TAG")
124
+ echo "Tag $TARGET_TAG already exists, pointing to commit: $TAG_SHA"
125
+ if [ "$TAG_SHA" = "${{ github.sha }}" ]; then
126
+ echo "Tag $TARGET_TAG points to current commit (${{ github.sha }}). Treating as retry/resume of release."
127
+ IS_NEW_RELEASE="true"
128
+ SHOULD_TAG="false"
129
+ else
130
+ echo "Tag $TARGET_TAG was released from earlier commit ($TAG_SHA != ${{ github.sha }}). Skipping release."
131
+ IS_NEW_RELEASE="false"
132
+ SHOULD_TAG="false"
133
+ fi
134
+ else
135
+ echo "Target tag $TARGET_TAG does not exist. Scheduling new release for commit ${{ github.sha }}."
136
+ IS_NEW_RELEASE="true"
137
+ SHOULD_TAG="true"
138
+ fi
139
+ else
140
+ echo "Workflow triggered on unsupported ref (${{ github.ref }}). Release is strictly prohibited."
141
+ IS_NEW_RELEASE="false"
142
+ SHOULD_TAG="false"
143
+ fi
144
+
145
+ echo "is_new_release=$IS_NEW_RELEASE" >> "$GITHUB_OUTPUT"
146
+ echo "should_tag=$SHOULD_TAG" >> "$GITHUB_OUTPUT"
147
+ echo "release_tag=$TARGET_TAG" >> "$GITHUB_OUTPUT"
148
+
149
+ - name: Install build tools
150
+ if: steps.release-check.outputs.is_new_release == 'true'
151
+ run: |
152
+ python -m pip install --upgrade pip
153
+ pip install build twine
154
+
155
+ - name: Build sdist and wheel
156
+ if: steps.release-check.outputs.is_new_release == 'true'
157
+ run: |
158
+ python -m build
159
+
160
+ - name: Verify Package Metadata
161
+ if: steps.release-check.outputs.is_new_release == 'true'
162
+ run: |
163
+ python -m twine check dist/*
164
+
165
+ - name: Upload dist artifacts
166
+ if: steps.release-check.outputs.is_new_release == 'true'
167
+ uses: actions/upload-artifact@v4
168
+ with:
169
+ name: dist
170
+ path: dist/
171
+ retention-days: 7
172
+
173
+ - name: Create and push Git tag
174
+ if: steps.release-check.outputs.is_new_release == 'true' && steps.release-check.outputs.should_tag == 'true'
175
+ shell: bash
176
+ run: |
177
+ TARGET_TAG="${{ steps.release-check.outputs.release_tag }}"
178
+ echo "Creating and pushing Git tag: $TARGET_TAG pointing to commit $GITHUB_SHA"
179
+ git config user.name "github-actions[bot]"
180
+ git config user.email "github-actions[bot]@users.noreply.github.com"
181
+ git tag "$TARGET_TAG" "$GITHUB_SHA"
182
+ git push origin "$TARGET_TAG"
183
+
184
+ - name: Create GitHub Release and Upload Assets
185
+ uses: softprops/action-gh-release@v3
186
+ if: steps.release-check.outputs.is_new_release == 'true'
187
+ with:
188
+ tag_name: ${{ steps.release-check.outputs.release_tag }}
189
+ target_commitish: ${{ github.sha }}
190
+ files: dist/*
191
+ generate_release_notes: true
192
+
193
+ publish-pypi:
194
+ name: Publish to PyPI via OIDC
195
+ needs: [test, build]
196
+ if: needs.build.outputs.is_new_release == 'true'
197
+ runs-on: ubuntu-latest
198
+ environment:
199
+ name: pypi
200
+ url: https://pypi.org/p/opsscript-gate
201
+ permissions:
202
+ id-token: write
203
+ steps:
204
+ - name: Download dist artifacts
205
+ uses: actions/download-artifact@v4
206
+ with:
207
+ name: dist
208
+ path: dist
209
+
210
+ - name: Publish package distributions to PyPI
211
+ uses: pypa/gh-action-pypi-publish@release/v1
212
+ with:
213
+ packages-dir: dist/
@@ -5,6 +5,25 @@ 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.3.0] - 2026-09-19
9
+
10
+ ### Added
11
+ - Structured runtime failure diagnostics: detect common "command not found" failures across Linux distributions (Alpine/BusyBox ash, Debian/Ubuntu dash, Bash, and missing interpreter paths).
12
+ - High-confidence dual-signal classification: requires exit code 127 and matching shell output to produce a `missing_command` diagnostic.
13
+ - Additive `diagnostic` field in JSON report format (`SingleResult.to_dict()`) providing structured diagnostic information (backward-compatible for consumers tolerating additional fields).
14
+ - Enhanced terminal table formatting and GitHub Actions Step Summary showing clear failure causes in the summary overview.
15
+
16
+ ---
17
+
18
+ ## [0.2.1] - 2026-09-19
19
+
20
+ ### Fixed
21
+ - Strip complete ANSI CSI and OSC escape sequences from diagnostic text instead of leaving escape-sequence residue.
22
+ - Ensure diagnostic `max_length` includes the trailing ellipsis when truncation occurs.
23
+ - Add regression coverage for ANSI sanitization and truncation edge cases.
24
+
25
+ ---
26
+
8
27
  ## [0.2.0] - 2026-09-18
9
28
 
10
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opsscript-gate
3
- Version: 0.2.0
3
+ Version: 0.3.0
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
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
39
39
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
40
40
  [![Supported Distros](https://img.shields.io/badge/matrix-Debian%20%7C%20Ubuntu%20%7C%20Alpine-orange.svg)](#default-test-matrix)
41
41
 
42
- **OpsScript Gate** is a drop-in runtime compatibility gate for Linux shell scripts. It executes your shell scripts inside isolated Debian, Ubuntu, and Alpine containers before release, catching environment-specific runtime failures that static analysis cannot detect.
42
+ **OpsScript Gate** is a drop-in runtime compatibility gate for Linux shell scripts. It executes your shell scripts inside separate Debian, Ubuntu, and Alpine Docker containers before release, catching environment-specific runtime failures that static analysis cannot detect.
43
43
 
44
44
  <p align="center">
45
45
  <img src="https://raw.githubusercontent.com/Mresyzz/opsscript-gate/main/.github/assets/social-preview.png" alt="OpsScript Gate Terminal Preview" width="800">
@@ -51,27 +51,31 @@ Description-Content-Type: text/markdown
51
51
 
52
52
  ### In GitHub Actions
53
53
 
54
- Add one step to your pull request workflow (`.github/workflows/gate.yml`):
54
+ Here is a complete minimal workflow to drop into `.github/workflows/shell-compat.yml`:
55
55
 
56
56
  ```yaml
57
- - name: Verify Shell Script Portability
58
- uses: Mresyzz/opsscript-gate@v0.2.0
59
- with:
60
- script-path: scripts/setup.sh
61
- # shell: posix # optional: 'posix' (default), 'shebang', or 'auto'
57
+ name: Shell compatibility
58
+ on: [pull_request]
59
+
60
+ jobs:
61
+ shell-compat:
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - uses: actions/checkout@v7
65
+ - uses: Mresyzz/opsscript-gate@v0.3.0
66
+ with:
67
+ script-path: scripts/setup.sh
68
+ shell: auto
62
69
  ```
63
70
 
64
71
  ### In Local Terminal (CLI)
65
72
 
66
- Requires Python 3.10+ and a local Docker engine:
73
+ Requires Python 3.10+ and a running local Docker engine:
67
74
 
68
75
  ```bash
69
76
  # Install from PyPI
70
77
  pip install opsscript-gate
71
78
 
72
- # Latest development version
73
- pip install git+https://github.com/Mresyzz/opsscript-gate.git
74
-
75
79
  # Run compatibility gate against your script
76
80
  opsscript-gate run ./scripts/setup.sh
77
81
  ```
@@ -138,9 +142,11 @@ Failed Distributions - Output Snippets (last 15 lines):
138
142
 
139
143
  ## Security Boundaries
140
144
 
141
- OpsScript Gate uses conservative container defaults when running scripts:
145
+ OpsScript Gate is not a security sandbox for untrusted code. Containers may run as the image's default user, and Docker/host-kernel security boundaries still apply.
146
+
147
+ OpsScript Gate applies conservative container defaults when running scripts:
142
148
 
143
- 1. **Unprivileged by Design**:
149
+ 1. **Restricted Container Defaults**:
144
150
  - Containers run with `privileged=False`.
145
151
  - All Linux capabilities are dropped: `cap_drop=["ALL"]`.
146
152
  - Privilege escalation is disabled: `security_opt=["no-new-privileges:true"]`.
@@ -149,7 +155,7 @@ OpsScript Gate uses conservative container defaults when running scripts:
149
155
  - OpsScript Gate does not mount additional host filesystem paths into the test container.
150
156
  3. **Anti-Hang Deadlock Defense**:
151
157
  - Disables TTY and stdin (`stdin_open=False`, `tty=False`).
152
- - Redirects execution: `/bin/sh -c "/bin/sh /tmp/target_script.sh </dev/null"`.
158
+ - Redirects execution with input disconnected: `/bin/sh -c "... /tmp/target_script.sh </dev/null"`.
153
159
  - Injects `DEBIAN_FRONTEND=noninteractive` and `CI=true`.
154
160
  - Any script prompting for user input (`read -p`) fails immediately instead of blocking the CI runner.
155
161
  4. **Timeout & Container Cleanup**:
@@ -157,8 +163,8 @@ OpsScript Gate uses conservative container defaults when running scripts:
157
163
  - Container removal is attempted from a `finally` block during normal Python execution paths, including failures and timeouts.
158
164
  5. **Windows CRLF Defense**:
159
165
  - Automatically detects and normalizes carriage returns (`\r\n` -> `\n`) before container execution, preventing false `\r: command not found` errors.
160
- 6. **POSIX-oriented `/bin/sh` Baseline**:
161
- - Containers invoke `/bin/sh` directly, catching undeclared Bashism syntax (e.g. bash arrays, `[[ ... ]]`) that break in lightweight Alpine environments.
166
+ 6. **Minimal `/bin/sh` Baseline**:
167
+ - In default POSIX mode, containers invoke `/bin/sh` directly, catching undeclared Bashism syntax (e.g. bash arrays, `[[ ... ]]`) that break in lightweight Alpine environments.
162
168
 
163
169
  ---
164
170
 
@@ -169,7 +175,7 @@ OpsScript Gate uses conservative container defaults when running scripts:
169
175
  | `debian:12-slim` | Debian 12 (Bookworm) | Minimal glibc + APT base |
170
176
  | `ubuntu:22.04` | Ubuntu 22.04 LTS (Jammy) | Enterprise long-term support baseline |
171
177
  | `ubuntu:24.04` | Ubuntu 24.04 LTS (Noble) | Modern glibc, updated coreutils & defaults |
172
- | `alpine:3.20` | Alpine Linux 3.20 | Minimal musl libc + BusyBox (strict POSIX test) |
178
+ | `alpine:3.20` | Alpine Linux 3.20 | Minimal musl libc + BusyBox /bin/sh environment |
173
179
 
174
180
  You can customize the matrix at any time via `--matrix` or Action input `matrix`.
175
181
 
@@ -196,7 +202,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [--timeout TIMEOUT]
196
202
 
197
203
  ### Shell Execution Modes (`--shell`)
198
204
 
199
- - **`posix`** (default): Strictly executes with `/bin/sh`, ignoring any script shebang. Ideal for verifying that your script runs in minimal POSIX-compliant environments (e.g. Alpine BusyBox).
205
+ - **`posix`** (default): Strictly executes with `/bin/sh`, ignoring any script shebang. Useful for verifying that your script runs under minimal `/bin/sh` environments, including Alpine BusyBox.
200
206
  - **`shebang`**: Strictly honors the interpreter specified in the script's shebang (`#!/bin/sh`, `#!/bin/bash`, `#!/usr/bin/sh`, `#!/usr/bin/bash`, `#!/usr/bin/env sh`, `#!/usr/bin/env bash`). If the shebang is missing, malformed, or specifies an unsupported interpreter/flag, the check fails immediately with an error before running containers.
201
207
  - **`auto`**: Honors recognized shebangs if present; falls back to `/bin/sh` if no shebang is declared. Scripts with explicit unsupported or malformed shebangs fail immediately with an error (does not silently execute as POSIX).
202
208
 
@@ -217,6 +223,14 @@ Check out the [examples/](examples/) directory for self-contained, runnable scen
217
223
 
218
224
  ---
219
225
 
226
+ ## Dogfooding
227
+
228
+ 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.1` with `shell: auto`.
229
+
230
+ See the downstream workflow: [`.github/workflows/test.yml`](https://github.com/Mresyzz/linux-dev-bootstrap/blob/main/.github/workflows/test.yml).
231
+
232
+ ---
233
+
220
234
  ## Development & Testing
221
235
 
222
236
  The test suite uses Docker SDK mocking to ensure fast unit tests without needing a local daemon:
@@ -227,6 +241,9 @@ git clone https://github.com/Mresyzz/opsscript-gate.git
227
241
  cd opsscript-gate
228
242
  pip install -e .[test]
229
243
 
244
+ # Or install latest unreleased code directly from Git:
245
+ # pip install git+https://github.com/Mresyzz/opsscript-gate.git
246
+
230
247
  # Run unit tests (mocked)
231
248
  pytest -v -m "not integration"
232
249
 
@@ -11,7 +11,7 @@
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
12
  [![Supported Distros](https://img.shields.io/badge/matrix-Debian%20%7C%20Ubuntu%20%7C%20Alpine-orange.svg)](#default-test-matrix)
13
13
 
14
- **OpsScript Gate** is a drop-in runtime compatibility gate for Linux shell scripts. It executes your shell scripts inside isolated Debian, Ubuntu, and Alpine containers before release, catching environment-specific runtime failures that static analysis cannot detect.
14
+ **OpsScript Gate** is a drop-in runtime compatibility gate for Linux shell scripts. It executes your shell scripts inside separate Debian, Ubuntu, and Alpine Docker containers before release, catching environment-specific runtime failures that static analysis cannot detect.
15
15
 
16
16
  <p align="center">
17
17
  <img src="https://raw.githubusercontent.com/Mresyzz/opsscript-gate/main/.github/assets/social-preview.png" alt="OpsScript Gate Terminal Preview" width="800">
@@ -23,27 +23,31 @@
23
23
 
24
24
  ### In GitHub Actions
25
25
 
26
- Add one step to your pull request workflow (`.github/workflows/gate.yml`):
26
+ Here is a complete minimal workflow to drop into `.github/workflows/shell-compat.yml`:
27
27
 
28
28
  ```yaml
29
- - name: Verify Shell Script Portability
30
- uses: Mresyzz/opsscript-gate@v0.2.0
31
- with:
32
- script-path: scripts/setup.sh
33
- # shell: posix # optional: 'posix' (default), 'shebang', or 'auto'
29
+ name: Shell compatibility
30
+ on: [pull_request]
31
+
32
+ jobs:
33
+ shell-compat:
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v7
37
+ - uses: Mresyzz/opsscript-gate@v0.3.0
38
+ with:
39
+ script-path: scripts/setup.sh
40
+ shell: auto
34
41
  ```
35
42
 
36
43
  ### In Local Terminal (CLI)
37
44
 
38
- Requires Python 3.10+ and a local Docker engine:
45
+ Requires Python 3.10+ and a running local Docker engine:
39
46
 
40
47
  ```bash
41
48
  # Install from PyPI
42
49
  pip install opsscript-gate
43
50
 
44
- # Latest development version
45
- pip install git+https://github.com/Mresyzz/opsscript-gate.git
46
-
47
51
  # Run compatibility gate against your script
48
52
  opsscript-gate run ./scripts/setup.sh
49
53
  ```
@@ -110,9 +114,11 @@ Failed Distributions - Output Snippets (last 15 lines):
110
114
 
111
115
  ## Security Boundaries
112
116
 
113
- OpsScript Gate uses conservative container defaults when running scripts:
117
+ OpsScript Gate is not a security sandbox for untrusted code. Containers may run as the image's default user, and Docker/host-kernel security boundaries still apply.
118
+
119
+ OpsScript Gate applies conservative container defaults when running scripts:
114
120
 
115
- 1. **Unprivileged by Design**:
121
+ 1. **Restricted Container Defaults**:
116
122
  - Containers run with `privileged=False`.
117
123
  - All Linux capabilities are dropped: `cap_drop=["ALL"]`.
118
124
  - Privilege escalation is disabled: `security_opt=["no-new-privileges:true"]`.
@@ -121,7 +127,7 @@ OpsScript Gate uses conservative container defaults when running scripts:
121
127
  - OpsScript Gate does not mount additional host filesystem paths into the test container.
122
128
  3. **Anti-Hang Deadlock Defense**:
123
129
  - Disables TTY and stdin (`stdin_open=False`, `tty=False`).
124
- - Redirects execution: `/bin/sh -c "/bin/sh /tmp/target_script.sh </dev/null"`.
130
+ - Redirects execution with input disconnected: `/bin/sh -c "... /tmp/target_script.sh </dev/null"`.
125
131
  - Injects `DEBIAN_FRONTEND=noninteractive` and `CI=true`.
126
132
  - Any script prompting for user input (`read -p`) fails immediately instead of blocking the CI runner.
127
133
  4. **Timeout & Container Cleanup**:
@@ -129,8 +135,8 @@ OpsScript Gate uses conservative container defaults when running scripts:
129
135
  - Container removal is attempted from a `finally` block during normal Python execution paths, including failures and timeouts.
130
136
  5. **Windows CRLF Defense**:
131
137
  - Automatically detects and normalizes carriage returns (`\r\n` -> `\n`) before container execution, preventing false `\r: command not found` errors.
132
- 6. **POSIX-oriented `/bin/sh` Baseline**:
133
- - Containers invoke `/bin/sh` directly, catching undeclared Bashism syntax (e.g. bash arrays, `[[ ... ]]`) that break in lightweight Alpine environments.
138
+ 6. **Minimal `/bin/sh` Baseline**:
139
+ - In default POSIX mode, containers invoke `/bin/sh` directly, catching undeclared Bashism syntax (e.g. bash arrays, `[[ ... ]]`) that break in lightweight Alpine environments.
134
140
 
135
141
  ---
136
142
 
@@ -141,7 +147,7 @@ OpsScript Gate uses conservative container defaults when running scripts:
141
147
  | `debian:12-slim` | Debian 12 (Bookworm) | Minimal glibc + APT base |
142
148
  | `ubuntu:22.04` | Ubuntu 22.04 LTS (Jammy) | Enterprise long-term support baseline |
143
149
  | `ubuntu:24.04` | Ubuntu 24.04 LTS (Noble) | Modern glibc, updated coreutils & defaults |
144
- | `alpine:3.20` | Alpine Linux 3.20 | Minimal musl libc + BusyBox (strict POSIX test) |
150
+ | `alpine:3.20` | Alpine Linux 3.20 | Minimal musl libc + BusyBox /bin/sh environment |
145
151
 
146
152
  You can customize the matrix at any time via `--matrix` or Action input `matrix`.
147
153
 
@@ -168,7 +174,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [--timeout TIMEOUT]
168
174
 
169
175
  ### Shell Execution Modes (`--shell`)
170
176
 
171
- - **`posix`** (default): Strictly executes with `/bin/sh`, ignoring any script shebang. Ideal for verifying that your script runs in minimal POSIX-compliant environments (e.g. Alpine BusyBox).
177
+ - **`posix`** (default): Strictly executes with `/bin/sh`, ignoring any script shebang. Useful for verifying that your script runs under minimal `/bin/sh` environments, including Alpine BusyBox.
172
178
  - **`shebang`**: Strictly honors the interpreter specified in the script's shebang (`#!/bin/sh`, `#!/bin/bash`, `#!/usr/bin/sh`, `#!/usr/bin/bash`, `#!/usr/bin/env sh`, `#!/usr/bin/env bash`). If the shebang is missing, malformed, or specifies an unsupported interpreter/flag, the check fails immediately with an error before running containers.
173
179
  - **`auto`**: Honors recognized shebangs if present; falls back to `/bin/sh` if no shebang is declared. Scripts with explicit unsupported or malformed shebangs fail immediately with an error (does not silently execute as POSIX).
174
180
 
@@ -189,6 +195,14 @@ Check out the [examples/](examples/) directory for self-contained, runnable scen
189
195
 
190
196
  ---
191
197
 
198
+ ## Dogfooding
199
+
200
+ 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.1` with `shell: auto`.
201
+
202
+ See the downstream workflow: [`.github/workflows/test.yml`](https://github.com/Mresyzz/linux-dev-bootstrap/blob/main/.github/workflows/test.yml).
203
+
204
+ ---
205
+
192
206
  ## Development & Testing
193
207
 
194
208
  The test suite uses Docker SDK mocking to ensure fast unit tests without needing a local daemon:
@@ -199,6 +213,9 @@ git clone https://github.com/Mresyzz/opsscript-gate.git
199
213
  cd opsscript-gate
200
214
  pip install -e .[test]
201
215
 
216
+ # Or install latest unreleased code directly from Git:
217
+ # pip install git+https://github.com/Mresyzz/opsscript-gate.git
218
+
202
219
  # Run unit tests (mocked)
203
220
  pytest -v -m "not integration"
204
221
 
@@ -30,7 +30,9 @@ Reports will be reviewed as maintainer availability allows.
30
30
 
31
31
  ## Runner Security Boundaries
32
32
 
33
+ OpsScript Gate is not a security sandbox for untrusted code. Containers may run as the image's default user, and Docker containers still share the host kernel.
34
+
33
35
  OpsScript Gate applies conservative container defaults when running scripts:
34
- - **Unprivileged Execution**: Containers run with `privileged=False`, `cap_drop=["ALL"]`, and `security_opt=["no-new-privileges:true"]`.
36
+ - **Restricted Container Defaults**: Containers run with `privileged=False`, `cap_drop=["ALL"]`, and `security_opt=["no-new-privileges:true"]`.
35
37
  - **Read-Only Mounting**: Target scripts are mounted read-only (`:ro`). OpsScript Gate does not mount additional host filesystem paths into test containers.
36
38
  - **Resource Protection**: Containers are subject to timeouts and `SIGKILL` termination. Container removal is attempted from a `finally` block during normal Python execution paths, including failures and timeouts.
@@ -17,9 +17,10 @@ jobs:
17
17
  uses: actions/checkout@v7
18
18
 
19
19
  - name: Run OpsScript Gate
20
- uses: Mresyzz/opsscript-gate@v0.1.1
20
+ uses: Mresyzz/opsscript-gate@v0.3.0
21
21
  with:
22
22
  script-path: 'scripts/deploy.sh'
23
+ shell: auto
23
24
  matrix: 'debian:12-slim,ubuntu:22.04,ubuntu:24.04,alpine:3.20'
24
25
  timeout: '60'
25
26
  format: 'table'
@@ -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.3.0"
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.3.0"
4
4
  __all__ = ["__version__"]
@@ -20,6 +20,23 @@ class ShellMode(str, Enum):
20
20
  AUTO = "auto"
21
21
 
22
22
 
23
+ @dataclass
24
+ class FailureDiagnostic:
25
+ """Structured diagnostic information for execution failures."""
26
+ kind: str
27
+ message: str
28
+ command: str | None = None
29
+
30
+ def to_dict(self) -> dict[str, Any]:
31
+ d: dict[str, Any] = {
32
+ "kind": self.kind,
33
+ "message": self.message,
34
+ }
35
+ if self.command is not None:
36
+ d["command"] = self.command
37
+ return d
38
+
39
+
23
40
  @dataclass
24
41
  class SingleResult:
25
42
  """Execution result for a single Linux distribution."""
@@ -29,6 +46,7 @@ class SingleResult:
29
46
  duration: float
30
47
  output_snippet: str = ""
31
48
  error_message: str | None = None
49
+ diagnostic: FailureDiagnostic | None = None
32
50
 
33
51
  def to_dict(self) -> dict[str, Any]:
34
52
  return {
@@ -38,6 +56,7 @@ class SingleResult:
38
56
  "duration": round(self.duration, 3),
39
57
  "output_snippet": self.output_snippet,
40
58
  "error_message": self.error_message,
59
+ "diagnostic": self.diagnostic.to_dict() if self.diagnostic is not None else None,
41
60
  }
42
61
 
43
62
 
@@ -16,6 +16,8 @@ def format_terminal_table(report: RunReport) -> str:
16
16
 
17
17
  if r.status == DistroStatus.PASS:
18
18
  detail = "OK"
19
+ elif r.diagnostic:
20
+ detail = r.diagnostic.message
19
21
  elif r.error_message:
20
22
  detail = r.error_message
21
23
  else:
@@ -107,7 +109,12 @@ def format_github_summary(report: RunReport) -> str:
107
109
 
108
110
  exit_code_str = f"`{r.exit_code}`" if r.exit_code is not None else "`N/A`"
109
111
  duration_str = f"`{r.duration:.2f}s`"
110
- msg = r.error_message or "-"
112
+ if r.status == DistroStatus.PASS:
113
+ msg = "OK"
114
+ elif r.diagnostic:
115
+ msg = r.diagnostic.message
116
+ else:
117
+ msg = r.error_message or "-"
111
118
  msg_escaped = msg.replace("|", "\\|")
112
119
 
113
120
  lines.append(
@@ -123,6 +130,11 @@ def format_github_summary(report: RunReport) -> str:
123
130
  for r in failures:
124
131
  lines.append(f"<details><summary><b>[{r.status.value}] {r.distro}</b></summary>")
125
132
  lines.append("")
133
+ if r.diagnostic:
134
+ clean_kind = r.diagnostic.kind.replace("`", "'")
135
+ clean_msg = r.diagnostic.message.replace("`", "'")
136
+ lines.append(f"> **Diagnostic:** `{clean_kind}` — `{clean_msg}`")
137
+ lines.append("")
126
138
  if r.error_message:
127
139
  lines.append(f"> **Error:** {r.error_message}")
128
140
  lines.append("")