OpsScript Gate
ShellCheck tells you if your script looks portable. OpsScript Gate checks if it actually runs there.
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.
Quickstart
In GitHub Actions
Add one step to your pull request workflow (.github/workflows/gate.yml):
- name: Verify Shell Script Portability
uses: Mresyzz/opsscript-gate@v0.1.1
with:
script-path: scripts/setup.sh
In Local Terminal (CLI)
Requires Python 3.10+ and a local Docker engine:
# Install directly from GitHub
pip install git+https://github.com/Mresyzz/opsscript-gate.git
# Run compatibility gate against your script
opsscript-gate run ./scripts/setup.sh
Example: What Static Analysis Misses
Consider this deployment script:
#!/bin/sh
set -e
echo "Fetching package information..."
apt-get --version
Running shellcheck reports 0 errors, 0 warnings because the syntax is valid POSIX shell.
However, when verified with OpsScript Gate:
+--------------------+----------+-----------+----------------------------------------------------+
| Distro | Status | Exit Code | Details |
+--------------------+----------+-----------+----------------------------------------------------+
| debian:12-slim | PASS | 0 | OK |
| ubuntu:22.04 | PASS | 0 | OK |
| ubuntu:24.04 | PASS | 0 | OK |
| alpine:3.20 | FAIL | 127 | Script failed with non-zero exit code: 127 |
+--------------------+----------+-----------+----------------------------------------------------+
Result: FAILED
============================================================
Failed Distributions - Output Snippets (last 15 lines):
============================================================
--- [alpine:3.20] (FAIL) ---
/tmp/target_script.sh: line 4: apt-get: not found
Example output; timing values omitted because they vary by host and image cache state.
Why it failed: Alpine Linux is musl/BusyBox-based and uses apk, not apt-get. OpsScript Gate catches the missing utility (exit code 127) during test execution, before the script is deployed.
Why OpsScript Gate?
OpsScript Gate vs ShellCheck vs Custom CI Matrix
| Capability | OpsScript Gate | ShellCheck | Handwritten CI Matrix |
|---|---|---|---|
| Runtime execution | Yes | No (Static AST only) | Yes |
| Real distro environments | Yes (Debian, Ubuntu, Alpine) | No | Yes |
| Preconfigured defaults | Yes | Yes | Requires custom workflow configuration |
| Safe container defaults | Built-in (ro, cap_drop, kill) |
N/A | User-defined |
| Anti-hang stdin protection | Built-in (</dev/null, noninteractive) |
No | User-defined |
| Unified summary & diagnostics | Built-in (ASCII + Step Summary) | Static warnings | User-defined |
- ShellCheck is indispensable for static analysis (syntax, quoting, SC warnings). OpsScript Gate complements it by testing actual execution behavior in real distributions.
- Handwritten CI Matrix requires maintaining complex Docker configurations, volume mounts, timeout guards, and log parsers across every project. OpsScript Gate packages this into a single check.
Security Boundaries
OpsScript Gate uses conservative container defaults when running scripts:
- Unprivileged by Design:
- Containers run with
privileged=False. - All Linux capabilities are dropped:
cap_drop=["ALL"]. - Privilege escalation is disabled:
security_opt=["no-new-privileges:true"].
- Containers run with
- Read-Only Target Mount:
- The tested script is mounted read-only (
:ro) at/tmp/target_script.sh. - OpsScript Gate does not mount additional host filesystem paths into the test container.
- The tested script is mounted read-only (
- Anti-Hang Deadlock Defense:
- Disables TTY and stdin (
stdin_open=False,tty=False). - Redirects execution:
/bin/sh -c "/bin/sh /tmp/target_script.sh </dev/null". - Injects
DEBIAN_FRONTEND=noninteractiveandCI=true. - Any script prompting for user input (
read -p) fails immediately instead of blocking the CI runner.
- Disables TTY and stdin (
- Timeout & Container Cleanup:
- Enforces a configurable timeout (default: 60s). Timed-out containers are sent
SIGKILLand markedTIMED_OUT. - Container removal is attempted from a
finallyblock during normal Python execution paths, including failures and timeouts.
- Enforces a configurable timeout (default: 60s). Timed-out containers are sent
- Windows CRLF Defense:
- Automatically detects and normalizes carriage returns (
\r\n->\n) before container execution, preventing false\r: command not founderrors.
- Automatically detects and normalizes carriage returns (
- POSIX-oriented
/bin/shBaseline:- Containers invoke
/bin/shdirectly, catching undeclared Bashism syntax (e.g. bash arrays,[[ ... ]]) that break in lightweight Alpine environments.
- Containers invoke
Default Test Matrix
| Image | Distribution | Focus |
|---|---|---|
debian:12-slim |
Debian 12 (Bookworm) | Minimal glibc + APT base |
ubuntu:22.04 |
Ubuntu 22.04 LTS (Jammy) | Enterprise long-term support baseline |
ubuntu:24.04 |
Ubuntu 24.04 LTS (Noble) | Modern glibc, updated coreutils & defaults |
alpine:3.20 |
Alpine Linux 3.20 | Minimal musl libc + BusyBox (strict POSIX test) |
You can customize the matrix at any time via --matrix or Action input matrix.
CLI Reference
usage: opsscript-gate run [-h] [--matrix MATRIX] [--timeout TIMEOUT]
[--format {table,markdown,json}]
script_path
| Parameter | Type | Default | Description |
|---|---|---|---|
script_path |
Positional | Required | Path to target shell script |
--matrix |
String | debian:12-slim,ubuntu:22.04,ubuntu:24.04,alpine:3.20 |
Comma-separated list of Docker images |
--timeout |
Integer | 60 |
Hard timeout per container in seconds |
--format |
Choice | table |
Output format: table, markdown, or json |
--version |
Flag | - | Show version number |
-h, --help |
Flag | - | Show argument help |
Exit Code Convention
0: All distributions passed (PASS).1: At least one distribution failed (FAIL), timed out (TIMED_OUT), or errored (ERROR).
Examples
Check out the examples/ directory for self-contained, runnable scenarios:
examples/basic/: A clean POSIX script that passes across all distributions.examples/alpine-incompatibility/: Demonstrates catching implicit Debian/Ubuntu dependencies (e.g.apt-get).examples/interactive-hang/: Demonstrates how unhandledreadprompts fail immediately instead of hanging.examples/github-actions/: Ready-to-copy production pull request workflow.
Development & Testing
The test suite uses Docker SDK mocking to ensure fast unit tests without needing a local daemon:
# Clone and install with test dependencies
git clone https://github.com/Mresyzz/opsscript-gate.git
cd opsscript-gate
pip install -e .[test]
# Run unit tests (mocked)
pytest -v -m "not integration"
# Run integration tests (Requires Docker daemon)
pytest -v
Roadmap
See ROADMAP.md for planned capabilities, including:
- Shebang-aware execution modes (
--shell auto|posix|shebang) - Container resource limits (
--mem-limit,--pids-limit) - Configurable network isolation (
--network none|bridge) - Parallel matrix execution
Contributing & Security
- Contributing: Please review CONTRIBUTING.md for pull request guidelines and security boundaries.
- Security Policy: Read SECURITY.md to report vulnerabilities responsibly.
- Changelog: See CHANGELOG.md for release history.
License
OpsScript Gate is licensed under the MIT License.
Release files for opsscript-gate 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| opsscript_gate-0.1.2.tar.gz | 76.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| opsscript_gate-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 89.6 kB
Release files / opsscript_gate-0.1.2.tar.gz
| Download URL | opsscript_gate-0.1.2.tar.gz |
|---|---|
| Size | 76.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cbcb9003007242cc2a7fd93e01bdc0c4b6f93b16b60a7324a110a014359e8271
|
|
BLAKE2b-256 checksum How to use checksums |
b74596b33c9cc116efd40c9261ad7fd8cb25cf1c8709734d0e8c942e35f89236
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logRelease files / opsscript_gate-0.1.2-py3-none-any.whl
| Download URL | opsscript_gate-0.1.2-py3-none-any.whl |
|---|---|
| Size | 13.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3503770a37346b13e3dd36dbdcbe068762e55cbd0c941f9e0ccdb0ad2e4a4342
|
|
BLAKE2b-256 checksum How to use checksums |
a912ddf5470d286d02015de7dc44492c9ac164a408c913433234da341bf12b9c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency log