OpsScript Gate — shell script compatibility testing
简体中文 · Configuration · Troubleshooting · Self-hosted model installer guide
ShellCheck analyzes shell syntax. OpsScript Gate executes scripts in Linux containers.
OpsScript Gate is a runtime compatibility checker for Linux shell scripts. It executes scripts inside unprivileged Debian, Ubuntu, and Alpine containers, catching environment-specific failures, missing interpreters, and package-manager assumptions that static analysis cannot detect.
It is designed for shell testing, portable shell validation, Bash/POSIX compatibility checks, and cross-distro CI where syntax-only tooling is not enough.
If your repository contains an installer, bootstrap script, or shell entrypoint, the fastest path is to
open the InstallReady scanner, enter owner/repository, and
choose Add workflow on GitHub. It generates a reviewable workflow without an account, API key, or
service signup. You can also copy the maintained zero-configuration workflow.
Try the browser scanner: InstallReady checks this repository without executing code or requiring a login.
Current release: v0.8.2
The current release includes a project config file, a dry-run plan, presets, exclusions, saved reports, and changed-script selection for pull requests. The project controls were introduced in v0.5.0; projects on v0.4.1 and earlier do not include them.
opsscript-gate init
opsscript-gate doctor
opsscript-gate run --dry-run
opsscript-gate run --format json --output reports/compatibility.json
init creates .opsscript-gate.json and a GitHub Actions workflow without replacing
existing files. Review its exclusions (tests/*, examples/*) and offline network
default before running. No Docker is required for init or --dry-run.
Use opsscript-gate doctor to check Python and Docker before a real run.
Useful for standalone installers, container entrypoints and release scripts that
must work on both GNU/Linux and Alpine/BusyBox. Each script runs independently;
repository files, sibling scripts and project dependencies are not mounted.
If an installer needs a small, explicit set of tools such as curl, pass
packages in the config or Action input; the runner installs them with the
image's native package manager before testing. Package setup is opt-in and
requires bridge networking. See configuration and migration and
why a shell script works on Ubuntu but fails on Alpine.
Quickstart
In GitHub Actions
Add this workflow to .github/workflows/shell-compat.yml:
The repository includes a maintained workflow template. For a one-click setup in another public repository, use the InstallReady scanner above; it opens GitHub's workflow editor with the generated file ready to review.
You can copy the maintained example from examples/github-actions/workflow.yml
and change the script-path, or use the minimal workflow below.
name: Shell Compatibility Gate
on: [pull_request, push]
jobs:
compat:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: Mresyzz/opsscript-gate@v0.8.2
This zero-configuration form discovers shell scripts in the repository. Use
opsscript-gate run --dry-run to inspect the selection, or set script-path when the
repository has a single installer. For self-hosted model installers, see the
installer guide.
Or test a specific script with custom execution modes:
- uses: Mresyzz/opsscript-gate@v0.8.2
with:
script-path: scripts/install.sh
shell: auto
jobs: 4
In Local Terminal (CLI)
Requires Python 3.10+ and a running local Docker engine:
# Install from PyPI
pip install opsscript-gate
# Run against a specific script
opsscript-gate run ./scripts/install.sh
# Or auto-discover scripts across your repository
opsscript-gate run
Test only scripts changed in a pull request
Fetch the comparison revision and pass the pull request base SHA to avoid running unrelated scripts:
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: Mresyzz/opsscript-gate@v0.8.2
with:
changed-since: ${{ github.event.pull_request.base.sha }}
preset: minimal
If the change does not include a shell script, the check passes without starting a
container. The same selection can be previewed locally with
opsscript-gate run --changed-since origin/main --dry-run.
Example: package-manager mismatch
Create install.sh:
#!/bin/sh
set -e
apt-get --version
Then run:
opsscript-gate run ./install.sh
A typical result across the default matrix looks like this:
Debian 12 PASS
Ubuntu 22.04 PASS
Ubuntu 24.04 PASS
Alpine 3.20 FAIL
apt-get: not found
The script is valid shell, but it assumes a command that is not present in Alpine.
CI output
The action reports failures in two places:
GitHub annotations
Failures include the script path, line number, distribution, and a short diagnostic:
::error file=scripts/setup.sh,line=4,title=OpsScript Gate: [alpine:3.20] command not found: apt-get::command not found: apt-get — Alpine normally uses apk instead of apt-get.
Step summary
The action also writes a markdown matrix to $GITHUB_STEP_SUMMARY:
## 🛡️ OpsScript Gate Compatibility Report
**Target Script**: `scripts/setup.sh`
**Status**: ❌ **CHECKS FAILED (3/4 Passed)**
**Total Duration**: `1.24s`
### 📊 Compatibility Matrix
| Distribution | Status | Exit Code | Time | Diagnostic & Recommendation |
| :--- | :---: | :---: | :---: | :--- |
| `debian:12-slim` | ✅ PASS | `0` | `0.42s` | OK |
| `ubuntu:22.04` | ✅ PASS | `0` | `0.38s` | OK |
| `ubuntu:24.04` | ✅ PASS | `0` | `0.35s` | OK |
| `alpine:3.20` | ❌ FAIL | `127` | `0.19s` | ⚠️ Missing command: `apt-get` (line 4)<br>💡 *Alpine normally uses apk instead of apt-get.* |
<details>
<summary><b>Markdown result</b></summary>
...
</details>
SARIF for Code Scanning
Use sarif when the result should appear in GitHub Code Scanning or another
SARIF-compatible viewer. Upload it explicitly with the official upload action:
- uses: Mresyzz/opsscript-gate@v0.8.2
with:
script-path: scripts/install.sh
format: sarif
output: reports/opsscript-gate.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: reports/opsscript-gate.sarif
Each runtime failure becomes a location-aware result with the distribution, exit code, command, and diagnostic line. A passing matrix produces a valid SARIF file with no findings.
Example: runtime behavior that static analysis misses
Consider this clean deployment script:
#!/bin/sh
set -e
echo "Fetching package information..."
apt-get --version
Running shellcheck reports 0 errors, 0 warnings because the syntax is syntactically valid POSIX shell.
However, when verified with OpsScript Gate:
+----------------+----------+-----------+----------+------------------------------------+
| Distro | Status | Exit Code | Duration | Details |
+----------------+----------+-----------+----------+------------------------------------+
| debian:12-slim | PASS | 0 | 0.42s | OK |
| ubuntu:22.04 | PASS | 0 | 0.38s | OK |
| ubuntu:24.04 | PASS | 0 | 0.35s | OK |
| alpine:3.20 | FAIL | 127 | 0.19s | command not found: apt-get (line 4)|
+----------------+----------+-----------+----------+------------------------------------+
Total duration: 0.58s | Result: FAILED
Remediation Recommendations:
* [alpine:3.20] Alpine normally uses apk instead of apt-get.
============================================================
Failed Distributions - Output Snippets (last 15 lines):
============================================================
--- [alpine:3.20] (FAIL) ---
sh: line 4: apt-get: not found
Why it failed: Alpine uses apk, not apt-get. The report includes the missing command and its line number.
Scope compared with other checks
OpsScript Gate compared with ShellCheck and a 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 |
| Restricted container defaults | Built-in (ro, cap_drop, resource limits) |
N/A | User-defined |
| Anti-hang stdin protection | Built-in (</dev/null, noninteractive) |
No | User-defined |
| Line-Level Annotations & Hints | Built-in | Static warnings | User-defined |
| Parallel Matrix Execution | Built-in (--jobs) |
N/A | Manual matrix config |
ShellCheck checks shell syntax and common mistakes. OpsScript Gate runs the script in real distributions. A custom CI matrix can do the same job, but each repository must maintain its own images, mounts, timeouts, and report handling.
Security boundaries and container isolation
OpsScript Gate is a runtime compatibility testing tool, not a security sandbox for hostile or fully untrusted code. Containers still share the host kernel, so target scripts should be treated accordingly.
View security boundaries and runtime hardening details
The runner uses restricted container defaults:
- Restricted Container Defaults:
- 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
- Resource Constraints:
- Memory limits enforced per container (
--mem-limit, default:256m). - Process caps enforced to prevent fork bombs (
--pids-limit, default:128). - Network isolation configurable (
--network bridgeor--network none).
- Memory limits enforced per container (
- Read-Only Target Mount:
- The tested script is mounted read-only (
:ro) at/tmp/target_script.sh. - No host directories or sensitive sockets are mounted into test containers.
- The tested script is mounted read-only (
- Anti-Hang Deadlock Defense:
- Disables TTY and stdin (
stdin_open=False,tty=False). - Disconnects standard input:
/bin/sh -c "... /tmp/target_script.sh </dev/null". - Injects
DEBIAN_FRONTEND=noninteractiveandCI=true. Interactive prompts (read -p) fail instead of hanging CI runners.
- Disables TTY and stdin (
- Hard Timeout & Container Cleanup:
- Enforces configurable timeout (default: 60s). Timed-out containers are sent
SIGKILLand markedTIMED_OUT. - Container removal is attempted from a
finallyblock in normal, failure, and timeout execution paths.
- Enforces configurable timeout (default: 60s). Timed-out containers are sent
- Bounded Output & Memory Protection:
- Captures container logs using a rolling byte buffer capped at 256 KiB (
MAX_CAPTURED_LOG_BYTES) and a 500-line tail limit (MAX_LOG_TAIL_LINES) to reduce memory-exhaustion risk from runaway output.
- Captures container logs using a rolling byte buffer capped at 256 KiB (
- Untrusted Log Neutralization & Terminal Defense:
- Neutralizes line-leading workflow commands (
[container] ::) to prevent forged GitHub Actions annotations in CI runners. - Strips ANSI escape sequences and dangerous C0 control characters, and normalizes carriage returns (
\r) to defeat terminal line-overwrite spoofing. - Employs context-sensitive escaping (
escape_inline_code,escape_markdown_text,escape_markdown_table_cell,escape_html_text,format_safe_code_fence) to protect Step Summary output contexts.
- Neutralizes line-leading workflow commands (
- Command Injection Defense:
- OpsScript Gate's own annotations (
::error) apply strict percent-encoding for workflow-command fields and message bodies.
- OpsScript Gate's own annotations (
- Bounded Streaming CRLF & Shebang Defense:
- Stream-normalizes CRLF in 64 KiB chunks and bounds shebang parsing to 4096 bytes without whole-file memory allocation.
- Pre-normalizes scripts once before parallel matrix runs, sharing a read-only prepared path across worker threads.
For the complete supported-version policy and vulnerability reporting guidance, see SECURITY.md.
When NOT to use OpsScript Gate
OpsScript Gate is intentionally focused on Linux shell runtime compatibility. It is not designed for:
- executing hostile or fully untrusted third-party scripts
- kernel-level or privileged behavior testing
- replacing full integration or end-to-end test suites
- validating macOS or Windows behavior
- proving that a script is secure
Use it when you want to know whether a shell script actually runs across the supported Linux distributions.
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 /bin/sh environment |
Customize the matrix at any time via --matrix or Action input matrix.
CLI reference
Check local prerequisites before starting containers:
opsscript-gate doctor [--format table|json]
usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
[--format {table,markdown,json}]
[--shell {posix,shebang,auto}]
[--mem-limit MEM_LIMIT] [--pids-limit PIDS_LIMIT]
[--network NETWORK]
[script_path]
| Parameter | Type | Default | Description |
|---|---|---|---|
script_path |
Positional | Optional | Path to target shell script (auto-discovers if omitted) |
-j, --jobs |
Integer | min(2, size) |
Number of concurrent container jobs |
--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, json, or sarif |
--shell |
Choice | posix |
Execution mode: posix (default), shebang, or auto |
--mem-limit |
String | 256m |
Memory limit per container (e.g. 256m, 512m) |
--pids-limit |
Integer | 128 |
Maximum number of processes per container |
--network |
Choice | bridge |
Container network mode: bridge or none |
--changed-since |
Git revision | - | Test shell scripts changed between this revision and HEAD |
--version |
Flag | - | Show version number |
-h, --help |
Flag | - | Show argument help |
Shell Execution Modes (--shell)
posix(default): Strictly executes with/bin/sh, ignoring any script shebang to verify portability against minimal POSIX environments (including Alpine BusyBox).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). Fails immediately if shebang is missing, malformed, or unsupported.auto: Honors recognized shebangs if present; safely falls back to/bin/shif no shebang is present.
Exit Code Convention
0: All tested scripts and distributions passed (PASS).1: At least one distribution failed (FAIL), timed out (TIMED_OUT), or errored (ERROR).
v0.5.0 project controls
| Option | Purpose |
|---|---|
init |
Generate configuration and a GitHub workflow without overwriting files |
--dry-run |
Preview selected scripts, images and total executions without Docker |
--config PATH |
Load a specific JSON configuration file |
--preset minimal |
Debian + Alpine (two container runs per script) |
--preset ubuntu |
Ubuntu 22.04 + 24.04 |
--exclude 'tests/*' |
Exclude a repository-relative glob; repeatable |
--max-scripts 50 |
Raise the default discovery limit of 20; overflow is an error |
--output report.json |
Save the selected output format, including failed test reports |
Explicit CLI options override project settings. Explicit script paths bypass discovery and its exclusions. See complete configuration semantics.
🧪 Development & Testing
# Clone repository
git clone https://github.com/Mresyzz/opsscript-gate.git
cd opsscript-gate
# Install in editable mode with test dependencies
pip install -e .[test]
# Run unit tests (Mocked, no Docker daemon required)
pytest -v -m "not integration"
# Run integration tests (Requires local Docker daemon)
pytest -v
Feedback
Please report compatibility cases, bugs, and pull requests in the issue tracker.
📄 License
OpsScript Gate is licensed under the MIT License.
Metadata
Release files for opsscript-gate 0.8.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.8.2.tar.gz | 128.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| opsscript_gate-0.8.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 164.6 kB
Release files / opsscript_gate-0.8.2.tar.gz
| Download URL | opsscript_gate-0.8.2.tar.gz |
|---|---|
| Size | 128.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9e5667bfda9ca4b2704a6f456cc835a620c173bf240dd09843494751cc59ab41
|
|
BLAKE2b-256 checksum How to use checksums |
47a0de8e4c730840d7288f8195ec1c305175cfd41e51d624ba978965b6dcb717
|
| 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 Oct 2, 2026.
Transparency logRelease files / opsscript_gate-0.8.2-py3-none-any.whl
| Download URL | opsscript_gate-0.8.2-py3-none-any.whl |
|---|---|
| Size | 36.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ded013bbb1fbcc7c67dea1c30b0c7c6435b862c80d9678ed3cf9993b4bd47ac2
|
|
BLAKE2b-256 checksum How to use checksums |
8c045b33d42851cc14485cd177fdcaff9a02680786c8e05dfc19054019a86d0c
|
| 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 Oct 2, 2026.
Transparency log