opsscript-gate 0.6.2__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.2 → opsscript_gate-0.7.0}/.github/workflow-templates/opsscript-gate.yml +1 -1
  2. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/CHANGELOG.md +14 -0
  3. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/CITATION.cff +2 -2
  4. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/PKG-INFO +46 -20
  5. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/README.md +44 -18
  6. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/README.zh-CN.md +13 -4
  7. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/action.yml +2 -2
  8. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/docs/configuration.md +7 -3
  9. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/docs/gpt-oss.md +1 -1
  10. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/examples/github-actions/README.md +1 -1
  11. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/examples/github-actions/workflow.yml +1 -1
  12. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/pyproject.toml +5 -1
  13. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/src/opsscript_gate/__init__.py +1 -1
  14. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/src/opsscript_gate/cli.py +51 -2
  15. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/src/opsscript_gate/config.py +1 -1
  16. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/src/opsscript_gate/reporter.py +120 -0
  17. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/tests/test_action_contract.py +1 -0
  18. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/tests/test_onboarding.py +22 -0
  19. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/tests/test_runner.py +50 -0
  20. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.gitattributes +0 -0
  21. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  22. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.github/ISSUE_TEMPLATE/distro_request.yml +0 -0
  23. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  24. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.github/assets/social-preview.png +0 -0
  25. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.github/pull_request_template.md +0 -0
  26. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.github/workflow-templates/properties.json +0 -0
  27. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.github/workflows/demo.yml +0 -0
  28. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.github/workflows/release.yml +0 -0
  29. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.github/workflows/test.yml +0 -0
  30. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/.gitignore +0 -0
  31. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/CONTRIBUTING.md +0 -0
  32. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/LICENSE +0 -0
  33. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/ROADMAP.md +0 -0
  34. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/SECURITY.md +0 -0
  35. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/docs/troubleshooting.md +0 -0
  36. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/examples/alpine-incompatibility/README.md +0 -0
  37. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/examples/alpine-incompatibility/apt_dependency.sh +0 -0
  38. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/examples/basic/README.md +0 -0
  39. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/examples/basic/clean_setup.sh +0 -0
  40. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/examples/interactive-hang/README.md +0 -0
  41. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/examples/interactive-hang/prompt_confirm.sh +0 -0
  42. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/src/opsscript_gate/discovery.py +0 -0
  43. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/src/opsscript_gate/models.py +0 -0
  44. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/src/opsscript_gate/remediation.py +0 -0
  45. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/src/opsscript_gate/runner.py +0 -0
  46. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/tests/fixtures/fail_deps.sh +0 -0
  47. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/tests/fixtures/fail_hang.sh +0 -0
  48. {opsscript_gate-0.6.2 → opsscript_gate-0.7.0}/tests/fixtures/pass_basic.sh +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.2
28
+ uses: Mresyzz/opsscript-gate@v0.7.0
29
29
  with:
30
30
  shell: auto
31
31
  network: none
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
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
9
+
10
+ ### Added
11
+ - Added `opsscript-gate doctor` to diagnose Python and Docker prerequisites before a local run.
12
+
13
+ ### Changed
14
+ - Expanded package and Marketplace keywords for shell installers, ShellCheck users, portability, and BusyBox.
15
+ - Clarified the Action description around shell installer runtime compatibility.
16
+
3
17
  ## [0.6.2] - 2026-10-01
4
18
 
5
19
  ### Changed
@@ -5,8 +5,8 @@ type: software
5
5
  authors:
6
6
  - family-names: Mresyzz
7
7
  given-names: Mresy
8
- version: 0.6.2
9
- date-released: 2026-09-30
8
+ version: 0.7.0
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
12
12
  license: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opsscript-gate
3
- Version: 0.6.2
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
@@ -11,7 +11,7 @@ Project-URL: Documentation, https://github.com/Mresyzz/opsscript-gate/tree/main/
11
11
  Author: Mresyzz
12
12
  License: MIT
13
13
  License-File: LICENSE
14
- Keywords: alpine,bootstrap,ci,compatibility,cross-distro,debian,devops,docker,github-actions,installer,linux,posix,shell,shellcheck-alternative,testing,ubuntu
14
+ Keywords: alpine,bootstrap,busybox,ci,compatibility,cross-distro,debian,devops,docker,github-actions,installer,installer-testing,linux,posix,shell,shell-portability,shellcheck,shellcheck-alternative,testing,ubuntu
15
15
  Classifier: Development Status :: 4 - Beta
16
16
  Classifier: Intended Audience :: Developers
17
17
  Classifier: Intended Audience :: System Administrators
@@ -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.2
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
@@ -64,6 +64,7 @@ were introduced in v0.5.0; projects on v0.4.1 and earlier do not include them.
64
64
 
65
65
  ```bash
66
66
  opsscript-gate init
67
+ opsscript-gate doctor
67
68
  opsscript-gate run --dry-run
68
69
  opsscript-gate run --format json --output reports/compatibility.json
69
70
  ```
@@ -71,6 +72,7 @@ opsscript-gate run --format json --output reports/compatibility.json
71
72
  `init` creates `.opsscript-gate.json` and a GitHub Actions workflow without replacing
72
73
  existing files. Review its exclusions (`tests/*`, `examples/*`) and offline network
73
74
  default before running. No Docker is required for `init` or `--dry-run`.
75
+ Use `opsscript-gate doctor` to check Python and Docker before a real run.
74
76
 
75
77
  Useful for standalone installers, container entrypoints and release scripts that
76
78
  must work on both GNU/Linux and Alpine/BusyBox. Each script runs independently;
@@ -84,8 +86,9 @@ See [configuration and migration](docs/configuration.md) and
84
86
 
85
87
  Add this workflow to `.github/workflows/shell-compat.yml`:
86
88
 
87
- You can also select **OpsScript Gate shell compatibility** from GitHub's workflow templates, or copy the
88
- 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.
89
92
 
90
93
  You can copy the maintained example from [`examples/github-actions/workflow.yml`](examples/github-actions/workflow.yml)
91
94
  and change the `script-path`, or use the minimal workflow below.
@@ -99,7 +102,9 @@ jobs:
99
102
  runs-on: ubuntu-latest
100
103
  steps:
101
104
  - uses: actions/checkout@v7
102
- - uses: Mresyzz/opsscript-gate@v0.6.2
105
+ with:
106
+ persist-credentials: false
107
+ - uses: Mresyzz/opsscript-gate@v0.7.0
103
108
  ```
104
109
 
105
110
  This zero-configuration form discovers shell scripts in the repository. Use
@@ -110,7 +115,7 @@ repository has a single installer. For self-hosted model installers, see the
110
115
  Or test a specific script with custom execution modes:
111
116
 
112
117
  ```yaml
113
- - uses: Mresyzz/opsscript-gate@v0.6.2
118
+ - uses: Mresyzz/opsscript-gate@v0.7.0
114
119
  with:
115
120
  script-path: scripts/install.sh
116
121
  shell: auto
@@ -140,8 +145,9 @@ unrelated scripts:
140
145
  ```yaml
141
146
  - uses: actions/checkout@v7
142
147
  with:
148
+ persist-credentials: false
143
149
  fetch-depth: 0
144
- - uses: Mresyzz/opsscript-gate@v0.6.2
150
+ - uses: Mresyzz/opsscript-gate@v0.7.0
145
151
  with:
146
152
  changed-since: ${{ github.event.pull_request.base.sha }}
147
153
  preset: minimal
@@ -218,6 +224,28 @@ The action also writes a markdown matrix to `$GITHUB_STEP_SUMMARY`:
218
224
  </details>
219
225
  ```
220
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
+
221
249
  ---
222
250
 
223
251
  ## Example: runtime behavior that static analysis misses
@@ -353,6 +381,12 @@ Customize the matrix at any time via `--matrix` or Action input `matrix`.
353
381
 
354
382
  ## CLI reference
355
383
 
384
+ Check local prerequisites before starting containers:
385
+
386
+ ```text
387
+ opsscript-gate doctor [--format table|json]
388
+ ```
389
+
356
390
  ```text
357
391
  usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
358
392
  [--format {table,markdown,json}]
@@ -368,7 +402,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
368
402
  | `-j, --jobs` | Integer | `min(2, size)` | Number of concurrent container jobs |
369
403
  | `--matrix` | String | `debian:12-slim,ubuntu:22.04,ubuntu:24.04,alpine:3.20` | Comma-separated list of Docker images |
370
404
  | `--timeout` | Integer | `60` | Hard timeout per container in seconds |
371
- | `--format` | Choice | `table` | Output format: `table`, `markdown`, or `json` |
405
+ | `--format` | Choice | `table` | Output format: `table`, `markdown`, `json`, or `sarif` |
372
406
  | `--shell` | Choice | `posix` | Execution mode: `posix` (default), `shebang`, or `auto` |
373
407
  | `--mem-limit` | String | `256m` | Memory limit per container (e.g. `256m`, `512m`) |
374
408
  | `--pids-limit`| Integer | `128` | Maximum number of processes per container |
@@ -405,14 +439,6 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
405
439
  Explicit CLI options override project settings. Explicit script paths bypass discovery
406
440
  and its exclusions. See [complete configuration semantics](docs/configuration.md).
407
441
 
408
- ## Downstream example
409
-
410
- [`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.
411
-
412
- See the downstream workflow: [`.github/workflows/test.yml`](https://github.com/Mresyzz/linux-dev-bootstrap/blob/main/.github/workflows/test.yml).
413
-
414
- ---
415
-
416
442
  ## 🧪 Development & Testing
417
443
 
418
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.2
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
@@ -34,6 +34,7 @@ were introduced in v0.5.0; projects on v0.4.1 and earlier do not include them.
34
34
 
35
35
  ```bash
36
36
  opsscript-gate init
37
+ opsscript-gate doctor
37
38
  opsscript-gate run --dry-run
38
39
  opsscript-gate run --format json --output reports/compatibility.json
39
40
  ```
@@ -41,6 +42,7 @@ opsscript-gate run --format json --output reports/compatibility.json
41
42
  `init` creates `.opsscript-gate.json` and a GitHub Actions workflow without replacing
42
43
  existing files. Review its exclusions (`tests/*`, `examples/*`) and offline network
43
44
  default before running. No Docker is required for `init` or `--dry-run`.
45
+ Use `opsscript-gate doctor` to check Python and Docker before a real run.
44
46
 
45
47
  Useful for standalone installers, container entrypoints and release scripts that
46
48
  must work on both GNU/Linux and Alpine/BusyBox. Each script runs independently;
@@ -54,8 +56,9 @@ See [configuration and migration](docs/configuration.md) and
54
56
 
55
57
  Add this workflow to `.github/workflows/shell-compat.yml`:
56
58
 
57
- You can also select **OpsScript Gate shell compatibility** from GitHub's workflow templates, or copy the
58
- 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.
59
62
 
60
63
  You can copy the maintained example from [`examples/github-actions/workflow.yml`](examples/github-actions/workflow.yml)
61
64
  and change the `script-path`, or use the minimal workflow below.
@@ -69,7 +72,9 @@ jobs:
69
72
  runs-on: ubuntu-latest
70
73
  steps:
71
74
  - uses: actions/checkout@v7
72
- - uses: Mresyzz/opsscript-gate@v0.6.2
75
+ with:
76
+ persist-credentials: false
77
+ - uses: Mresyzz/opsscript-gate@v0.7.0
73
78
  ```
74
79
 
75
80
  This zero-configuration form discovers shell scripts in the repository. Use
@@ -80,7 +85,7 @@ repository has a single installer. For self-hosted model installers, see the
80
85
  Or test a specific script with custom execution modes:
81
86
 
82
87
  ```yaml
83
- - uses: Mresyzz/opsscript-gate@v0.6.2
88
+ - uses: Mresyzz/opsscript-gate@v0.7.0
84
89
  with:
85
90
  script-path: scripts/install.sh
86
91
  shell: auto
@@ -110,8 +115,9 @@ unrelated scripts:
110
115
  ```yaml
111
116
  - uses: actions/checkout@v7
112
117
  with:
118
+ persist-credentials: false
113
119
  fetch-depth: 0
114
- - uses: Mresyzz/opsscript-gate@v0.6.2
120
+ - uses: Mresyzz/opsscript-gate@v0.7.0
115
121
  with:
116
122
  changed-since: ${{ github.event.pull_request.base.sha }}
117
123
  preset: minimal
@@ -188,6 +194,28 @@ The action also writes a markdown matrix to `$GITHUB_STEP_SUMMARY`:
188
194
  </details>
189
195
  ```
190
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
+
191
219
  ---
192
220
 
193
221
  ## Example: runtime behavior that static analysis misses
@@ -323,6 +351,12 @@ Customize the matrix at any time via `--matrix` or Action input `matrix`.
323
351
 
324
352
  ## CLI reference
325
353
 
354
+ Check local prerequisites before starting containers:
355
+
356
+ ```text
357
+ opsscript-gate doctor [--format table|json]
358
+ ```
359
+
326
360
  ```text
327
361
  usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
328
362
  [--format {table,markdown,json}]
@@ -338,7 +372,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
338
372
  | `-j, --jobs` | Integer | `min(2, size)` | Number of concurrent container jobs |
339
373
  | `--matrix` | String | `debian:12-slim,ubuntu:22.04,ubuntu:24.04,alpine:3.20` | Comma-separated list of Docker images |
340
374
  | `--timeout` | Integer | `60` | Hard timeout per container in seconds |
341
- | `--format` | Choice | `table` | Output format: `table`, `markdown`, or `json` |
375
+ | `--format` | Choice | `table` | Output format: `table`, `markdown`, `json`, or `sarif` |
342
376
  | `--shell` | Choice | `posix` | Execution mode: `posix` (default), `shebang`, or `auto` |
343
377
  | `--mem-limit` | String | `256m` | Memory limit per container (e.g. `256m`, `512m`) |
344
378
  | `--pids-limit`| Integer | `128` | Maximum number of processes per container |
@@ -375,14 +409,6 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
375
409
  Explicit CLI options override project settings. Explicit script paths bypass discovery
376
410
  and its exclusions. See [complete configuration semantics](docs/configuration.md).
377
411
 
378
- ## Downstream example
379
-
380
- [`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.
381
-
382
- See the downstream workflow: [`.github/workflows/test.yml`](https://github.com/Mresyzz/linux-dev-bootstrap/blob/main/.github/workflows/test.yml).
383
-
384
- ---
385
-
386
412
  ## 🧪 Development & Testing
387
413
 
388
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.2
13
+ ## 当前版本 v0.7.0
14
14
 
15
- v0.6.2 已作为正式版本发布;如果你要从源码验证当前分支,也可以执行:
15
+ v0.7.0 已作为正式版本发布;如果你要从源码验证当前分支,也可以执行:
16
16
 
17
17
  ```bash
18
18
  pip install -e .
@@ -23,13 +23,19 @@ opsscript-gate run examples/basic/clean_setup.sh --dry-run
23
23
 
24
24
  ```bash
25
25
  opsscript-gate init
26
+ opsscript-gate doctor
26
27
  opsscript-gate run --dry-run
27
28
  opsscript-gate run --format json --output reports/compatibility.json
28
29
  ```
29
30
 
30
- `init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.6.2`。
31
+ `init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.7.0`。
31
32
  预览不需要 Docker;真实运行需要 Python 3.10+
32
33
  和可访问的 Linux Docker 引擎。
34
+ `opsscript-gate doctor` 可以在真实运行前检查 Python 和 Docker。
35
+
36
+ 需要接入 GitHub Code Scanning 时,将 `--format sarif --output reports/opsscript-gate.sarif`
37
+ 传给运行命令,再用 `github/codeql-action/upload-sarif@v3` 上传报告。每个失败发行版
38
+ 都会保留脚本路径、诊断行号、命令和退出码;全部通过时仍会生成合法的空 SARIF 报告。
33
39
 
34
40
  生成的配置默认排除 `tests/*`、`examples/*`,使用 Debian + Alpine,尊重
35
41
  脚本 shebang,并关闭网络。请根据实际脚本审核配置;需要下载文件时启用
@@ -47,13 +53,16 @@ opsscript-gate run --format json --output reports/compatibility.json
47
53
  返回码 `0` 表示全部通过,`1` 表示检查失败或发生错误。`--dry-run` 成功只表示
48
54
  执行计划生成成功。容器不是不可信代码的安全沙箱。
49
55
 
56
+ 需要排查本地环境时运行 `opsscript-gate doctor --format json`;它会检查 Python、
57
+ Docker daemon 和可选的项目配置。
58
+
50
59
  在 Pull Request 中只检查本次修改的脚本,可以先获取完整 Git 历史,再传入目标分支提交:
51
60
 
52
61
  ```yaml
53
62
  - uses: actions/checkout@v7
54
63
  with:
55
64
  fetch-depth: 0
56
- - uses: Mresyzz/opsscript-gate@v0.6.2
65
+ - uses: Mresyzz/opsscript-gate@v0.7.0
57
66
  with:
58
67
  changed-since: ${{ github.event.pull_request.base.sha }}
59
68
  preset: minimal
@@ -1,5 +1,5 @@
1
1
  name: 'OpsScript Gate'
2
- description: 'Run Linux shell scripts in Debian, Ubuntu and Alpine containers to catch runtime compatibility failures before merge'
2
+ description: 'Run Linux shell installers across Debian, Ubuntu, and Alpine; a ShellCheck companion for runtime compatibility in CI.'
3
3
  author: 'Mresyzz'
4
4
  branding:
5
5
  icon: 'shield'
@@ -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:
@@ -7,11 +7,15 @@ have these commands.
7
7
 
8
8
  ```bash
9
9
  opsscript-gate init
10
+ opsscript-gate doctor
10
11
  opsscript-gate run --dry-run
11
12
  ```
12
13
 
13
14
  `init` refuses to overwrite either `.opsscript-gate.json` or
14
- `.github/workflows/opsscript-gate.yml`. The generated workflow uses the v0.6.2 tag.
15
+ `.github/workflows/opsscript-gate.yml`. The generated workflow uses the v0.7.0 tag.
16
+
17
+ `doctor` checks Python and Docker connectivity without executing repository scripts.
18
+ Use `opsscript-gate doctor --format json` when collecting a support report.
15
19
 
16
20
  The generated config uses the two-image `minimal` preset, honors recognized shell
17
21
  shebangs, disables networking, and excludes `tests/*` and `examples/*`. Review these
@@ -95,7 +99,7 @@ For pull requests, set `changed-since` to the base commit and fetch full Git his
95
99
  - uses: actions/checkout@v7
96
100
  with:
97
101
  fetch-depth: 0
98
- - uses: Mresyzz/opsscript-gate@v0.6.2
102
+ - uses: Mresyzz/opsscript-gate@v0.7.0
99
103
  with:
100
104
  changed-since: ${{ github.event.pull_request.base.sha }}
101
105
  preset: minimal
@@ -105,7 +109,7 @@ The Action passes when no changed shell scripts are selected. This keeps unrelat
105
109
  documentation or application changes from starting container jobs.
106
110
 
107
111
  ```yaml
108
- - uses: Mresyzz/opsscript-gate@v0.6.2
112
+ - uses: Mresyzz/opsscript-gate@v0.7.0
109
113
  with:
110
114
  config: .opsscript-gate.json
111
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.2
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.2 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.2
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.2"
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" }
@@ -29,6 +29,10 @@ keywords = [
29
29
  "ubuntu",
30
30
  "alpine",
31
31
  "shellcheck-alternative",
32
+ "shellcheck",
33
+ "shell-portability",
34
+ "installer-testing",
35
+ "busybox",
32
36
  ]
33
37
  classifiers = [
34
38
  "Development Status :: 4 - Beta",
@@ -1,4 +1,4 @@
1
1
  """OpsScript Gate: Lightweight cross-distro compatibility pre-check for Linux ops scripts."""
2
2
 
3
- __version__ = "0.6.2"
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
  )
@@ -24,6 +26,7 @@ from opsscript_gate.runner import (
24
26
  DEFAULT_MATRIX,
25
27
  DEFAULT_TIMEOUT,
26
28
  DockerDaemonError,
29
+ get_docker_client,
27
30
  run_matrix,
28
31
  )
29
32
 
@@ -43,6 +46,16 @@ def build_parser() -> argparse.ArgumentParser:
43
46
 
44
47
  subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
45
48
  subparsers.add_parser("init", help="Create project configuration and GitHub workflow")
49
+ doctor_parser = subparsers.add_parser(
50
+ "doctor",
51
+ help="Check the local environment before running containers",
52
+ )
53
+ doctor_parser.add_argument(
54
+ "--format",
55
+ choices=["table", "json"],
56
+ default="table",
57
+ help="Output format: 'table' (default) or 'json'",
58
+ )
46
59
 
47
60
  # 'run' subcommand
48
61
  run_parser = subparsers.add_parser(
@@ -80,9 +93,9 @@ def build_parser() -> argparse.ArgumentParser:
80
93
  )
81
94
  run_parser.add_argument(
82
95
  "--format",
83
- choices=["table", "markdown", "json"],
96
+ choices=["table", "markdown", "json", "sarif"],
84
97
  default="table",
85
- help="Output report format: 'table' (default ASCII), 'markdown', or 'json'",
98
+ help="Output report format: 'table' (default ASCII), 'markdown', 'json', or 'sarif'",
86
99
  )
87
100
  run_parser.add_argument(
88
101
  "--shell",
@@ -149,6 +162,38 @@ def main(argv: list[str] | None = None) -> int:
149
162
  sys.stderr.write(f"Error: {err}\n")
150
163
  return 1
151
164
 
165
+ if args.command == "doctor":
166
+ checks = [
167
+ {
168
+ "name": "python",
169
+ "ok": sys.version_info >= (3, 10),
170
+ "detail": f"Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
171
+ }
172
+ ]
173
+ try:
174
+ get_docker_client()
175
+ checks.append({"name": "docker", "ok": True, "detail": "Docker daemon is reachable"})
176
+ except DockerDaemonError as err:
177
+ checks.append({"name": "docker", "ok": False, "detail": str(err)})
178
+
179
+ config_path = Path(".opsscript-gate.json")
180
+ checks.append({
181
+ "name": "project",
182
+ "ok": True,
183
+ "detail": f"Found {config_path}" if config_path.is_file() else "No project config (optional)",
184
+ })
185
+ passed = all(check["ok"] for check in checks)
186
+ if args.format == "json":
187
+ print(json.dumps({"status": "PASS" if passed else "FAIL", "checks": checks}, indent=2))
188
+ else:
189
+ print("OpsScript Gate doctor")
190
+ for check in checks:
191
+ marker = "PASS" if check["ok"] else "FAIL"
192
+ print(f"[{marker}] {check['name']}: {check['detail']}")
193
+ if not passed:
194
+ print("Run `opsscript-gate init` after fixing the failed check, then preview with `opsscript-gate run --dry-run`.")
195
+ return 0 if passed else 1
196
+
152
197
  if args.command == "run":
153
198
  try:
154
199
  config = load_config(args.config)
@@ -258,6 +303,8 @@ def main(argv: list[str] | None = None) -> int:
258
303
  # Format report output
259
304
  if args.format == "json":
260
305
  output = format_json(report)
306
+ elif args.format == "sarif":
307
+ output = format_sarif(report, script_path=script_path)
261
308
  elif args.format == "markdown":
262
309
  output = format_github_summary(report, script_path=script_path)
263
310
  else:
@@ -310,6 +357,8 @@ def main(argv: list[str] | None = None) -> int:
310
357
 
311
358
  if args.format == "json":
312
359
  output = format_json(multi_report)
360
+ elif args.format == "sarif":
361
+ output = format_multi_sarif(multi_report)
313
362
  elif args.format == "markdown":
314
363
  output = format_multi_github_summary(multi_report)
315
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.2
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
 
@@ -8,6 +8,7 @@ from opsscript_gate.cli import main
8
8
  from opsscript_gate.config import CONFIG_NAME, init_project, load_config
9
9
  from opsscript_gate.discovery import discover_changed_scripts, discover_scripts, is_shell_script
10
10
  from opsscript_gate.models import RunReport, SingleResult, DistroStatus
11
+ from opsscript_gate.runner import DockerDaemonError
11
12
 
12
13
 
13
14
  @pytest.fixture
@@ -35,6 +36,27 @@ def test_init_preview_and_no_overwrite(project, capsys):
35
36
  assert Path(CONFIG_NAME).read_bytes() == original
36
37
 
37
38
 
39
+ def test_doctor_reports_local_prerequisites(project, capsys):
40
+ with patch("opsscript_gate.cli.get_docker_client"):
41
+ assert main(["doctor"]) == 0
42
+ output = capsys.readouterr().out
43
+ assert "OpsScript Gate doctor" in output
44
+ assert "[PASS] python" in output
45
+ assert "[PASS] docker" in output
46
+ assert "No project config (optional)" in output
47
+
48
+
49
+ def test_doctor_json_reports_docker_failure(project, capsys):
50
+ with patch(
51
+ "opsscript_gate.cli.get_docker_client",
52
+ side_effect=DockerDaemonError("Docker is not running"),
53
+ ):
54
+ assert main(["doctor", "--format", "json"]) == 1
55
+ report = json.loads(capsys.readouterr().out)
56
+ assert report["status"] == "FAIL"
57
+ assert next(check for check in report["checks"] if check["name"] == "docker")["ok"] is False
58
+
59
+
38
60
  def test_init_existing_workflow_leaves_config_absent(project):
39
61
  workflow = project / ".github/workflows/opsscript-gate.yml"
40
62
  workflow.parent.mkdir(parents=True)
@@ -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