opsscript-gate 0.9.5__tar.gz → 0.9.7__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.
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/CHANGELOG.md +17 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/PKG-INFO +12 -13
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/README.md +11 -12
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/README.zh-CN.md +6 -4
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/action.yml +27 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/pyproject.toml +1 -1
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/src/opsscript_gate/__init__.py +1 -1
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/src/opsscript_gate/cli.py +84 -4
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/src/opsscript_gate/config.py +42 -2
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/src/opsscript_gate/discovery.py +6 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/src/opsscript_gate/remediation.py +13 -1
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/src/opsscript_gate/runner.py +118 -18
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/tests/test_action_contract.py +2 -2
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/tests/test_audit_coverage.py +355 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/tests/test_onboarding.py +3 -1
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.gitattributes +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/ISSUE_TEMPLATE/distro_request.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/assets/social-preview.png +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/pull_request_template.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/workflow-templates/opsscript-gate.properties.json +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/workflow-templates/opsscript-gate.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/workflows/demo.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/workflows/deploy-pages.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/workflows/release.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.github/workflows/test.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/.gitignore +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/CITATION.cff +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/CONTRIBUTING.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/LICENSE +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/ROADMAP.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/SECURITY.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/demo.html +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/command-not-found.html +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/command-not-found.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/configuration.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/gpt-oss.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/index.html +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/installer-smoke-test.html +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/installer-smoke-test.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/llms.txt +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/robots.txt +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/sitemap.xml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/docs/troubleshooting.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/examples/alpine-incompatibility/README.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/examples/alpine-incompatibility/apt_dependency.sh +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/examples/basic/README.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/examples/basic/clean_setup.sh +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/examples/github-actions/README.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/examples/github-actions/installer-smoke.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/examples/github-actions/workflow.yml +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/examples/interactive-hang/README.md +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/examples/interactive-hang/prompt_confirm.sh +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/index.html +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/llms.txt +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/src/opsscript_gate/models.py +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/src/opsscript_gate/reporter.py +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/tests/conftest.py +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/tests/fixtures/fail_deps.sh +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/tests/fixtures/fail_hang.sh +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/tests/fixtures/pass_basic.sh +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/tests/test_installer_example.py +0 -0
- {opsscript_gate-0.9.5 → opsscript_gate-0.9.7}/tests/test_runner.py +0 -0
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.7] - 2026-11-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added fast-fail support via CLI `-x` / `--fail-fast`, configuration `"fail_fast": true`, and composite action input `fail-fast` to stop immediately on the first failed check or script.
|
|
7
|
+
- Added native shebang parsing and runtime execution support for POSIX lightweight shell interpreters `dash` and `ash` (`/bin/dash`, `/usr/bin/dash`, `/usr/bin/env dash`, `/bin/ash`, `/usr/bin/ash`, `/usr/bin/env ash`).
|
|
8
|
+
- Enhanced `--workdir` integration to automatically map workspace-relative script paths and retain script `$0` identity for scripts that compute local paths via `dirname "$0"`.
|
|
9
|
+
- Added cross-distro package remediation recommendations for `dash` and `ash` missing interpreters across Debian and Alpine.
|
|
10
|
+
|
|
11
|
+
## [0.9.6] - 2026-11-08
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Added safe container environment variable injection via CLI `--env` and configuration `"env": {...}`, with strict POSIX variable name validation (`^[A-Za-z_][A-Za-z0-9_]*$`).
|
|
15
|
+
- Added host workspace context read-only mounting via CLI `--workdir` and configuration `"workdir": "..."`, automatically setting `/workspace` as the container working directory while strictly preserving host immutability (`mode: "ro"`).
|
|
16
|
+
- Added `env` and `workdir` inputs to the GitHub Composite Action with transparent argument forwarding.
|
|
17
|
+
- Expanded `COMMON_COMMAND_PACKAGES` to provide cross-distro package remediation for standard operations tools (`find`, `xargs`, `grep`, `sed`, `awk`, `diff`, `patch`, `ssh`, `rsync`, `htop`).
|
|
18
|
+
- Added Windows case-insensitive path collision protection in `_output_overwrites_protected` to prevent accidental overwrites of critical project files (e.g. `README.md`).
|
|
19
|
+
|
|
3
20
|
## [0.9.5] - 2026-11-08
|
|
4
21
|
|
|
5
22
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opsscript-gate
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.7
|
|
4
4
|
Summary: Cross-distro shell script compatibility and portability checker for Debian, Ubuntu, and Alpine. Runs real containers to diagnose command not found.
|
|
5
5
|
Project-URL: Homepage, https://mresyzz.github.io/opsscript-gate/
|
|
6
6
|
Project-URL: Demo, https://mresyzz.github.io/opsscript-gate/
|
|
@@ -81,7 +81,7 @@ apt-get --version
|
|
|
81
81
|
Run it locally with `opsscript-gate run ./install.sh`, or add the following step to CI:
|
|
82
82
|
|
|
83
83
|
```yaml
|
|
84
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
84
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
85
85
|
with:
|
|
86
86
|
script-path: install.sh
|
|
87
87
|
```
|
|
@@ -90,20 +90,19 @@ The result identifies the failing distribution, exit code, line, missing command
|
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
|
-
## Current release: v0.9.
|
|
93
|
+
## Current release: v0.9.7
|
|
94
94
|
|
|
95
|
-
The current release includes
|
|
96
|
-
saved reports, changed-script selection for pull requests, and repeat runs for detecting
|
|
97
|
-
flaky runtime behavior. The project controls
|
|
98
|
-
were introduced in v0.5.0; projects on v0.4.1 and earlier do not include them.
|
|
95
|
+
The current release includes fail-fast execution (`-x` / `--fail-fast`), native `dash` and `ash` shebang support, host workspace context mounting (`--workdir`) with relative script `$0` path preservation, safe environment variable passing (`--env`), and expanded package recommendations.
|
|
99
96
|
|
|
100
97
|
```bash
|
|
101
98
|
opsscript-gate init
|
|
102
99
|
opsscript-gate doctor
|
|
103
100
|
opsscript-gate run --dry-run
|
|
104
101
|
opsscript-gate run --format json --output reports/compatibility.json
|
|
105
|
-
#
|
|
106
|
-
opsscript-gate run
|
|
102
|
+
# Stop immediately on the first failed check
|
|
103
|
+
opsscript-gate run -x
|
|
104
|
+
# Pass environment variables and mount repository workspace read-only
|
|
105
|
+
opsscript-gate run ./install.sh --env APP_ENV=staging --workdir .
|
|
107
106
|
```
|
|
108
107
|
|
|
109
108
|
`init` creates `.opsscript-gate.json` and a GitHub Actions workflow without replacing
|
|
@@ -148,7 +147,7 @@ jobs:
|
|
|
148
147
|
- uses: actions/checkout@v7
|
|
149
148
|
with:
|
|
150
149
|
persist-credentials: false
|
|
151
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
150
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
152
151
|
```
|
|
153
152
|
|
|
154
153
|
This zero-configuration form discovers shell scripts in the repository. Use
|
|
@@ -159,7 +158,7 @@ repository has a single installer. For self-hosted model installers, see the
|
|
|
159
158
|
Or test a specific script with custom execution modes:
|
|
160
159
|
|
|
161
160
|
```yaml
|
|
162
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
161
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
163
162
|
with:
|
|
164
163
|
script-path: scripts/install.sh
|
|
165
164
|
shell: auto
|
|
@@ -193,7 +192,7 @@ unrelated scripts:
|
|
|
193
192
|
with:
|
|
194
193
|
persist-credentials: false
|
|
195
194
|
fetch-depth: 0
|
|
196
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
195
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
197
196
|
with:
|
|
198
197
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
199
198
|
preset: minimal
|
|
@@ -284,7 +283,7 @@ Use `sarif` when the result should appear in GitHub Code Scanning or another
|
|
|
284
283
|
SARIF-compatible viewer. Upload it explicitly with the official upload action:
|
|
285
284
|
|
|
286
285
|
```yaml
|
|
287
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
286
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
288
287
|
with:
|
|
289
288
|
script-path: scripts/install.sh
|
|
290
289
|
format: sarif
|
|
@@ -48,7 +48,7 @@ apt-get --version
|
|
|
48
48
|
Run it locally with `opsscript-gate run ./install.sh`, or add the following step to CI:
|
|
49
49
|
|
|
50
50
|
```yaml
|
|
51
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
51
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
52
52
|
with:
|
|
53
53
|
script-path: install.sh
|
|
54
54
|
```
|
|
@@ -57,20 +57,19 @@ The result identifies the failing distribution, exit code, line, missing command
|
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
60
|
-
## Current release: v0.9.
|
|
60
|
+
## Current release: v0.9.7
|
|
61
61
|
|
|
62
|
-
The current release includes
|
|
63
|
-
saved reports, changed-script selection for pull requests, and repeat runs for detecting
|
|
64
|
-
flaky runtime behavior. The project controls
|
|
65
|
-
were introduced in v0.5.0; projects on v0.4.1 and earlier do not include them.
|
|
62
|
+
The current release includes fail-fast execution (`-x` / `--fail-fast`), native `dash` and `ash` shebang support, host workspace context mounting (`--workdir`) with relative script `$0` path preservation, safe environment variable passing (`--env`), and expanded package recommendations.
|
|
66
63
|
|
|
67
64
|
```bash
|
|
68
65
|
opsscript-gate init
|
|
69
66
|
opsscript-gate doctor
|
|
70
67
|
opsscript-gate run --dry-run
|
|
71
68
|
opsscript-gate run --format json --output reports/compatibility.json
|
|
72
|
-
#
|
|
73
|
-
opsscript-gate run
|
|
69
|
+
# Stop immediately on the first failed check
|
|
70
|
+
opsscript-gate run -x
|
|
71
|
+
# Pass environment variables and mount repository workspace read-only
|
|
72
|
+
opsscript-gate run ./install.sh --env APP_ENV=staging --workdir .
|
|
74
73
|
```
|
|
75
74
|
|
|
76
75
|
`init` creates `.opsscript-gate.json` and a GitHub Actions workflow without replacing
|
|
@@ -115,7 +114,7 @@ jobs:
|
|
|
115
114
|
- uses: actions/checkout@v7
|
|
116
115
|
with:
|
|
117
116
|
persist-credentials: false
|
|
118
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
117
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
119
118
|
```
|
|
120
119
|
|
|
121
120
|
This zero-configuration form discovers shell scripts in the repository. Use
|
|
@@ -126,7 +125,7 @@ repository has a single installer. For self-hosted model installers, see the
|
|
|
126
125
|
Or test a specific script with custom execution modes:
|
|
127
126
|
|
|
128
127
|
```yaml
|
|
129
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
128
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
130
129
|
with:
|
|
131
130
|
script-path: scripts/install.sh
|
|
132
131
|
shell: auto
|
|
@@ -160,7 +159,7 @@ unrelated scripts:
|
|
|
160
159
|
with:
|
|
161
160
|
persist-credentials: false
|
|
162
161
|
fetch-depth: 0
|
|
163
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
162
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
164
163
|
with:
|
|
165
164
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
166
165
|
preset: minimal
|
|
@@ -251,7 +250,7 @@ Use `sarif` when the result should appear in GitHub Code Scanning or another
|
|
|
251
250
|
SARIF-compatible viewer. Upload it explicitly with the official upload action:
|
|
252
251
|
|
|
253
252
|
```yaml
|
|
254
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
253
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
255
254
|
with:
|
|
256
255
|
script-path: scripts/install.sh
|
|
257
256
|
format: sarif
|
|
@@ -17,9 +17,9 @@ Bash 依赖、包管理器假设和交互式阻塞。它与 ShellCheck 互补。
|
|
|
17
17
|
|
|
18
18
|
也可以直接打开 [OpsScript Gate 在线演示](https://mresyzz.github.io/opsscript-gate/),先查看 ShellCheck 与运行时验证的差异,再复制 workflow 到自己的仓库。
|
|
19
19
|
|
|
20
|
-
## 当前版本 v0.9.
|
|
20
|
+
## 当前版本 v0.9.7
|
|
21
21
|
|
|
22
|
-
v0.9.
|
|
22
|
+
v0.9.7 是当前已发布版本;如果你要从源码验证当前分支,也可以执行:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
pip install -e .
|
|
@@ -33,9 +33,11 @@ opsscript-gate init
|
|
|
33
33
|
opsscript-gate doctor
|
|
34
34
|
opsscript-gate run --dry-run
|
|
35
35
|
opsscript-gate run --format json --output reports/compatibility.json
|
|
36
|
+
# 开启失败即停(-x / --fail-fast),传递环境变量并只读挂载工作区
|
|
37
|
+
opsscript-gate run ./install.sh -x --env APP_ENV=staging --workdir .
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
`init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.9.
|
|
40
|
+
`init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.9.7`。
|
|
39
41
|
预览不需要 Docker;真实运行需要 Python 3.10+
|
|
40
42
|
和可访问的 Linux Docker 引擎。
|
|
41
43
|
`opsscript-gate doctor` 可以在真实运行前检查 Python 和 Docker。
|
|
@@ -74,7 +76,7 @@ Docker daemon 和可选的项目配置。
|
|
|
74
76
|
- uses: actions/checkout@v7
|
|
75
77
|
with:
|
|
76
78
|
fetch-depth: 0
|
|
77
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
79
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
78
80
|
with:
|
|
79
81
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
80
82
|
preset: minimal
|
|
@@ -50,6 +50,14 @@ inputs:
|
|
|
50
50
|
description: 'Optional comma- or newline-separated packages to install before running (requires network=bridge)'
|
|
51
51
|
required: false
|
|
52
52
|
default: ''
|
|
53
|
+
env:
|
|
54
|
+
description: 'Newline-separated list of environment variables to pass into containers (e.g. KEY=VALUE or KEY)'
|
|
55
|
+
required: false
|
|
56
|
+
default: ''
|
|
57
|
+
workdir:
|
|
58
|
+
description: 'Host directory to mount read-only into /workspace as the container working directory'
|
|
59
|
+
required: false
|
|
60
|
+
default: ''
|
|
53
61
|
|
|
54
62
|
config:
|
|
55
63
|
description: 'Path to project JSON configuration'
|
|
@@ -71,6 +79,10 @@ inputs:
|
|
|
71
79
|
description: 'Preview the execution plan without Docker'
|
|
72
80
|
required: false
|
|
73
81
|
default: 'false'
|
|
82
|
+
fail-fast:
|
|
83
|
+
description: 'Stop immediately on the first failed distribution or script check (default: false)'
|
|
84
|
+
required: false
|
|
85
|
+
default: 'false'
|
|
74
86
|
output:
|
|
75
87
|
description: 'Save a report file for upload-artifact or other tooling'
|
|
76
88
|
required: false
|
|
@@ -116,11 +128,14 @@ runs:
|
|
|
116
128
|
INPUT_PIDS_LIMIT: ${{ inputs.pids-limit }}
|
|
117
129
|
INPUT_NETWORK: ${{ inputs.network }}
|
|
118
130
|
INPUT_PACKAGES: ${{ inputs.packages }}
|
|
131
|
+
INPUT_ENV: ${{ inputs.env }}
|
|
132
|
+
INPUT_WORKDIR: ${{ inputs.workdir }}
|
|
119
133
|
INPUT_CONFIG: ${{ inputs.config }}
|
|
120
134
|
INPUT_PRESET: ${{ inputs.preset }}
|
|
121
135
|
INPUT_EXCLUDE: ${{ inputs.exclude }}
|
|
122
136
|
INPUT_MAX_SCRIPTS: ${{ inputs.max-scripts }}
|
|
123
137
|
INPUT_DRY_RUN: ${{ inputs.dry-run }}
|
|
138
|
+
INPUT_FAIL_FAST: ${{ inputs.fail-fast }}
|
|
124
139
|
INPUT_OUTPUT: ${{ inputs.output }}
|
|
125
140
|
INPUT_CHANGED_SINCE: ${{ inputs.changed-since }}
|
|
126
141
|
run: |
|
|
@@ -137,6 +152,7 @@ runs:
|
|
|
137
152
|
[ -z "$INPUT_PIDS_LIMIT" ] || ARGS+=(--pids-limit "$INPUT_PIDS_LIMIT")
|
|
138
153
|
[ -z "$INPUT_NETWORK" ] || ARGS+=(--network "$INPUT_NETWORK")
|
|
139
154
|
[ -z "$INPUT_PACKAGES" ] || ARGS+=(--packages "$INPUT_PACKAGES")
|
|
155
|
+
[ -z "$INPUT_WORKDIR" ] || ARGS+=(--workdir "$INPUT_WORKDIR")
|
|
140
156
|
[ -z "$INPUT_MATRIX" ] || ARGS+=(--matrix "$INPUT_MATRIX")
|
|
141
157
|
[ -z "$INPUT_JOBS" ] || ARGS+=(--jobs "$INPUT_JOBS")
|
|
142
158
|
[ -z "$INPUT_CONFIG" ] || ARGS+=(--config "$INPUT_CONFIG")
|
|
@@ -144,10 +160,21 @@ runs:
|
|
|
144
160
|
[ -z "$INPUT_MAX_SCRIPTS" ] || ARGS+=(--max-scripts "$INPUT_MAX_SCRIPTS")
|
|
145
161
|
[ -z "$INPUT_OUTPUT" ] || ARGS+=(--output "$INPUT_OUTPUT")
|
|
146
162
|
[ -z "$INPUT_CHANGED_SINCE" ] || ARGS+=(--changed-since "$INPUT_CHANGED_SINCE")
|
|
163
|
+
while IFS= read -r env_var; do
|
|
164
|
+
env_var="${env_var%$'\r'}"
|
|
165
|
+
[ -z "$env_var" ] || ARGS+=("--env" "$env_var")
|
|
166
|
+
done <<< "$INPUT_ENV"
|
|
147
167
|
while IFS= read -r pattern; do
|
|
148
168
|
pattern="${pattern%$'\r'}"
|
|
149
169
|
[ -z "$pattern" ] || ARGS+=("--exclude" "$pattern")
|
|
150
170
|
done <<< "$INPUT_EXCLUDE"
|
|
171
|
+
FAIL_FAST_VALUE="$(printf '%s' "$INPUT_FAIL_FAST" | tr '[:upper:]' '[:lower:]')"
|
|
172
|
+
if [ "$FAIL_FAST_VALUE" = "true" ]; then
|
|
173
|
+
ARGS+=("--fail-fast")
|
|
174
|
+
elif [ "$FAIL_FAST_VALUE" != "false" ]; then
|
|
175
|
+
echo "fail-fast must be true or false" >&2
|
|
176
|
+
exit 1
|
|
177
|
+
fi
|
|
151
178
|
DRY_RUN_VALUE="$(printf '%s' "$INPUT_DRY_RUN" | tr '[:upper:]' '[:lower:]')"
|
|
152
179
|
if [ "$DRY_RUN_VALUE" = "true" ]; then
|
|
153
180
|
ARGS+=("--dry-run")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "opsscript-gate"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.7"
|
|
8
8
|
description = "Cross-distro shell script compatibility and portability checker for Debian, Ubuntu, and Alpine. Runs real containers to diagnose command not found."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -9,7 +9,20 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
from opsscript_gate import __version__
|
|
11
11
|
from opsscript_gate.discovery import discover_changed_scripts, discover_scripts
|
|
12
|
-
from opsscript_gate.config import
|
|
12
|
+
from opsscript_gate.config import (
|
|
13
|
+
DEFAULTS,
|
|
14
|
+
ENV_KEY_RE,
|
|
15
|
+
MAX_MATRIX_IMAGES,
|
|
16
|
+
MAX_REPEAT_ATTEMPTS,
|
|
17
|
+
MAX_TIMEOUT_SECONDS,
|
|
18
|
+
PRESETS,
|
|
19
|
+
init_project,
|
|
20
|
+
load_config,
|
|
21
|
+
validate_env,
|
|
22
|
+
validate_memory_limit,
|
|
23
|
+
validate_packages,
|
|
24
|
+
validate_workdir,
|
|
25
|
+
)
|
|
13
26
|
from opsscript_gate.models import MultiScriptReport, RunReport
|
|
14
27
|
from opsscript_gate.reporter import (
|
|
15
28
|
emit_github_annotations,
|
|
@@ -144,6 +157,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
144
157
|
default=None,
|
|
145
158
|
help="Optional comma-separated packages to install in each container before running (repeatable; requires network=bridge)",
|
|
146
159
|
)
|
|
160
|
+
run_parser.add_argument(
|
|
161
|
+
"--env",
|
|
162
|
+
action="append",
|
|
163
|
+
default=None,
|
|
164
|
+
help="Environment variable to pass into containers in KEY=VALUE or KEY format (repeatable)",
|
|
165
|
+
)
|
|
166
|
+
run_parser.add_argument(
|
|
167
|
+
"--workdir",
|
|
168
|
+
type=str,
|
|
169
|
+
default=None,
|
|
170
|
+
help="Host directory to mount read-only into /workspace as the container working directory",
|
|
171
|
+
)
|
|
172
|
+
run_parser.add_argument(
|
|
173
|
+
"-x", "--fail-fast",
|
|
174
|
+
action="store_true",
|
|
175
|
+
default=False,
|
|
176
|
+
help="Stop immediately on the first failed distribution or script check",
|
|
177
|
+
)
|
|
147
178
|
run_parser.add_argument(
|
|
148
179
|
"--changed-since",
|
|
149
180
|
help="Only test shell scripts changed between this Git revision and HEAD",
|
|
@@ -181,6 +212,33 @@ def parse_packages_argument(packages_raw: list[str] | None) -> list[str]:
|
|
|
181
212
|
return validate_packages(packages)
|
|
182
213
|
|
|
183
214
|
|
|
215
|
+
def parse_env_arguments(
|
|
216
|
+
env_args: list[str] | None,
|
|
217
|
+
base_env: dict[str, str] | None = None,
|
|
218
|
+
) -> dict[str, str]:
|
|
219
|
+
"""Parse repeated --env arguments, resolving KEY=VALUE or KEY from host env."""
|
|
220
|
+
merged: dict[str, str] = dict(base_env or {})
|
|
221
|
+
if not env_args:
|
|
222
|
+
return validate_env(merged) if merged else {}
|
|
223
|
+
for item in env_args:
|
|
224
|
+
for entry in item.splitlines():
|
|
225
|
+
entry = entry.strip()
|
|
226
|
+
if not entry:
|
|
227
|
+
continue
|
|
228
|
+
if "=" in entry:
|
|
229
|
+
key, val = entry.split("=", 1)
|
|
230
|
+
else:
|
|
231
|
+
key = entry
|
|
232
|
+
val = os.environ.get(key, "")
|
|
233
|
+
key = key.strip()
|
|
234
|
+
if not ENV_KEY_RE.fullmatch(key):
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"Invalid environment variable name: '{key}'. Must be a valid POSIX identifier (^[A-Za-z_][A-Za-z0-9_]*$)"
|
|
237
|
+
)
|
|
238
|
+
merged[key] = val
|
|
239
|
+
return validate_env(merged)
|
|
240
|
+
|
|
241
|
+
|
|
184
242
|
def run_checked_matrix(
|
|
185
243
|
args: argparse.Namespace, script_path: str, matrix: list[str]
|
|
186
244
|
) -> RunReport | None:
|
|
@@ -197,6 +255,9 @@ def run_checked_matrix(
|
|
|
197
255
|
network=args.network,
|
|
198
256
|
packages=args.packages,
|
|
199
257
|
repeat=args.repeat,
|
|
258
|
+
env=args.env,
|
|
259
|
+
workdir=args.workdir,
|
|
260
|
+
fail_fast=getattr(args, "fail_fast", False),
|
|
200
261
|
)
|
|
201
262
|
except DockerDaemonError as err:
|
|
202
263
|
sys.stderr.write(f"Docker Error: {err}\n")
|
|
@@ -207,8 +268,11 @@ def run_checked_matrix(
|
|
|
207
268
|
|
|
208
269
|
def _output_overwrites_protected(output_path: Path, protected: list[Path]) -> bool:
|
|
209
270
|
"""Reject paths that resolve to or share an inode with a protected file."""
|
|
210
|
-
|
|
211
|
-
|
|
271
|
+
for p in protected:
|
|
272
|
+
if output_path == p:
|
|
273
|
+
return True
|
|
274
|
+
if os.name == "nt" and str(output_path).lower() == str(p).lower():
|
|
275
|
+
return True
|
|
212
276
|
if not output_path.exists():
|
|
213
277
|
return False
|
|
214
278
|
for target in protected:
|
|
@@ -293,6 +357,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
293
357
|
explicit = {token.split("=", 1)[0] for token in argv if token.startswith("--")}
|
|
294
358
|
if any(token.startswith("-j") for token in argv):
|
|
295
359
|
explicit.add("--jobs")
|
|
360
|
+
if any(token == "-x" or token.startswith("-x") for token in argv):
|
|
361
|
+
explicit.add("--fail-fast")
|
|
296
362
|
for key in DEFAULTS:
|
|
297
363
|
if key in config and "--" + key.replace("_", "-") not in explicit:
|
|
298
364
|
setattr(args, key, config[key])
|
|
@@ -316,6 +382,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
316
382
|
args.packages = parse_packages_argument(args.packages)
|
|
317
383
|
if args.packages and args.network == "none":
|
|
318
384
|
raise ValueError("packages require network=bridge")
|
|
385
|
+
base_env = config.get("env") if isinstance(config.get("env"), dict) else {}
|
|
386
|
+
args.env = parse_env_arguments(args.env, base_env=base_env)
|
|
387
|
+
if args.workdir:
|
|
388
|
+
args.workdir = validate_workdir(args.workdir)
|
|
319
389
|
if args.matrix is not None and not any(x.strip() for x in args.matrix.split(",")):
|
|
320
390
|
raise ValueError("matrix must contain at least one image")
|
|
321
391
|
except ValueError as err:
|
|
@@ -385,14 +455,21 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
385
455
|
plan = {"dry_run": True, "scripts": target_scripts, "matrix": matrix,
|
|
386
456
|
"executions": len(target_scripts) * len(matrix) * args.repeat, "shell": args.shell,
|
|
387
457
|
"network": args.network, "packages": args.packages, "timeout": args.timeout,
|
|
388
|
-
"repeat": args.repeat
|
|
458
|
+
"repeat": args.repeat, "env": args.env, "workdir": args.workdir,
|
|
459
|
+
"fail_fast": args.fail_fast}
|
|
389
460
|
if changed_selection:
|
|
390
461
|
plan["changed_since"] = args.changed_since
|
|
462
|
+
env_preview = f"\nEnv: {', '.join(f'{k}={v}' for k, v in args.env.items())}" if args.env else ""
|
|
463
|
+
workdir_preview = f"\nWorkdir: {args.workdir}" if args.workdir else ""
|
|
464
|
+
fail_fast_preview = f"\nFail-Fast: {args.fail_fast}" if args.fail_fast else ""
|
|
391
465
|
output = json.dumps(plan, indent=2) if args.format == "json" else (
|
|
392
466
|
"Execution preview (no scripts executed)\n"
|
|
393
467
|
+ "\n".join(f" {path}" for path in target_scripts)
|
|
394
468
|
+ f"\nImages: {', '.join(matrix)}\nContainer executions: {plan['executions']}"
|
|
395
469
|
+ (f"\nPackages: {', '.join(args.packages)}" if args.packages else "")
|
|
470
|
+
+ env_preview
|
|
471
|
+
+ workdir_preview
|
|
472
|
+
+ fail_fast_preview
|
|
396
473
|
+ f"\nShell: {args.shell} | Network: {args.network} | Timeout: {args.timeout}s | Repeat: {args.repeat}"
|
|
397
474
|
)
|
|
398
475
|
return 0 if publish_output(output, args.output) else 1
|
|
@@ -455,6 +532,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
455
532
|
if args.format not in ("json", "sarif"):
|
|
456
533
|
emit_github_annotations(report, script_path=script_path)
|
|
457
534
|
|
|
535
|
+
if args.fail_fast and not report.all_passed:
|
|
536
|
+
break
|
|
537
|
+
|
|
458
538
|
total_duration = time.perf_counter() - multi_start
|
|
459
539
|
all_passed = all(r.all_passed for r in multi_reports.values())
|
|
460
540
|
multi_report = MultiScriptReport(
|
|
@@ -17,7 +17,7 @@ DEFAULTS = {
|
|
|
17
17
|
"matrix": None, "preset": None, "jobs": None, "timeout": 60,
|
|
18
18
|
"shell": "posix", "mem_limit": "256m", "pids_limit": 128,
|
|
19
19
|
"network": "bridge", "packages": [], "exclude": [], "max_scripts": 20,
|
|
20
|
-
"repeat": 1,
|
|
20
|
+
"repeat": 1, "env": {}, "workdir": None, "fail_fast": False,
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
MAX_CONFIG_SIZE_BYTES = 256 * 1024
|
|
@@ -26,6 +26,39 @@ MAX_TIMEOUT_SECONDS = 3600
|
|
|
26
26
|
MAX_MATRIX_IMAGES = 32
|
|
27
27
|
|
|
28
28
|
PACKAGE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9+._:-]*$")
|
|
29
|
+
ENV_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_env(value: object) -> dict[str, str]:
|
|
33
|
+
"""Validate and normalize environment variables specified in configuration or CLI."""
|
|
34
|
+
if not isinstance(value, dict):
|
|
35
|
+
raise ValueError("env must be a JSON object mapping variable names to string values")
|
|
36
|
+
normalized: dict[str, str] = {}
|
|
37
|
+
for k, v in value.items():
|
|
38
|
+
if not isinstance(k, str) or not ENV_KEY_RE.fullmatch(k):
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Invalid environment variable name '{k}'. Must be a valid POSIX identifier (^[A-Za-z_][A-Za-z0-9_]*$)"
|
|
41
|
+
)
|
|
42
|
+
if not isinstance(v, str):
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Environment variable '{k}' value must be a string, got {type(v).__name__}"
|
|
45
|
+
)
|
|
46
|
+
normalized[k] = v
|
|
47
|
+
return normalized
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_workdir(value: object) -> str:
|
|
51
|
+
"""Validate that the workspace directory exists, is a directory, and is not root."""
|
|
52
|
+
if not isinstance(value, str) or not value.strip():
|
|
53
|
+
raise ValueError("workdir must be a nonempty path string")
|
|
54
|
+
path = Path(value.strip()).resolve()
|
|
55
|
+
if not path.exists():
|
|
56
|
+
raise ValueError(f"workdir directory does not exist: {value}")
|
|
57
|
+
if not path.is_dir():
|
|
58
|
+
raise ValueError(f"workdir is not a directory: {value}")
|
|
59
|
+
if path == path.parent:
|
|
60
|
+
raise ValueError(f"workdir must not be the root directory: {value}")
|
|
61
|
+
return str(path)
|
|
29
62
|
|
|
30
63
|
|
|
31
64
|
def validate_memory_limit(value: object) -> str:
|
|
@@ -93,6 +126,13 @@ def load_config(filename: str | None) -> dict:
|
|
|
93
126
|
validate_packages(value)
|
|
94
127
|
elif key == "mem_limit":
|
|
95
128
|
validate_memory_limit(value)
|
|
129
|
+
elif key == "env":
|
|
130
|
+
validate_env(value)
|
|
131
|
+
elif key == "workdir":
|
|
132
|
+
validate_workdir(value)
|
|
133
|
+
elif key == "fail_fast":
|
|
134
|
+
if not isinstance(value, bool):
|
|
135
|
+
raise ValueError("fail_fast must be a boolean")
|
|
96
136
|
elif not isinstance(value, str) or not value.strip():
|
|
97
137
|
raise ValueError(f"{key} must be a nonempty string")
|
|
98
138
|
for key, choices in {"shell": ("posix", "auto", "shebang"),
|
|
@@ -126,7 +166,7 @@ jobs:
|
|
|
126
166
|
- uses: actions/checkout@v7
|
|
127
167
|
with:
|
|
128
168
|
persist-credentials: false
|
|
129
|
-
- uses: Mresyzz/opsscript-gate@v0.9.
|
|
169
|
+
- uses: Mresyzz/opsscript-gate@v0.9.7
|
|
130
170
|
with:
|
|
131
171
|
config: .opsscript-gate.json
|
|
132
172
|
""",
|
|
@@ -34,6 +34,12 @@ SHELL_SHEBANG_PREFIXES = (
|
|
|
34
34
|
"#!/usr/bin/bash",
|
|
35
35
|
"#!/usr/bin/env sh",
|
|
36
36
|
"#!/usr/bin/env bash",
|
|
37
|
+
"#!/bin/dash",
|
|
38
|
+
"#!/usr/bin/dash",
|
|
39
|
+
"#!/usr/bin/env dash",
|
|
40
|
+
"#!/bin/ash",
|
|
41
|
+
"#!/usr/bin/ash",
|
|
42
|
+
"#!/usr/bin/env ash",
|
|
37
43
|
)
|
|
38
44
|
|
|
39
45
|
MAX_DISCOVERED_SCRIPTS = 20
|
|
@@ -89,7 +89,7 @@ def _match_interpreter(distro: str | None, command: str | None, kind: str) -> bo
|
|
|
89
89
|
return True
|
|
90
90
|
if command and (command.startswith("/bin/") or command.startswith("/usr/bin/")):
|
|
91
91
|
base = _normalize_cmd_basename(command)
|
|
92
|
-
if base in ("bash", "sh", "zsh", "python", "python3", "perl"):
|
|
92
|
+
if base in ("bash", "sh", "dash", "ash", "zsh", "python", "python3", "perl"):
|
|
93
93
|
return True
|
|
94
94
|
return False
|
|
95
95
|
|
|
@@ -123,6 +123,18 @@ COMMON_COMMAND_PACKAGES: dict[str, dict[str, str]] = {
|
|
|
123
123
|
"envsubst": {"debian": "gettext-base", "alpine": "gettext", "rhel": "gettext"},
|
|
124
124
|
"crontab": {"debian": "cron", "alpine": "dcron", "rhel": "cronie"},
|
|
125
125
|
"ps": {"debian": "procps", "alpine": "procps", "rhel": "procps-ng"},
|
|
126
|
+
"find": {"debian": "findutils", "alpine": "findutils", "rhel": "findutils"},
|
|
127
|
+
"xargs": {"debian": "findutils", "alpine": "findutils", "rhel": "findutils"},
|
|
128
|
+
"grep": {"debian": "grep", "alpine": "grep", "rhel": "grep"},
|
|
129
|
+
"sed": {"debian": "sed", "alpine": "sed", "rhel": "sed"},
|
|
130
|
+
"awk": {"debian": "gawk", "alpine": "gawk", "rhel": "gawk"},
|
|
131
|
+
"diff": {"debian": "diffutils", "alpine": "diffutils", "rhel": "diffutils"},
|
|
132
|
+
"patch": {"debian": "patch", "alpine": "patch", "rhel": "patch"},
|
|
133
|
+
"ssh": {"debian": "openssh-client", "alpine": "openssh-client", "rhel": "openssh-clients"},
|
|
134
|
+
"rsync": {"debian": "rsync", "alpine": "rsync", "rhel": "rsync"},
|
|
135
|
+
"htop": {"debian": "htop", "alpine": "htop", "rhel": "htop"},
|
|
136
|
+
"dash": {"debian": "dash", "alpine": "dash", "rhel": "dash"},
|
|
137
|
+
"ash": {"debian": "ash", "alpine": "busybox", "rhel": "busybox"},
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
|