opsscript-gate 0.6.3__tar.gz → 0.7.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 (48) hide show
  1. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/workflow-templates/opsscript-gate.yml +1 -1
  2. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/CHANGELOG.md +6 -1
  3. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/CITATION.cff +1 -1
  4. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/PKG-INFO +37 -19
  5. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/README.md +36 -18
  6. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/README.zh-CN.md +8 -4
  7. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/action.yml +1 -1
  8. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/docs/configuration.md +3 -3
  9. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/docs/gpt-oss.md +1 -1
  10. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/examples/github-actions/README.md +1 -1
  11. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/examples/github-actions/workflow.yml +1 -1
  12. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/pyproject.toml +1 -1
  13. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/src/opsscript_gate/__init__.py +1 -1
  14. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/src/opsscript_gate/cli.py +8 -2
  15. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/src/opsscript_gate/config.py +1 -1
  16. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/src/opsscript_gate/reporter.py +120 -0
  17. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/tests/test_action_contract.py +1 -0
  18. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/tests/test_runner.py +50 -0
  19. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.gitattributes +0 -0
  20. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  21. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/ISSUE_TEMPLATE/distro_request.yml +0 -0
  22. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  23. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/assets/social-preview.png +0 -0
  24. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/pull_request_template.md +0 -0
  25. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/workflow-templates/properties.json +0 -0
  26. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/workflows/demo.yml +0 -0
  27. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/workflows/release.yml +0 -0
  28. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.github/workflows/test.yml +0 -0
  29. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/.gitignore +0 -0
  30. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/CONTRIBUTING.md +0 -0
  31. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/LICENSE +0 -0
  32. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/ROADMAP.md +0 -0
  33. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/SECURITY.md +0 -0
  34. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/docs/troubleshooting.md +0 -0
  35. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/examples/alpine-incompatibility/README.md +0 -0
  36. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/examples/alpine-incompatibility/apt_dependency.sh +0 -0
  37. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/examples/basic/README.md +0 -0
  38. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/examples/basic/clean_setup.sh +0 -0
  39. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/examples/interactive-hang/README.md +0 -0
  40. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/examples/interactive-hang/prompt_confirm.sh +0 -0
  41. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/src/opsscript_gate/discovery.py +0 -0
  42. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/src/opsscript_gate/models.py +0 -0
  43. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/src/opsscript_gate/remediation.py +0 -0
  44. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/src/opsscript_gate/runner.py +0 -0
  45. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/tests/fixtures/fail_deps.sh +0 -0
  46. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/tests/fixtures/fail_hang.sh +0 -0
  47. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/tests/fixtures/pass_basic.sh +0 -0
  48. {opsscript_gate-0.6.3 → opsscript_gate-0.7.0}/tests/test_onboarding.py +0 -0
@@ -25,7 +25,7 @@ jobs:
25
25
  persist-credentials: false
26
26
 
27
27
  - name: Run OpsScript Gate
28
- uses: Mresyzz/opsscript-gate@v0.6.3
28
+ uses: Mresyzz/opsscript-gate@v0.7.0
29
29
  with:
30
30
  shell: auto
31
31
  network: none
@@ -1,6 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## [0.6.3] - 2026-10-01
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+ - Added SARIF 2.1.0 output for location-aware GitHub Code Scanning integration.
7
+
8
+ ## [0.7.0] - 2026-10-01
4
9
 
5
10
  ### Added
6
11
  - Added `opsscript-gate doctor` to diagnose Python and Docker prerequisites before a local run.
@@ -5,7 +5,7 @@ type: software
5
5
  authors:
6
6
  - family-names: Mresyzz
7
7
  given-names: Mresy
8
- version: 0.6.3
8
+ version: 0.7.0
9
9
  date-released: 2026-10-01
10
10
  repository-code: https://github.com/Mresyzz/opsscript-gate
11
11
  url: https://github.com/Mresyzz/opsscript-gate
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opsscript-gate
3
- Version: 0.6.3
3
+ Version: 0.7.0
4
4
  Summary: Run Linux shell scripts in Debian, Ubuntu and Alpine containers to catch runtime compatibility failures before merge
5
5
  Project-URL: Homepage, https://mresyzz.github.io/InstallReady/
6
6
  Project-URL: Demo, https://mresyzz.github.io/InstallReady/?repo=Mresyzz%2Fopsscript-gate
@@ -48,15 +48,15 @@ Description-Content-Type: text/markdown
48
48
  It is designed for shell testing, portable shell validation, Bash/POSIX compatibility checks, and cross-distro CI where syntax-only tooling is not enough.
49
49
 
50
50
  If your repository contains an installer, bootstrap script, or shell entrypoint, the fastest path is to
51
- copy the [zero-configuration workflow](.github/workflow-templates/opsscript-gate.yml). It only runs when
52
- shell files change, uses offline containers by default, and uploads a report that can be attached to a PR.
53
- No account, API key, or service signup is required.
51
+ open the [InstallReady scanner](https://mresyzz.github.io/InstallReady/), enter `owner/repository`, and
52
+ choose **Add workflow on GitHub**. It generates a reviewable workflow without an account, API key, or
53
+ service signup. You can also copy the maintained [zero-configuration workflow](.github/workflow-templates/opsscript-gate.yml).
54
54
 
55
55
  Try the browser scanner: [InstallReady](https://mresyzz.github.io/InstallReady/?repo=Mresyzz%2Fopsscript-gate) checks this repository without executing code or requiring a login.
56
56
 
57
57
  ---
58
58
 
59
- ## Current release: v0.6.3
59
+ ## Current release: v0.7.0
60
60
 
61
61
  The current release includes a project config file, a dry-run plan, presets, exclusions,
62
62
  saved reports, and changed-script selection for pull requests. The project controls
@@ -86,8 +86,9 @@ See [configuration and migration](docs/configuration.md) and
86
86
 
87
87
  Add this workflow to `.github/workflows/shell-compat.yml`:
88
88
 
89
- You can also select **OpsScript Gate shell compatibility** from GitHub's workflow templates, or copy the
90
- maintained [starter workflow](.github/workflow-templates/opsscript-gate.yml).
89
+ The repository includes a maintained [workflow template](.github/workflow-templates/opsscript-gate.yml).
90
+ For a one-click setup in another public repository, use the InstallReady scanner above; it opens GitHub's
91
+ workflow editor with the generated file ready to review.
91
92
 
92
93
  You can copy the maintained example from [`examples/github-actions/workflow.yml`](examples/github-actions/workflow.yml)
93
94
  and change the `script-path`, or use the minimal workflow below.
@@ -101,7 +102,9 @@ jobs:
101
102
  runs-on: ubuntu-latest
102
103
  steps:
103
104
  - uses: actions/checkout@v7
104
- - uses: Mresyzz/opsscript-gate@v0.6.3
105
+ with:
106
+ persist-credentials: false
107
+ - uses: Mresyzz/opsscript-gate@v0.7.0
105
108
  ```
106
109
 
107
110
  This zero-configuration form discovers shell scripts in the repository. Use
@@ -112,7 +115,7 @@ repository has a single installer. For self-hosted model installers, see the
112
115
  Or test a specific script with custom execution modes:
113
116
 
114
117
  ```yaml
115
- - uses: Mresyzz/opsscript-gate@v0.6.3
118
+ - uses: Mresyzz/opsscript-gate@v0.7.0
116
119
  with:
117
120
  script-path: scripts/install.sh
118
121
  shell: auto
@@ -142,8 +145,9 @@ unrelated scripts:
142
145
  ```yaml
143
146
  - uses: actions/checkout@v7
144
147
  with:
148
+ persist-credentials: false
145
149
  fetch-depth: 0
146
- - uses: Mresyzz/opsscript-gate@v0.6.3
150
+ - uses: Mresyzz/opsscript-gate@v0.7.0
147
151
  with:
148
152
  changed-since: ${{ github.event.pull_request.base.sha }}
149
153
  preset: minimal
@@ -220,6 +224,28 @@ The action also writes a markdown matrix to `$GITHUB_STEP_SUMMARY`:
220
224
  </details>
221
225
  ```
222
226
 
227
+ ### SARIF for Code Scanning
228
+
229
+ Use `sarif` when the result should appear in GitHub Code Scanning or another
230
+ SARIF-compatible viewer. Upload it explicitly with the official upload action:
231
+
232
+ ```yaml
233
+ - uses: Mresyzz/opsscript-gate@v0.7.0
234
+ with:
235
+ script-path: scripts/install.sh
236
+ format: sarif
237
+ output: reports/opsscript-gate.sarif
238
+
239
+ - uses: github/codeql-action/upload-sarif@v3
240
+ if: always()
241
+ with:
242
+ sarif_file: reports/opsscript-gate.sarif
243
+ ```
244
+
245
+ Each runtime failure becomes a location-aware result with the distribution,
246
+ exit code, command, and diagnostic line. A passing matrix produces a valid SARIF
247
+ file with no findings.
248
+
223
249
  ---
224
250
 
225
251
  ## Example: runtime behavior that static analysis misses
@@ -376,7 +402,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
376
402
  | `-j, --jobs` | Integer | `min(2, size)` | Number of concurrent container jobs |
377
403
  | `--matrix` | String | `debian:12-slim,ubuntu:22.04,ubuntu:24.04,alpine:3.20` | Comma-separated list of Docker images |
378
404
  | `--timeout` | Integer | `60` | Hard timeout per container in seconds |
379
- | `--format` | Choice | `table` | Output format: `table`, `markdown`, or `json` |
405
+ | `--format` | Choice | `table` | Output format: `table`, `markdown`, `json`, or `sarif` |
380
406
  | `--shell` | Choice | `posix` | Execution mode: `posix` (default), `shebang`, or `auto` |
381
407
  | `--mem-limit` | String | `256m` | Memory limit per container (e.g. `256m`, `512m`) |
382
408
  | `--pids-limit`| Integer | `128` | Maximum number of processes per container |
@@ -413,14 +439,6 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
413
439
  Explicit CLI options override project settings. Explicit script paths bypass discovery
414
440
  and its exclusions. See [complete configuration semantics](docs/configuration.md).
415
441
 
416
- ## Downstream example
417
-
418
- [`Mresyzz/linux-dev-bootstrap`](https://github.com/Mresyzz/linux-dev-bootstrap) contains a workflow that validates `install.sh` across the default Debian, Ubuntu, and Alpine matrix.
419
-
420
- See the downstream workflow: [`.github/workflows/test.yml`](https://github.com/Mresyzz/linux-dev-bootstrap/blob/main/.github/workflows/test.yml).
421
-
422
- ---
423
-
424
442
  ## 🧪 Development & Testing
425
443
 
426
444
  ```bash
@@ -18,15 +18,15 @@
18
18
  It is designed for shell testing, portable shell validation, Bash/POSIX compatibility checks, and cross-distro CI where syntax-only tooling is not enough.
19
19
 
20
20
  If your repository contains an installer, bootstrap script, or shell entrypoint, the fastest path is to
21
- copy the [zero-configuration workflow](.github/workflow-templates/opsscript-gate.yml). It only runs when
22
- shell files change, uses offline containers by default, and uploads a report that can be attached to a PR.
23
- No account, API key, or service signup is required.
21
+ open the [InstallReady scanner](https://mresyzz.github.io/InstallReady/), enter `owner/repository`, and
22
+ choose **Add workflow on GitHub**. It generates a reviewable workflow without an account, API key, or
23
+ service signup. You can also copy the maintained [zero-configuration workflow](.github/workflow-templates/opsscript-gate.yml).
24
24
 
25
25
  Try the browser scanner: [InstallReady](https://mresyzz.github.io/InstallReady/?repo=Mresyzz%2Fopsscript-gate) checks this repository without executing code or requiring a login.
26
26
 
27
27
  ---
28
28
 
29
- ## Current release: v0.6.3
29
+ ## Current release: v0.7.0
30
30
 
31
31
  The current release includes a project config file, a dry-run plan, presets, exclusions,
32
32
  saved reports, and changed-script selection for pull requests. The project controls
@@ -56,8 +56,9 @@ See [configuration and migration](docs/configuration.md) and
56
56
 
57
57
  Add this workflow to `.github/workflows/shell-compat.yml`:
58
58
 
59
- You can also select **OpsScript Gate shell compatibility** from GitHub's workflow templates, or copy the
60
- maintained [starter workflow](.github/workflow-templates/opsscript-gate.yml).
59
+ The repository includes a maintained [workflow template](.github/workflow-templates/opsscript-gate.yml).
60
+ For a one-click setup in another public repository, use the InstallReady scanner above; it opens GitHub's
61
+ workflow editor with the generated file ready to review.
61
62
 
62
63
  You can copy the maintained example from [`examples/github-actions/workflow.yml`](examples/github-actions/workflow.yml)
63
64
  and change the `script-path`, or use the minimal workflow below.
@@ -71,7 +72,9 @@ jobs:
71
72
  runs-on: ubuntu-latest
72
73
  steps:
73
74
  - uses: actions/checkout@v7
74
- - uses: Mresyzz/opsscript-gate@v0.6.3
75
+ with:
76
+ persist-credentials: false
77
+ - uses: Mresyzz/opsscript-gate@v0.7.0
75
78
  ```
76
79
 
77
80
  This zero-configuration form discovers shell scripts in the repository. Use
@@ -82,7 +85,7 @@ repository has a single installer. For self-hosted model installers, see the
82
85
  Or test a specific script with custom execution modes:
83
86
 
84
87
  ```yaml
85
- - uses: Mresyzz/opsscript-gate@v0.6.3
88
+ - uses: Mresyzz/opsscript-gate@v0.7.0
86
89
  with:
87
90
  script-path: scripts/install.sh
88
91
  shell: auto
@@ -112,8 +115,9 @@ unrelated scripts:
112
115
  ```yaml
113
116
  - uses: actions/checkout@v7
114
117
  with:
118
+ persist-credentials: false
115
119
  fetch-depth: 0
116
- - uses: Mresyzz/opsscript-gate@v0.6.3
120
+ - uses: Mresyzz/opsscript-gate@v0.7.0
117
121
  with:
118
122
  changed-since: ${{ github.event.pull_request.base.sha }}
119
123
  preset: minimal
@@ -190,6 +194,28 @@ The action also writes a markdown matrix to `$GITHUB_STEP_SUMMARY`:
190
194
  </details>
191
195
  ```
192
196
 
197
+ ### SARIF for Code Scanning
198
+
199
+ Use `sarif` when the result should appear in GitHub Code Scanning or another
200
+ SARIF-compatible viewer. Upload it explicitly with the official upload action:
201
+
202
+ ```yaml
203
+ - uses: Mresyzz/opsscript-gate@v0.7.0
204
+ with:
205
+ script-path: scripts/install.sh
206
+ format: sarif
207
+ output: reports/opsscript-gate.sarif
208
+
209
+ - uses: github/codeql-action/upload-sarif@v3
210
+ if: always()
211
+ with:
212
+ sarif_file: reports/opsscript-gate.sarif
213
+ ```
214
+
215
+ Each runtime failure becomes a location-aware result with the distribution,
216
+ exit code, command, and diagnostic line. A passing matrix produces a valid SARIF
217
+ file with no findings.
218
+
193
219
  ---
194
220
 
195
221
  ## Example: runtime behavior that static analysis misses
@@ -346,7 +372,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
346
372
  | `-j, --jobs` | Integer | `min(2, size)` | Number of concurrent container jobs |
347
373
  | `--matrix` | String | `debian:12-slim,ubuntu:22.04,ubuntu:24.04,alpine:3.20` | Comma-separated list of Docker images |
348
374
  | `--timeout` | Integer | `60` | Hard timeout per container in seconds |
349
- | `--format` | Choice | `table` | Output format: `table`, `markdown`, or `json` |
375
+ | `--format` | Choice | `table` | Output format: `table`, `markdown`, `json`, or `sarif` |
350
376
  | `--shell` | Choice | `posix` | Execution mode: `posix` (default), `shebang`, or `auto` |
351
377
  | `--mem-limit` | String | `256m` | Memory limit per container (e.g. `256m`, `512m`) |
352
378
  | `--pids-limit`| Integer | `128` | Maximum number of processes per container |
@@ -383,14 +409,6 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
383
409
  Explicit CLI options override project settings. Explicit script paths bypass discovery
384
410
  and its exclusions. See [complete configuration semantics](docs/configuration.md).
385
411
 
386
- ## Downstream example
387
-
388
- [`Mresyzz/linux-dev-bootstrap`](https://github.com/Mresyzz/linux-dev-bootstrap) contains a workflow that validates `install.sh` across the default Debian, Ubuntu, and Alpine matrix.
389
-
390
- See the downstream workflow: [`.github/workflows/test.yml`](https://github.com/Mresyzz/linux-dev-bootstrap/blob/main/.github/workflows/test.yml).
391
-
392
- ---
393
-
394
412
  ## 🧪 Development & Testing
395
413
 
396
414
  ```bash
@@ -10,9 +10,9 @@ Bash 依赖、包管理器假设和交互式阻塞。它与 ShellCheck 互补。
10
10
 
11
11
  也可以直接打开 [InstallReady 在线扫描器](https://mresyzz.github.io/InstallReady/?repo=Mresyzz%2Fopsscript-gate),无需登录即可检查公开仓库。
12
12
 
13
- ## 当前版本 v0.6.3
13
+ ## 当前版本 v0.7.0
14
14
 
15
- v0.6.3 已作为正式版本发布;如果你要从源码验证当前分支,也可以执行:
15
+ v0.7.0 已作为正式版本发布;如果你要从源码验证当前分支,也可以执行:
16
16
 
17
17
  ```bash
18
18
  pip install -e .
@@ -28,11 +28,15 @@ opsscript-gate run --dry-run
28
28
  opsscript-gate run --format json --output reports/compatibility.json
29
29
  ```
30
30
 
31
- `init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.6.3`。
31
+ `init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.7.0`。
32
32
  预览不需要 Docker;真实运行需要 Python 3.10+
33
33
  和可访问的 Linux Docker 引擎。
34
34
  `opsscript-gate doctor` 可以在真实运行前检查 Python 和 Docker。
35
35
 
36
+ 需要接入 GitHub Code Scanning 时,将 `--format sarif --output reports/opsscript-gate.sarif`
37
+ 传给运行命令,再用 `github/codeql-action/upload-sarif@v3` 上传报告。每个失败发行版
38
+ 都会保留脚本路径、诊断行号、命令和退出码;全部通过时仍会生成合法的空 SARIF 报告。
39
+
36
40
  生成的配置默认排除 `tests/*`、`examples/*`,使用 Debian + Alpine,尊重
37
41
  脚本 shebang,并关闭网络。请根据实际脚本审核配置;需要下载文件时启用
38
42
  `--network bridge`。保留现有无配置行为:四个发行版、POSIX shell、bridge 网络。
@@ -58,7 +62,7 @@ Docker daemon 和可选的项目配置。
58
62
  - uses: actions/checkout@v7
59
63
  with:
60
64
  fetch-depth: 0
61
- - uses: Mresyzz/opsscript-gate@v0.6.3
65
+ - uses: Mresyzz/opsscript-gate@v0.7.0
62
66
  with:
63
67
  changed-since: ${{ github.event.pull_request.base.sha }}
64
68
  preset: minimal
@@ -23,7 +23,7 @@ inputs:
23
23
  required: false
24
24
  default: ''
25
25
  format:
26
- description: 'Output report format: table, markdown, or json'
26
+ description: 'Output report format: table, markdown, json, or sarif'
27
27
  required: false
28
28
  default: ''
29
29
  shell:
@@ -12,7 +12,7 @@ opsscript-gate run --dry-run
12
12
  ```
13
13
 
14
14
  `init` refuses to overwrite either `.opsscript-gate.json` or
15
- `.github/workflows/opsscript-gate.yml`. The generated workflow uses the v0.6.3 tag.
15
+ `.github/workflows/opsscript-gate.yml`. The generated workflow uses the v0.7.0 tag.
16
16
 
17
17
  `doctor` checks Python and Docker connectivity without executing repository scripts.
18
18
  Use `opsscript-gate doctor --format json` when collecting a support report.
@@ -99,7 +99,7 @@ For pull requests, set `changed-since` to the base commit and fetch full Git his
99
99
  - uses: actions/checkout@v7
100
100
  with:
101
101
  fetch-depth: 0
102
- - uses: Mresyzz/opsscript-gate@v0.6.3
102
+ - uses: Mresyzz/opsscript-gate@v0.7.0
103
103
  with:
104
104
  changed-since: ${{ github.event.pull_request.base.sha }}
105
105
  preset: minimal
@@ -109,7 +109,7 @@ The Action passes when no changed shell scripts are selected. This keeps unrelat
109
109
  documentation or application changes from starting container jobs.
110
110
 
111
111
  ```yaml
112
- - uses: Mresyzz/opsscript-gate@v0.6.3
112
+ - uses: Mresyzz/opsscript-gate@v0.7.0
113
113
  with:
114
114
  config: .opsscript-gate.json
115
115
  format: json
@@ -33,7 +33,7 @@ jobs:
33
33
  fetch-depth: 0
34
34
  persist-credentials: false
35
35
 
36
- - uses: Mresyzz/opsscript-gate@v0.6.3
36
+ - uses: Mresyzz/opsscript-gate@v0.7.0
37
37
  with:
38
38
  changed-since: ${{ github.event.pull_request.base.sha }}
39
39
  preset: minimal
@@ -2,4 +2,4 @@
2
2
 
3
3
  This example shows a GitHub Actions workflow for checking a shell script on pull requests.
4
4
 
5
- Copy `workflow.yml` into your repository at `.github/workflows/script-gate.yml` and point `script-path` to the script you want to validate. The example uses the current v0.6.3 Action release.
5
+ Copy `workflow.yml` into your repository at `.github/workflows/script-gate.yml` and point `script-path` to the script you want to validate. The example uses the current v0.7.0 Action release.
@@ -19,7 +19,7 @@ jobs:
19
19
  persist-credentials: false
20
20
 
21
21
  - name: Run OpsScript Gate
22
- uses: Mresyzz/opsscript-gate@v0.6.3
22
+ uses: Mresyzz/opsscript-gate@v0.7.0
23
23
  with:
24
24
  script-path: 'scripts/deploy.sh'
25
25
  shell: auto
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "opsscript-gate"
7
- version = "0.6.3"
7
+ version = "0.7.0"
8
8
  description = "Run Linux shell scripts in Debian, Ubuntu and Alpine containers to catch runtime compatibility failures before merge"
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.6.3"
3
+ __version__ = "0.7.0"
4
4
  __all__ = ["__version__"]
@@ -15,8 +15,10 @@ from opsscript_gate.reporter import (
15
15
  emit_github_annotations,
16
16
  format_github_summary,
17
17
  format_json,
18
+ format_multi_sarif,
18
19
  format_multi_github_summary,
19
20
  format_multi_terminal_table,
21
+ format_sarif,
20
22
  format_terminal_table,
21
23
  write_github_step_summary,
22
24
  )
@@ -91,9 +93,9 @@ def build_parser() -> argparse.ArgumentParser:
91
93
  )
92
94
  run_parser.add_argument(
93
95
  "--format",
94
- choices=["table", "markdown", "json"],
96
+ choices=["table", "markdown", "json", "sarif"],
95
97
  default="table",
96
- help="Output report format: 'table' (default ASCII), 'markdown', or 'json'",
98
+ help="Output report format: 'table' (default ASCII), 'markdown', 'json', or 'sarif'",
97
99
  )
98
100
  run_parser.add_argument(
99
101
  "--shell",
@@ -301,6 +303,8 @@ def main(argv: list[str] | None = None) -> int:
301
303
  # Format report output
302
304
  if args.format == "json":
303
305
  output = format_json(report)
306
+ elif args.format == "sarif":
307
+ output = format_sarif(report, script_path=script_path)
304
308
  elif args.format == "markdown":
305
309
  output = format_github_summary(report, script_path=script_path)
306
310
  else:
@@ -353,6 +357,8 @@ def main(argv: list[str] | None = None) -> int:
353
357
 
354
358
  if args.format == "json":
355
359
  output = format_json(multi_report)
360
+ elif args.format == "sarif":
361
+ output = format_multi_sarif(multi_report)
356
362
  elif args.format == "markdown":
357
363
  output = format_multi_github_summary(multi_report)
358
364
  else:
@@ -63,7 +63,7 @@ jobs:
63
63
  - uses: actions/checkout@v7
64
64
  with:
65
65
  persist-credentials: false
66
- - uses: Mresyzz/opsscript-gate@v0.6.3
66
+ - uses: Mresyzz/opsscript-gate@v0.7.0
67
67
  with:
68
68
  config: .opsscript-gate.json
69
69
  """,
@@ -3,6 +3,7 @@ import json
3
3
  import os
4
4
  import re
5
5
  import sys
6
+ from opsscript_gate import __version__
6
7
  from opsscript_gate.models import DistroStatus, MultiScriptReport, RunReport, SingleResult
7
8
 
8
9
  # Structure-altering Markdown characters in regular prose:
@@ -463,6 +464,125 @@ def format_json(report: RunReport | MultiScriptReport) -> str:
463
464
  return json.dumps(report.to_dict(), indent=2, ensure_ascii=False)
464
465
 
465
466
 
467
+ _SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json"
468
+ _SARIF_RULES = {
469
+ DistroStatus.FAIL: ("OSG001", "Runtime compatibility failure"),
470
+ DistroStatus.TIMED_OUT: ("OSG002", "Runtime compatibility check timed out"),
471
+ DistroStatus.ERROR: ("OSG003", "Runtime compatibility check error"),
472
+ }
473
+
474
+
475
+ def _sarif_path(script_path: str) -> str:
476
+ """Return a repository-relative SARIF artifact path."""
477
+ clean = (script_path or "script.sh").replace("\\", "/")
478
+ while clean.startswith("./"):
479
+ clean = clean[2:]
480
+ return clean or "script.sh"
481
+
482
+
483
+ def _sarif_message(result: SingleResult) -> str:
484
+ parts = [f"[{result.distro}] {result.status.value}"]
485
+ if result.diagnostic:
486
+ parts.append(result.diagnostic.message)
487
+ if result.diagnostic.hint:
488
+ parts.append(result.diagnostic.hint)
489
+ elif result.error_message:
490
+ parts.append(result.error_message)
491
+ elif result.exit_code is not None:
492
+ parts.append(f"Execution exited with code {result.exit_code}.")
493
+ return " — ".join(parts)
494
+
495
+
496
+ def _sarif_result(result: SingleResult, script_path: str) -> dict[str, object] | None:
497
+ if result.status == DistroStatus.PASS:
498
+ return None
499
+
500
+ rule_id, rule_name = _SARIF_RULES[result.status]
501
+ physical_location: dict[str, object] = {
502
+ "artifactLocation": {
503
+ "uri": _sarif_path(script_path),
504
+ },
505
+ }
506
+ if result.diagnostic and result.diagnostic.line and result.diagnostic.line > 0:
507
+ physical_location["region"] = {"startLine": result.diagnostic.line}
508
+
509
+ properties: dict[str, object] = {
510
+ "distribution": result.distro,
511
+ "status": result.status.value,
512
+ "durationSeconds": round(result.duration, 3),
513
+ }
514
+ if result.exit_code is not None:
515
+ properties["exitCode"] = result.exit_code
516
+ if result.diagnostic and result.diagnostic.command:
517
+ properties["command"] = result.diagnostic.command
518
+
519
+ return {
520
+ "ruleId": rule_id,
521
+ "level": "error",
522
+ "message": {"text": _sarif_message(result)},
523
+ "locations": [{"physicalLocation": physical_location}],
524
+ "properties": properties,
525
+ "help": {
526
+ "text": rule_name,
527
+ "uri": "https://github.com/Mresyzz/opsscript-gate#ci-output",
528
+ },
529
+ }
530
+
531
+
532
+ def _sarif_run(results: list[dict[str, object]]) -> dict[str, object]:
533
+ return {
534
+ "tool": {
535
+ "driver": {
536
+ "name": "OpsScript Gate",
537
+ "version": __version__,
538
+ "informationUri": "https://github.com/Mresyzz/opsscript-gate",
539
+ "rules": [
540
+ {
541
+ "id": rule_id,
542
+ "name": rule_name,
543
+ "shortDescription": {"text": rule_name},
544
+ "helpUri": "https://github.com/Mresyzz/opsscript-gate#ci-output",
545
+ }
546
+ for rule_id, rule_name in _SARIF_RULES.values()
547
+ ],
548
+ },
549
+ },
550
+ "results": results,
551
+ }
552
+
553
+
554
+ def format_sarif(report: RunReport, script_path: str = "") -> str:
555
+ """Format runtime failures as SARIF 2.1.0 for GitHub Code Scanning."""
556
+ results = [
557
+ item
558
+ for result in report.results
559
+ if (item := _sarif_result(result, script_path)) is not None
560
+ ]
561
+ payload = {
562
+ "$schema": _SARIF_SCHEMA,
563
+ "version": "2.1.0",
564
+ "runs": [_sarif_run(results)],
565
+ }
566
+ return json.dumps(payload, indent=2, ensure_ascii=False)
567
+
568
+
569
+ def format_multi_sarif(report: MultiScriptReport) -> str:
570
+ """Format multi-script runtime failures as one SARIF 2.1.0 run."""
571
+ results: list[dict[str, object]] = []
572
+ for script_path, script_report in report.reports.items():
573
+ results.extend(
574
+ item
575
+ for result in script_report.results
576
+ if (item := _sarif_result(result, script_path)) is not None
577
+ )
578
+ payload = {
579
+ "$schema": _SARIF_SCHEMA,
580
+ "version": "2.1.0",
581
+ "runs": [_sarif_run(results)],
582
+ }
583
+ return json.dumps(payload, indent=2, ensure_ascii=False)
584
+
585
+
466
586
  def write_github_step_summary(
467
587
  report_or_content: RunReport | MultiScriptReport | str,
468
588
  script_path: str = "",
@@ -14,6 +14,7 @@ def test_action_metadata_is_valid_and_inputs_are_wired():
14
14
  "--network", "--matrix", "--jobs", "--config", "--preset",
15
15
  "--max-scripts", "--output", "--exclude"):
16
16
  assert flag in run_text
17
+ assert "json, or sarif" in text
17
18
  assert "--changed-since" in run_text
18
19
 
19
20
 
@@ -19,8 +19,10 @@ from opsscript_gate.reporter import (
19
19
  format_github_annotations,
20
20
  format_github_summary,
21
21
  format_json,
22
+ format_multi_sarif,
22
23
  format_multi_github_summary,
23
24
  format_multi_terminal_table,
25
+ format_sarif,
24
26
  format_terminal_table,
25
27
  write_github_step_summary,
26
28
  )
@@ -300,6 +302,54 @@ def test_reporter_json():
300
302
  assert data["results"][0]["status"] == "PASS"
301
303
 
302
304
 
305
+ def test_reporter_sarif_contains_failure_location_and_metadata():
306
+ result = SingleResult(
307
+ "alpine:3.20",
308
+ DistroStatus.FAIL,
309
+ 127,
310
+ 0.42,
311
+ diagnostic=FailureDiagnostic(
312
+ kind="missing_command",
313
+ message="apt-get: command not found",
314
+ command="apt-get",
315
+ line=4,
316
+ distro="alpine:3.20",
317
+ hint="Use apk instead of apt-get.",
318
+ ),
319
+ )
320
+ report = RunReport([SingleResult("debian:12-slim", DistroStatus.PASS, 0, 0.2), result])
321
+
322
+ payload = json.loads(format_sarif(report, script_path="./scripts/install.sh"))
323
+ assert payload["$schema"].endswith("sarif-2.1.0.json")
324
+ assert payload["version"] == "2.1.0"
325
+ finding = payload["runs"][0]["results"][0]
326
+ assert finding["ruleId"] == "OSG001"
327
+ assert finding["level"] == "error"
328
+ assert finding["locations"][0]["physicalLocation"]["artifactLocation"]["uri"] == "scripts/install.sh"
329
+ assert finding["locations"][0]["physicalLocation"]["region"]["startLine"] == 4
330
+ assert finding["properties"]["distribution"] == "alpine:3.20"
331
+ assert finding["properties"]["command"] == "apt-get"
332
+
333
+
334
+ def test_reporter_multi_sarif_keeps_script_paths():
335
+ report = MultiScriptReport(
336
+ reports={
337
+ "install.sh": RunReport([SingleResult("alpine:3.20", DistroStatus.FAIL, 1, 0.1)]),
338
+ "scripts/setup.sh": RunReport([SingleResult("debian:12-slim", DistroStatus.TIMED_OUT, None, 5.0)]),
339
+ },
340
+ total_duration=5.1,
341
+ )
342
+
343
+ payload = json.loads(format_multi_sarif(report))
344
+ results = payload["runs"][0]["results"]
345
+ assert len(results) == 2
346
+ paths = {
347
+ result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
348
+ for result in results
349
+ }
350
+ assert paths == {"install.sh", "scripts/setup.sh"}
351
+
352
+
303
353
  def test_write_github_step_summary(tmp_path):
304
354
  summary_file = tmp_path / "step_summary.md"
305
355
  r1 = SingleResult("debian:12-slim", DistroStatus.PASS, 0, 1.0)
File without changes