opsscript-gate 0.6.3__tar.gz → 0.8.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.
- opsscript_gate-0.6.3/.github/workflow-templates/properties.json → opsscript_gate-0.8.0/.github/workflow-templates/opsscript-gate.properties.json +2 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.github/workflow-templates/opsscript-gate.yml +1 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/CHANGELOG.md +9 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/CITATION.cff +2 -2
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/PKG-INFO +41 -20
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/README.md +40 -19
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/README.zh-CN.md +12 -5
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/SECURITY.md +2 -2
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/action.yml +7 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/docs/configuration.md +13 -4
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/docs/gpt-oss.md +1 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/examples/github-actions/README.md +1 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/examples/github-actions/workflow.yml +1 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/pyproject.toml +1 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/src/opsscript_gate/__init__.py +1 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/src/opsscript_gate/cli.py +35 -4
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/src/opsscript_gate/config.py +22 -2
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/src/opsscript_gate/reporter.py +120 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/src/opsscript_gate/runner.py +48 -2
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/tests/test_action_contract.py +3 -2
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/tests/test_onboarding.py +17 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/tests/test_runner.py +125 -1
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.gitattributes +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.github/ISSUE_TEMPLATE/distro_request.yml +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.github/assets/social-preview.png +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.github/pull_request_template.md +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.github/workflows/demo.yml +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.github/workflows/release.yml +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.github/workflows/test.yml +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/.gitignore +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/CONTRIBUTING.md +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/LICENSE +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/ROADMAP.md +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/docs/troubleshooting.md +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/examples/alpine-incompatibility/README.md +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/examples/alpine-incompatibility/apt_dependency.sh +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/examples/basic/README.md +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/examples/basic/clean_setup.sh +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/examples/interactive-hang/README.md +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/examples/interactive-hang/prompt_confirm.sh +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/src/opsscript_gate/discovery.py +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/src/opsscript_gate/models.py +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/src/opsscript_gate/remediation.py +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/tests/fixtures/fail_deps.sh +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/tests/fixtures/fail_hang.sh +0 -0
- {opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/tests/fixtures/pass_basic.sh +0 -0
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
"name": "OpsScript Gate shell compatibility",
|
|
3
3
|
"description": "Run shell scripts in Debian, Ubuntu and Alpine containers on changes to shell files.",
|
|
4
4
|
"iconName": "octicon-shield-check",
|
|
5
|
-
"categories": ["Continuous integration", "Testing"]
|
|
5
|
+
"categories": ["Continuous integration", "Testing"],
|
|
6
|
+
"filePatterns": [".*\\.sh$", ".*\\.bash$"]
|
|
6
7
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
3
|
+
## [0.8.0] - 2026-10-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added opt-in `packages` setup for installing validated package names with
|
|
7
|
+
`apt-get` or `apk` before each container run; package setup requires bridge
|
|
8
|
+
networking and leaves existing offline runs unchanged.
|
|
9
|
+
|
|
10
|
+
## [0.7.0] - 2026-10-01
|
|
4
11
|
|
|
5
12
|
### Added
|
|
6
13
|
- Added `opsscript-gate doctor` to diagnose Python and Docker prerequisites before a local run.
|
|
14
|
+
- Added SARIF 2.1.0 output for location-aware GitHub Code Scanning integration.
|
|
7
15
|
|
|
8
16
|
### Changed
|
|
9
17
|
- Expanded package and Marketplace keywords for shell installers, ShellCheck users, portability, and BusyBox.
|
|
@@ -5,8 +5,8 @@ type: software
|
|
|
5
5
|
authors:
|
|
6
6
|
- family-names: Mresyzz
|
|
7
7
|
given-names: Mresy
|
|
8
|
-
version: 0.
|
|
9
|
-
date-released: 2026-10-
|
|
8
|
+
version: 0.8.0
|
|
9
|
+
date-released: 2026-10-02
|
|
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.
|
|
3
|
+
Version: 0.8.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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
59
|
+
## Current release: v0.8.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
|
|
@@ -77,7 +77,10 @@ Use `opsscript-gate doctor` to check Python and Docker before a real run.
|
|
|
77
77
|
Useful for standalone installers, container entrypoints and release scripts that
|
|
78
78
|
must work on both GNU/Linux and Alpine/BusyBox. Each script runs independently;
|
|
79
79
|
repository files, sibling scripts and project dependencies are **not** mounted.
|
|
80
|
-
|
|
80
|
+
If an installer needs a small, explicit set of tools such as `curl`, pass
|
|
81
|
+
`packages` in the config or Action input; the runner installs them with the
|
|
82
|
+
image's native package manager before testing. Package setup is opt-in and
|
|
83
|
+
requires bridge networking. See [configuration and migration](docs/configuration.md) and
|
|
81
84
|
[why a shell script works on Ubuntu but fails on Alpine](docs/troubleshooting.md).
|
|
82
85
|
|
|
83
86
|
## Quickstart
|
|
@@ -86,8 +89,9 @@ See [configuration and migration](docs/configuration.md) and
|
|
|
86
89
|
|
|
87
90
|
Add this workflow to `.github/workflows/shell-compat.yml`:
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
The repository includes a maintained [workflow template](.github/workflow-templates/opsscript-gate.yml).
|
|
93
|
+
For a one-click setup in another public repository, use the InstallReady scanner above; it opens GitHub's
|
|
94
|
+
workflow editor with the generated file ready to review.
|
|
91
95
|
|
|
92
96
|
You can copy the maintained example from [`examples/github-actions/workflow.yml`](examples/github-actions/workflow.yml)
|
|
93
97
|
and change the `script-path`, or use the minimal workflow below.
|
|
@@ -101,7 +105,9 @@ jobs:
|
|
|
101
105
|
runs-on: ubuntu-latest
|
|
102
106
|
steps:
|
|
103
107
|
- uses: actions/checkout@v7
|
|
104
|
-
|
|
108
|
+
with:
|
|
109
|
+
persist-credentials: false
|
|
110
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
105
111
|
```
|
|
106
112
|
|
|
107
113
|
This zero-configuration form discovers shell scripts in the repository. Use
|
|
@@ -112,7 +118,7 @@ repository has a single installer. For self-hosted model installers, see the
|
|
|
112
118
|
Or test a specific script with custom execution modes:
|
|
113
119
|
|
|
114
120
|
```yaml
|
|
115
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
121
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
116
122
|
with:
|
|
117
123
|
script-path: scripts/install.sh
|
|
118
124
|
shell: auto
|
|
@@ -142,8 +148,9 @@ unrelated scripts:
|
|
|
142
148
|
```yaml
|
|
143
149
|
- uses: actions/checkout@v7
|
|
144
150
|
with:
|
|
151
|
+
persist-credentials: false
|
|
145
152
|
fetch-depth: 0
|
|
146
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
153
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
147
154
|
with:
|
|
148
155
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
149
156
|
preset: minimal
|
|
@@ -220,6 +227,28 @@ The action also writes a markdown matrix to `$GITHUB_STEP_SUMMARY`:
|
|
|
220
227
|
</details>
|
|
221
228
|
```
|
|
222
229
|
|
|
230
|
+
### SARIF for Code Scanning
|
|
231
|
+
|
|
232
|
+
Use `sarif` when the result should appear in GitHub Code Scanning or another
|
|
233
|
+
SARIF-compatible viewer. Upload it explicitly with the official upload action:
|
|
234
|
+
|
|
235
|
+
```yaml
|
|
236
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
237
|
+
with:
|
|
238
|
+
script-path: scripts/install.sh
|
|
239
|
+
format: sarif
|
|
240
|
+
output: reports/opsscript-gate.sarif
|
|
241
|
+
|
|
242
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
243
|
+
if: always()
|
|
244
|
+
with:
|
|
245
|
+
sarif_file: reports/opsscript-gate.sarif
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Each runtime failure becomes a location-aware result with the distribution,
|
|
249
|
+
exit code, command, and diagnostic line. A passing matrix produces a valid SARIF
|
|
250
|
+
file with no findings.
|
|
251
|
+
|
|
223
252
|
---
|
|
224
253
|
|
|
225
254
|
## Example: runtime behavior that static analysis misses
|
|
@@ -376,7 +405,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
|
|
|
376
405
|
| `-j, --jobs` | Integer | `min(2, size)` | Number of concurrent container jobs |
|
|
377
406
|
| `--matrix` | String | `debian:12-slim,ubuntu:22.04,ubuntu:24.04,alpine:3.20` | Comma-separated list of Docker images |
|
|
378
407
|
| `--timeout` | Integer | `60` | Hard timeout per container in seconds |
|
|
379
|
-
| `--format` | Choice | `table` | Output format: `table`, `markdown`, or `
|
|
408
|
+
| `--format` | Choice | `table` | Output format: `table`, `markdown`, `json`, or `sarif` |
|
|
380
409
|
| `--shell` | Choice | `posix` | Execution mode: `posix` (default), `shebang`, or `auto` |
|
|
381
410
|
| `--mem-limit` | String | `256m` | Memory limit per container (e.g. `256m`, `512m`) |
|
|
382
411
|
| `--pids-limit`| Integer | `128` | Maximum number of processes per container |
|
|
@@ -413,14 +442,6 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
|
|
|
413
442
|
Explicit CLI options override project settings. Explicit script paths bypass discovery
|
|
414
443
|
and its exclusions. See [complete configuration semantics](docs/configuration.md).
|
|
415
444
|
|
|
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
445
|
## 🧪 Development & Testing
|
|
425
446
|
|
|
426
447
|
```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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
29
|
+
## Current release: v0.8.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
|
|
@@ -47,7 +47,10 @@ Use `opsscript-gate doctor` to check Python and Docker before a real run.
|
|
|
47
47
|
Useful for standalone installers, container entrypoints and release scripts that
|
|
48
48
|
must work on both GNU/Linux and Alpine/BusyBox. Each script runs independently;
|
|
49
49
|
repository files, sibling scripts and project dependencies are **not** mounted.
|
|
50
|
-
|
|
50
|
+
If an installer needs a small, explicit set of tools such as `curl`, pass
|
|
51
|
+
`packages` in the config or Action input; the runner installs them with the
|
|
52
|
+
image's native package manager before testing. Package setup is opt-in and
|
|
53
|
+
requires bridge networking. See [configuration and migration](docs/configuration.md) and
|
|
51
54
|
[why a shell script works on Ubuntu but fails on Alpine](docs/troubleshooting.md).
|
|
52
55
|
|
|
53
56
|
## Quickstart
|
|
@@ -56,8 +59,9 @@ See [configuration and migration](docs/configuration.md) and
|
|
|
56
59
|
|
|
57
60
|
Add this workflow to `.github/workflows/shell-compat.yml`:
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
The repository includes a maintained [workflow template](.github/workflow-templates/opsscript-gate.yml).
|
|
63
|
+
For a one-click setup in another public repository, use the InstallReady scanner above; it opens GitHub's
|
|
64
|
+
workflow editor with the generated file ready to review.
|
|
61
65
|
|
|
62
66
|
You can copy the maintained example from [`examples/github-actions/workflow.yml`](examples/github-actions/workflow.yml)
|
|
63
67
|
and change the `script-path`, or use the minimal workflow below.
|
|
@@ -71,7 +75,9 @@ jobs:
|
|
|
71
75
|
runs-on: ubuntu-latest
|
|
72
76
|
steps:
|
|
73
77
|
- uses: actions/checkout@v7
|
|
74
|
-
|
|
78
|
+
with:
|
|
79
|
+
persist-credentials: false
|
|
80
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
75
81
|
```
|
|
76
82
|
|
|
77
83
|
This zero-configuration form discovers shell scripts in the repository. Use
|
|
@@ -82,7 +88,7 @@ repository has a single installer. For self-hosted model installers, see the
|
|
|
82
88
|
Or test a specific script with custom execution modes:
|
|
83
89
|
|
|
84
90
|
```yaml
|
|
85
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
91
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
86
92
|
with:
|
|
87
93
|
script-path: scripts/install.sh
|
|
88
94
|
shell: auto
|
|
@@ -112,8 +118,9 @@ unrelated scripts:
|
|
|
112
118
|
```yaml
|
|
113
119
|
- uses: actions/checkout@v7
|
|
114
120
|
with:
|
|
121
|
+
persist-credentials: false
|
|
115
122
|
fetch-depth: 0
|
|
116
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
123
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
117
124
|
with:
|
|
118
125
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
119
126
|
preset: minimal
|
|
@@ -190,6 +197,28 @@ The action also writes a markdown matrix to `$GITHUB_STEP_SUMMARY`:
|
|
|
190
197
|
</details>
|
|
191
198
|
```
|
|
192
199
|
|
|
200
|
+
### SARIF for Code Scanning
|
|
201
|
+
|
|
202
|
+
Use `sarif` when the result should appear in GitHub Code Scanning or another
|
|
203
|
+
SARIF-compatible viewer. Upload it explicitly with the official upload action:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
207
|
+
with:
|
|
208
|
+
script-path: scripts/install.sh
|
|
209
|
+
format: sarif
|
|
210
|
+
output: reports/opsscript-gate.sarif
|
|
211
|
+
|
|
212
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
213
|
+
if: always()
|
|
214
|
+
with:
|
|
215
|
+
sarif_file: reports/opsscript-gate.sarif
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Each runtime failure becomes a location-aware result with the distribution,
|
|
219
|
+
exit code, command, and diagnostic line. A passing matrix produces a valid SARIF
|
|
220
|
+
file with no findings.
|
|
221
|
+
|
|
193
222
|
---
|
|
194
223
|
|
|
195
224
|
## Example: runtime behavior that static analysis misses
|
|
@@ -346,7 +375,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
|
|
|
346
375
|
| `-j, --jobs` | Integer | `min(2, size)` | Number of concurrent container jobs |
|
|
347
376
|
| `--matrix` | String | `debian:12-slim,ubuntu:22.04,ubuntu:24.04,alpine:3.20` | Comma-separated list of Docker images |
|
|
348
377
|
| `--timeout` | Integer | `60` | Hard timeout per container in seconds |
|
|
349
|
-
| `--format` | Choice | `table` | Output format: `table`, `markdown`, or `
|
|
378
|
+
| `--format` | Choice | `table` | Output format: `table`, `markdown`, `json`, or `sarif` |
|
|
350
379
|
| `--shell` | Choice | `posix` | Execution mode: `posix` (default), `shebang`, or `auto` |
|
|
351
380
|
| `--mem-limit` | String | `256m` | Memory limit per container (e.g. `256m`, `512m`) |
|
|
352
381
|
| `--pids-limit`| Integer | `128` | Maximum number of processes per container |
|
|
@@ -383,14 +412,6 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
|
|
|
383
412
|
Explicit CLI options override project settings. Explicit script paths bypass discovery
|
|
384
413
|
and its exclusions. See [complete configuration semantics](docs/configuration.md).
|
|
385
414
|
|
|
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
415
|
## 🧪 Development & Testing
|
|
395
416
|
|
|
396
417
|
```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.
|
|
13
|
+
## 当前版本 v0.8.0
|
|
14
14
|
|
|
15
|
-
v0.
|
|
15
|
+
v0.8.0 已作为正式版本发布;如果你要从源码验证当前分支,也可以执行:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
pip install -e .
|
|
@@ -28,14 +28,21 @@ opsscript-gate run --dry-run
|
|
|
28
28
|
opsscript-gate run --format json --output reports/compatibility.json
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
`init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.
|
|
31
|
+
`init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.8.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
|
-
`--network bridge
|
|
42
|
+
`--network bridge`。安装器如果需要 `curl`、证书或其他明确工具,可以配置
|
|
43
|
+
`packages`;运行器会用镜像自带的 `apt-get` 或 `apk` 安装它们。包安装是可选的,
|
|
44
|
+
只接受包名,不接受 Shell 命令,并且必须使用 `bridge` 网络。保留现有无配置行为:
|
|
45
|
+
四个发行版、POSIX shell、bridge 网络。
|
|
39
46
|
|
|
40
47
|
## 本轮改进
|
|
41
48
|
|
|
@@ -58,7 +65,7 @@ Docker daemon 和可选的项目配置。
|
|
|
58
65
|
- uses: actions/checkout@v7
|
|
59
66
|
with:
|
|
60
67
|
fetch-depth: 0
|
|
61
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
68
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
62
69
|
with:
|
|
63
70
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
64
71
|
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
|
|
26
|
+
description: 'Output report format: table, markdown, json, or sarif'
|
|
27
27
|
required: false
|
|
28
28
|
default: ''
|
|
29
29
|
shell:
|
|
@@ -42,6 +42,10 @@ inputs:
|
|
|
42
42
|
description: "Container network mode: 'bridge' or 'none' (default: bridge)"
|
|
43
43
|
required: false
|
|
44
44
|
default: ''
|
|
45
|
+
packages:
|
|
46
|
+
description: 'Optional comma- or newline-separated packages to install before running (requires network=bridge)'
|
|
47
|
+
required: false
|
|
48
|
+
default: ''
|
|
45
49
|
|
|
46
50
|
config:
|
|
47
51
|
description: 'Path to project JSON configuration'
|
|
@@ -97,6 +101,7 @@ runs:
|
|
|
97
101
|
INPUT_MEM_LIMIT: ${{ inputs.mem-limit }}
|
|
98
102
|
INPUT_PIDS_LIMIT: ${{ inputs.pids-limit }}
|
|
99
103
|
INPUT_NETWORK: ${{ inputs.network }}
|
|
104
|
+
INPUT_PACKAGES: ${{ inputs.packages }}
|
|
100
105
|
INPUT_CONFIG: ${{ inputs.config }}
|
|
101
106
|
INPUT_PRESET: ${{ inputs.preset }}
|
|
102
107
|
INPUT_EXCLUDE: ${{ inputs.exclude }}
|
|
@@ -116,6 +121,7 @@ runs:
|
|
|
116
121
|
[ -z "$INPUT_MEM_LIMIT" ] || ARGS+=(--mem-limit "$INPUT_MEM_LIMIT")
|
|
117
122
|
[ -z "$INPUT_PIDS_LIMIT" ] || ARGS+=(--pids-limit "$INPUT_PIDS_LIMIT")
|
|
118
123
|
[ -z "$INPUT_NETWORK" ] || ARGS+=(--network "$INPUT_NETWORK")
|
|
124
|
+
[ -z "$INPUT_PACKAGES" ] || ARGS+=(--packages "$INPUT_PACKAGES")
|
|
119
125
|
[ -z "$INPUT_MATRIX" ] || ARGS+=(--matrix "$INPUT_MATRIX")
|
|
120
126
|
[ -z "$INPUT_JOBS" ] || ARGS+=(--jobs "$INPUT_JOBS")
|
|
121
127
|
[ -z "$INPUT_CONFIG" ] || ARGS+=(--config "$INPUT_CONFIG")
|
|
@@ -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.
|
|
15
|
+
`.github/workflows/opsscript-gate.yml`. The generated workflow uses the v0.8.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.
|
|
@@ -34,6 +34,7 @@ directory, not the config file's parent.
|
|
|
34
34
|
"preset": "minimal",
|
|
35
35
|
"shell": "auto",
|
|
36
36
|
"network": "none",
|
|
37
|
+
"packages": [],
|
|
37
38
|
"timeout": 30,
|
|
38
39
|
"jobs": 2,
|
|
39
40
|
"mem_limit": "256m",
|
|
@@ -46,7 +47,11 @@ directory, not the config file's parent.
|
|
|
46
47
|
These are all supported keys, plus `matrix`, a comma-separated image string used
|
|
47
48
|
instead of `preset`. Unknown keys and incorrect value types are errors. Numeric
|
|
48
49
|
limits must be positive integers. `shell` accepts `posix`, `auto`, `shebang`;
|
|
49
|
-
`network` accepts `none`, `bridge`.
|
|
50
|
+
`network` accepts `none`, `bridge`. `packages` is an optional list of package
|
|
51
|
+
names to install inside each test container before the target script runs. The
|
|
52
|
+
runner uses `apt-get` for Debian/Ubuntu images and `apk` for Alpine images. It
|
|
53
|
+
accepts package names only, never shell commands, and requires `network: bridge`.
|
|
54
|
+
The default is an empty list, so existing networking settings and execution remain unchanged.
|
|
50
55
|
|
|
51
56
|
| Preset | Images |
|
|
52
57
|
| --- | --- |
|
|
@@ -99,7 +104,7 @@ For pull requests, set `changed-since` to the base commit and fetch full Git his
|
|
|
99
104
|
- uses: actions/checkout@v7
|
|
100
105
|
with:
|
|
101
106
|
fetch-depth: 0
|
|
102
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
107
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
103
108
|
with:
|
|
104
109
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
105
110
|
preset: minimal
|
|
@@ -109,11 +114,15 @@ The Action passes when no changed shell scripts are selected. This keeps unrelat
|
|
|
109
114
|
documentation or application changes from starting container jobs.
|
|
110
115
|
|
|
111
116
|
```yaml
|
|
112
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
117
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
113
118
|
with:
|
|
114
119
|
config: .opsscript-gate.json
|
|
115
120
|
format: json
|
|
116
121
|
output: reports/compatibility.json
|
|
122
|
+
network: bridge
|
|
123
|
+
packages: |
|
|
124
|
+
curl
|
|
125
|
+
ca-certificates
|
|
117
126
|
exclude: |
|
|
118
127
|
tests/*
|
|
119
128
|
examples/*
|
|
@@ -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.
|
|
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.8.0 Action release.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "opsscript-gate"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.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" }
|
|
@@ -9,14 +9,16 @@ 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 DEFAULTS, PRESETS, init_project, load_config
|
|
12
|
+
from opsscript_gate.config import DEFAULTS, PRESETS, init_project, load_config, validate_packages
|
|
13
13
|
from opsscript_gate.models import MultiScriptReport, RunReport
|
|
14
14
|
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 '
|
|
98
|
+
help="Output report format: 'table' (default ASCII), 'markdown', 'json', or 'sarif'",
|
|
97
99
|
)
|
|
98
100
|
run_parser.add_argument(
|
|
99
101
|
"--shell",
|
|
@@ -119,6 +121,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
119
121
|
default="bridge",
|
|
120
122
|
help="Network mode for containers: 'bridge' or 'none' (default: bridge)",
|
|
121
123
|
)
|
|
124
|
+
run_parser.add_argument(
|
|
125
|
+
"--packages",
|
|
126
|
+
action="append",
|
|
127
|
+
default=None,
|
|
128
|
+
help="Optional comma-separated packages to install in each container before running (repeatable; requires network=bridge)",
|
|
129
|
+
)
|
|
122
130
|
run_parser.add_argument(
|
|
123
131
|
"--changed-since",
|
|
124
132
|
help="Only test shell scripts changed between this Git revision and HEAD",
|
|
@@ -141,6 +149,19 @@ def parse_matrix_argument(matrix_raw: str | None) -> list[str]:
|
|
|
141
149
|
return images if images else DEFAULT_MATRIX
|
|
142
150
|
|
|
143
151
|
|
|
152
|
+
def parse_packages_argument(packages_raw: list[str] | None) -> list[str]:
|
|
153
|
+
"""Parse repeated comma/newline-separated package arguments."""
|
|
154
|
+
if not packages_raw:
|
|
155
|
+
return []
|
|
156
|
+
packages: list[str] = []
|
|
157
|
+
for raw in packages_raw:
|
|
158
|
+
for package in raw.replace("\r", "\n").replace(",", "\n").split("\n"):
|
|
159
|
+
name = package.strip()
|
|
160
|
+
if name:
|
|
161
|
+
packages.append(name)
|
|
162
|
+
return validate_packages(packages)
|
|
163
|
+
|
|
164
|
+
|
|
144
165
|
def main(argv: list[str] | None = None) -> int:
|
|
145
166
|
"""Main CLI entrypoint."""
|
|
146
167
|
parser = build_parser()
|
|
@@ -213,6 +234,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
213
234
|
raise ValueError(f"{key.replace('_', '-')} must be positive")
|
|
214
235
|
if args.network not in ("bridge", "none"):
|
|
215
236
|
raise ValueError("network must be bridge or none")
|
|
237
|
+
args.packages = parse_packages_argument(args.packages)
|
|
238
|
+
if args.packages and args.network == "none":
|
|
239
|
+
raise ValueError("packages require network=bridge")
|
|
216
240
|
if args.matrix is not None and not any(x.strip() for x in args.matrix.split(",")):
|
|
217
241
|
raise ValueError("matrix must contain at least one image")
|
|
218
242
|
except ValueError as err:
|
|
@@ -261,13 +285,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
261
285
|
if args.dry_run:
|
|
262
286
|
plan = {"dry_run": True, "scripts": target_scripts, "matrix": matrix,
|
|
263
287
|
"executions": len(target_scripts) * len(matrix), "shell": args.shell,
|
|
264
|
-
"network": args.network, "timeout": args.timeout}
|
|
288
|
+
"network": args.network, "packages": args.packages, "timeout": args.timeout}
|
|
265
289
|
if changed_selection:
|
|
266
290
|
plan["changed_since"] = args.changed_since
|
|
267
291
|
output = json.dumps(plan, indent=2) if args.format == "json" else (
|
|
268
292
|
"Execution preview (no scripts executed)\n"
|
|
269
293
|
+ "\n".join(f" {path}" for path in target_scripts)
|
|
270
294
|
+ f"\nImages: {', '.join(matrix)}\nContainer executions: {plan['executions']}"
|
|
295
|
+
+ (f"\nPackages: {', '.join(args.packages)}" if args.packages else "")
|
|
271
296
|
+ f"\nShell: {args.shell} | Network: {args.network} | Timeout: {args.timeout}s"
|
|
272
297
|
)
|
|
273
298
|
return 0 if publish_output(output, args.output) else 1
|
|
@@ -290,6 +315,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
290
315
|
mem_limit=args.mem_limit,
|
|
291
316
|
pids_limit=args.pids_limit,
|
|
292
317
|
network=args.network,
|
|
318
|
+
packages=args.packages,
|
|
293
319
|
)
|
|
294
320
|
except DockerDaemonError as err:
|
|
295
321
|
sys.stderr.write(f"Docker Error: {err}\n")
|
|
@@ -301,6 +327,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
301
327
|
# Format report output
|
|
302
328
|
if args.format == "json":
|
|
303
329
|
output = format_json(report)
|
|
330
|
+
elif args.format == "sarif":
|
|
331
|
+
output = format_sarif(report, script_path=script_path)
|
|
304
332
|
elif args.format == "markdown":
|
|
305
333
|
output = format_github_summary(report, script_path=script_path)
|
|
306
334
|
else:
|
|
@@ -332,6 +360,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
332
360
|
mem_limit=args.mem_limit,
|
|
333
361
|
pids_limit=args.pids_limit,
|
|
334
362
|
network=args.network,
|
|
363
|
+
packages=args.packages,
|
|
335
364
|
)
|
|
336
365
|
except DockerDaemonError as err:
|
|
337
366
|
sys.stderr.write(f"Docker Error: {err}\n")
|
|
@@ -353,6 +382,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
353
382
|
|
|
354
383
|
if args.format == "json":
|
|
355
384
|
output = format_json(multi_report)
|
|
385
|
+
elif args.format == "sarif":
|
|
386
|
+
output = format_multi_sarif(multi_report)
|
|
356
387
|
elif args.format == "markdown":
|
|
357
388
|
output = format_multi_github_summary(multi_report)
|
|
358
389
|
else:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import re
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
CONFIG_NAME = ".opsscript-gate.json"
|
|
@@ -12,9 +13,26 @@ PRESETS = {
|
|
|
12
13
|
DEFAULTS = {
|
|
13
14
|
"matrix": None, "preset": None, "jobs": None, "timeout": 60,
|
|
14
15
|
"shell": "posix", "mem_limit": "256m", "pids_limit": 128,
|
|
15
|
-
"network": "bridge", "exclude": [], "max_scripts": 20,
|
|
16
|
+
"network": "bridge", "packages": [], "exclude": [], "max_scripts": 20,
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
PACKAGE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9+._:-]*$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_packages(value: object) -> list[str]:
|
|
23
|
+
"""Validate and normalize package names used for optional container setup."""
|
|
24
|
+
if not isinstance(value, list) or not all(isinstance(p, str) for p in value):
|
|
25
|
+
raise ValueError("packages must be a list of package names")
|
|
26
|
+
packages: list[str] = []
|
|
27
|
+
for package in value:
|
|
28
|
+
name = package.strip()
|
|
29
|
+
if not name or not PACKAGE_NAME_RE.fullmatch(name):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"packages must contain nonempty package names using only letters, numbers, '+', '.', '_', ':', or '-'")
|
|
32
|
+
if name not in packages:
|
|
33
|
+
packages.append(name)
|
|
34
|
+
return packages
|
|
35
|
+
|
|
18
36
|
|
|
19
37
|
def load_config(filename: str | None) -> dict:
|
|
20
38
|
path = Path(filename or CONFIG_NAME)
|
|
@@ -36,6 +54,8 @@ def load_config(filename: str | None) -> dict:
|
|
|
36
54
|
elif key == "exclude":
|
|
37
55
|
if not isinstance(value, list) or not all(isinstance(p, str) and p for p in value):
|
|
38
56
|
raise ValueError("exclude must be a list of nonempty glob strings")
|
|
57
|
+
elif key == "packages":
|
|
58
|
+
validate_packages(value)
|
|
39
59
|
elif not isinstance(value, str) or not value.strip():
|
|
40
60
|
raise ValueError(f"{key} must be a nonempty string")
|
|
41
61
|
for key, choices in {"shell": ("posix", "auto", "shebang"),
|
|
@@ -63,7 +83,7 @@ jobs:
|
|
|
63
83
|
- uses: actions/checkout@v7
|
|
64
84
|
with:
|
|
65
85
|
persist-credentials: false
|
|
66
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
86
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
67
87
|
with:
|
|
68
88
|
config: .opsscript-gate.json
|
|
69
89
|
""",
|
|
@@ -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 = "",
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
+
import shlex
|
|
5
6
|
import tempfile
|
|
6
7
|
import time
|
|
7
8
|
from typing import Any, Sequence
|
|
@@ -12,6 +13,7 @@ from docker.errors import DockerException, ImageNotFound
|
|
|
12
13
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
from enum import Enum
|
|
16
|
+
from opsscript_gate.config import PACKAGE_NAME_RE
|
|
15
17
|
from opsscript_gate.models import DistroStatus, FailureDiagnostic, RunReport, ShellMode, SingleResult
|
|
16
18
|
from opsscript_gate.remediation import generate_remediation_hint
|
|
17
19
|
|
|
@@ -574,6 +576,32 @@ def _resolve_shell_command(
|
|
|
574
576
|
return None, f"Unsupported shell mode: {mode}"
|
|
575
577
|
|
|
576
578
|
|
|
579
|
+
def _wrap_with_package_setup(command: Sequence[str], packages: Sequence[str]) -> list[str]:
|
|
580
|
+
"""Preinstall explicit packages with the image's native package manager."""
|
|
581
|
+
if not packages:
|
|
582
|
+
return list(command)
|
|
583
|
+
|
|
584
|
+
if any(not isinstance(package, str) or not PACKAGE_NAME_RE.fullmatch(package) for package in packages):
|
|
585
|
+
raise ValueError("Invalid package name")
|
|
586
|
+
|
|
587
|
+
package_args = " ".join(shlex.quote(package) for package in packages)
|
|
588
|
+
install = (
|
|
589
|
+
"if command -v apt-get >/dev/null 2>&1; then "
|
|
590
|
+
"mkdir -p /tmp/opsscript-apt-archives/partial && "
|
|
591
|
+
"apt-get -o APT::Sandbox::User=root "
|
|
592
|
+
"-o Dir::Cache::archives=/tmp/opsscript-apt-archives/ update && "
|
|
593
|
+
f"apt-get -o APT::Sandbox::User=root "
|
|
594
|
+
"-o Dir::Cache::archives=/tmp/opsscript-apt-archives/ "
|
|
595
|
+
f"install -y --no-install-recommends {package_args} && "
|
|
596
|
+
"rm -rf /tmp/opsscript-apt-archives /var/lib/apt/lists/*; "
|
|
597
|
+
"elif command -v apk >/dev/null 2>&1; then "
|
|
598
|
+
f"apk add --no-cache {package_args}; "
|
|
599
|
+
"else echo 'No supported package manager found (apt-get or apk)' >&2; exit 125; fi"
|
|
600
|
+
)
|
|
601
|
+
target_command = " ".join(shlex.quote(part) for part in command)
|
|
602
|
+
return ["/bin/sh", "-c", f"{install} && exec {target_command}"]
|
|
603
|
+
|
|
604
|
+
|
|
577
605
|
def run_on_distro(
|
|
578
606
|
client: docker.DockerClient,
|
|
579
607
|
script_path: str,
|
|
@@ -585,12 +613,24 @@ def run_on_distro(
|
|
|
585
613
|
mem_limit: str = "256m",
|
|
586
614
|
pids_limit: int = 128,
|
|
587
615
|
network: str = "bridge",
|
|
616
|
+
packages: Sequence[str] | None = None,
|
|
588
617
|
prepared_script_path: str | None = None,
|
|
589
618
|
) -> SingleResult:
|
|
590
619
|
"""
|
|
591
620
|
Run a target script inside an unprivileged, non-interactive container.
|
|
592
621
|
Supports posix, shebang, and auto execution modes with strict resource limits.
|
|
593
622
|
"""
|
|
623
|
+
package_list = list(packages or [])
|
|
624
|
+
if package_list and network == "none":
|
|
625
|
+
return SingleResult(
|
|
626
|
+
distro=distro,
|
|
627
|
+
status=DistroStatus.ERROR,
|
|
628
|
+
exit_code=None,
|
|
629
|
+
duration=0.0,
|
|
630
|
+
output_snippet="",
|
|
631
|
+
error_message="Package setup requires network mode 'bridge'",
|
|
632
|
+
)
|
|
633
|
+
|
|
594
634
|
try:
|
|
595
635
|
mode = ShellMode(shell_mode)
|
|
596
636
|
except ValueError:
|
|
@@ -647,6 +687,7 @@ def run_on_distro(
|
|
|
647
687
|
)
|
|
648
688
|
|
|
649
689
|
safe_mount_src = normalize_host_path_for_docker(mount_src)
|
|
690
|
+
container_command = _wrap_with_package_setup(command, package_list)
|
|
650
691
|
|
|
651
692
|
# Security: Strict unprivileged options and read-only mount
|
|
652
693
|
volumes = {
|
|
@@ -667,7 +708,7 @@ def run_on_distro(
|
|
|
667
708
|
try:
|
|
668
709
|
container = client.containers.create(
|
|
669
710
|
image=distro,
|
|
670
|
-
command=
|
|
711
|
+
command=container_command,
|
|
671
712
|
volumes=volumes,
|
|
672
713
|
environment=environment,
|
|
673
714
|
stdin_open=False,
|
|
@@ -684,7 +725,7 @@ def run_on_distro(
|
|
|
684
725
|
client.images.pull(distro)
|
|
685
726
|
container = client.containers.create(
|
|
686
727
|
image=distro,
|
|
687
|
-
command=
|
|
728
|
+
command=container_command,
|
|
688
729
|
volumes=volumes,
|
|
689
730
|
environment=environment,
|
|
690
731
|
stdin_open=False,
|
|
@@ -799,6 +840,7 @@ def run_matrix(
|
|
|
799
840
|
mem_limit: str = "256m",
|
|
800
841
|
pids_limit: int = 128,
|
|
801
842
|
network: str = "bridge",
|
|
843
|
+
packages: Sequence[str] | None = None,
|
|
802
844
|
) -> RunReport:
|
|
803
845
|
"""
|
|
804
846
|
Run the compatibility check across all specified Linux distributions,
|
|
@@ -817,6 +859,9 @@ def run_matrix(
|
|
|
817
859
|
parsed_shebang = inspect_shebang(abs_script)
|
|
818
860
|
|
|
819
861
|
distro_list = list(matrix) if matrix else DEFAULT_MATRIX
|
|
862
|
+
package_list = list(packages or [])
|
|
863
|
+
if package_list and network == "none":
|
|
864
|
+
raise ValueError("Package setup requires network mode 'bridge'")
|
|
820
865
|
docker_client = client or get_docker_client()
|
|
821
866
|
|
|
822
867
|
effective_jobs = jobs if (jobs is not None and jobs > 0) else min(2, len(distro_list))
|
|
@@ -859,6 +904,7 @@ def run_matrix(
|
|
|
859
904
|
mem_limit=mem_limit,
|
|
860
905
|
pids_limit=pids_limit,
|
|
861
906
|
network=network,
|
|
907
|
+
packages=package_list,
|
|
862
908
|
prepared_script_path=prepared_script,
|
|
863
909
|
)
|
|
864
910
|
return index, res
|
|
@@ -7,13 +7,14 @@ ROOT = Path(__file__).parents[1]
|
|
|
7
7
|
def test_action_metadata_is_valid_and_inputs_are_wired():
|
|
8
8
|
text = (ROOT / "action.yml").read_text(encoding="utf-8")
|
|
9
9
|
assert "using: 'composite'" in text
|
|
10
|
-
for name in ("config", "preset", "exclude", "max-scripts", "dry-run", "output", "changed-since"):
|
|
10
|
+
for name in ("config", "preset", "exclude", "max-scripts", "dry-run", "output", "changed-since", "packages"):
|
|
11
11
|
assert f" {name}:" in text
|
|
12
12
|
run_text = text
|
|
13
13
|
for flag in ("--timeout", "--format", "--shell", "--mem-limit", "--pids-limit",
|
|
14
|
-
"--network", "--matrix", "--jobs", "--config", "--preset",
|
|
14
|
+
"--network", "--packages", "--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
|
|
|
@@ -69,7 +69,8 @@ def test_init_existing_workflow_leaves_config_absent(project):
|
|
|
69
69
|
|
|
70
70
|
@pytest.mark.parametrize("config", [[], {"typo": 1}, {"timeout": True},
|
|
71
71
|
{"jobs": 0}, {"exclude": "tests/*"}, {"shell": "fish"}, {"network": "host"},
|
|
72
|
-
{"preset": "unknown"}, {"matrix": "alpine", "preset": "minimal"}
|
|
72
|
+
{"preset": "unknown"}, {"matrix": "alpine", "preset": "minimal"},
|
|
73
|
+
{"packages": "curl"}, {"packages": ["curl; touch pwned"]}])
|
|
73
74
|
def test_invalid_config(project, config):
|
|
74
75
|
Path(CONFIG_NAME).write_text(json.dumps(config))
|
|
75
76
|
with patch("opsscript_gate.cli.run_matrix") as run:
|
|
@@ -88,6 +89,21 @@ def test_config_cli_precedence_and_saved_plan(project, capsys):
|
|
|
88
89
|
assert plan["timeout"] == 9
|
|
89
90
|
|
|
90
91
|
|
|
92
|
+
def test_packages_are_forwarded_and_visible_in_dry_run(project, capsys):
|
|
93
|
+
assert main([
|
|
94
|
+
"run", "install.sh", "--dry-run", "--format", "json",
|
|
95
|
+
"--packages", "curl,ca-certificates", "--packages", "tar",
|
|
96
|
+
]) == 0
|
|
97
|
+
plan = json.loads(capsys.readouterr().out)
|
|
98
|
+
assert plan["packages"] == ["curl", "ca-certificates", "tar"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_packages_reject_offline_network(project):
|
|
102
|
+
with patch("opsscript_gate.cli.run_matrix") as run:
|
|
103
|
+
assert main(["run", "install.sh", "--packages", "curl", "--network", "none"]) == 1
|
|
104
|
+
run.assert_not_called()
|
|
105
|
+
|
|
106
|
+
|
|
91
107
|
def test_preset_overrides_config_matrix(project, capsys):
|
|
92
108
|
Path(CONFIG_NAME).write_text('{"matrix": "custom:1"}')
|
|
93
109
|
assert main(["run", "--dry-run", "--preset", "ubuntu", "--format", "json"]) == 0
|
|
@@ -8,7 +8,7 @@ from unittest import mock
|
|
|
8
8
|
import pytest
|
|
9
9
|
from docker.errors import DockerException
|
|
10
10
|
|
|
11
|
-
from opsscript_gate.cli import build_parser, main, parse_matrix_argument
|
|
11
|
+
from opsscript_gate.cli import build_parser, main, parse_matrix_argument, parse_packages_argument
|
|
12
12
|
from opsscript_gate.discovery import discover_scripts, is_shell_script
|
|
13
13
|
from opsscript_gate.models import DistroStatus, FailureDiagnostic, MultiScriptReport, RunReport, ShellMode, SingleResult
|
|
14
14
|
from opsscript_gate.remediation import generate_remediation_hint, REMEDIATION_RULES
|
|
@@ -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
|
)
|
|
@@ -41,6 +43,7 @@ from opsscript_gate.runner import (
|
|
|
41
43
|
run_matrix,
|
|
42
44
|
run_on_distro,
|
|
43
45
|
sanitize_diagnostic_text,
|
|
46
|
+
_wrap_with_package_setup,
|
|
44
47
|
)
|
|
45
48
|
|
|
46
49
|
|
|
@@ -195,6 +198,50 @@ def test_run_on_distro_pass_mock(tmp_path):
|
|
|
195
198
|
mock_container.remove.assert_called_once_with(force=True)
|
|
196
199
|
|
|
197
200
|
|
|
201
|
+
def test_run_on_distro_package_setup_wraps_command(tmp_path):
|
|
202
|
+
script_file = tmp_path / "test.sh"
|
|
203
|
+
script_file.write_text("#!/bin/sh\necho OK\n", encoding="utf-8")
|
|
204
|
+
|
|
205
|
+
mock_client = mock.MagicMock()
|
|
206
|
+
mock_container = mock.MagicMock()
|
|
207
|
+
mock_container.status = "exited"
|
|
208
|
+
mock_container.attrs = {"State": {"ExitCode": 0}}
|
|
209
|
+
mock_container.logs.return_value = b"OK\n"
|
|
210
|
+
mock_client.containers.create.return_value = mock_container
|
|
211
|
+
|
|
212
|
+
result = run_on_distro(
|
|
213
|
+
mock_client,
|
|
214
|
+
str(script_file),
|
|
215
|
+
"alpine:3.20",
|
|
216
|
+
packages=["curl", "ca-certificates"],
|
|
217
|
+
network="bridge",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
assert result.status == DistroStatus.PASS
|
|
221
|
+
command = mock_client.containers.create.call_args.kwargs["command"]
|
|
222
|
+
assert command[:2] == ["/bin/sh", "-c"]
|
|
223
|
+
assert "Dir::Cache::archives=/tmp/opsscript-apt-archives/" in command[2]
|
|
224
|
+
assert "apk add --no-cache curl ca-certificates" in command[2]
|
|
225
|
+
assert "exec /bin/sh -c" in command[2]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_package_setup_requires_network(tmp_path):
|
|
229
|
+
script_file = tmp_path / "test.sh"
|
|
230
|
+
script_file.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
|
231
|
+
result = run_on_distro(
|
|
232
|
+
mock.MagicMock(), str(script_file), "alpine:3.20", packages=["curl"], network="none"
|
|
233
|
+
)
|
|
234
|
+
assert result.status == DistroStatus.ERROR
|
|
235
|
+
assert "requires network" in (result.error_message or "")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_package_setup_quotes_and_validates_names():
|
|
239
|
+
wrapped = _wrap_with_package_setup(["/bin/sh", "-c", "echo ok"], ["ca-certificates"])
|
|
240
|
+
assert "ca-certificates" in wrapped[2]
|
|
241
|
+
with pytest.raises(ValueError):
|
|
242
|
+
_wrap_with_package_setup(["/bin/sh"], ["curl;touch /tmp/pwned"])
|
|
243
|
+
|
|
244
|
+
|
|
198
245
|
def test_run_on_distro_fail_mock(tmp_path):
|
|
199
246
|
script_file = tmp_path / "test_fail.sh"
|
|
200
247
|
script_file.write_text("#!/bin/sh\napt-get: not found\nexit 127\n", encoding="utf-8")
|
|
@@ -300,6 +347,54 @@ def test_reporter_json():
|
|
|
300
347
|
assert data["results"][0]["status"] == "PASS"
|
|
301
348
|
|
|
302
349
|
|
|
350
|
+
def test_reporter_sarif_contains_failure_location_and_metadata():
|
|
351
|
+
result = SingleResult(
|
|
352
|
+
"alpine:3.20",
|
|
353
|
+
DistroStatus.FAIL,
|
|
354
|
+
127,
|
|
355
|
+
0.42,
|
|
356
|
+
diagnostic=FailureDiagnostic(
|
|
357
|
+
kind="missing_command",
|
|
358
|
+
message="apt-get: command not found",
|
|
359
|
+
command="apt-get",
|
|
360
|
+
line=4,
|
|
361
|
+
distro="alpine:3.20",
|
|
362
|
+
hint="Use apk instead of apt-get.",
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
report = RunReport([SingleResult("debian:12-slim", DistroStatus.PASS, 0, 0.2), result])
|
|
366
|
+
|
|
367
|
+
payload = json.loads(format_sarif(report, script_path="./scripts/install.sh"))
|
|
368
|
+
assert payload["$schema"].endswith("sarif-2.1.0.json")
|
|
369
|
+
assert payload["version"] == "2.1.0"
|
|
370
|
+
finding = payload["runs"][0]["results"][0]
|
|
371
|
+
assert finding["ruleId"] == "OSG001"
|
|
372
|
+
assert finding["level"] == "error"
|
|
373
|
+
assert finding["locations"][0]["physicalLocation"]["artifactLocation"]["uri"] == "scripts/install.sh"
|
|
374
|
+
assert finding["locations"][0]["physicalLocation"]["region"]["startLine"] == 4
|
|
375
|
+
assert finding["properties"]["distribution"] == "alpine:3.20"
|
|
376
|
+
assert finding["properties"]["command"] == "apt-get"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_reporter_multi_sarif_keeps_script_paths():
|
|
380
|
+
report = MultiScriptReport(
|
|
381
|
+
reports={
|
|
382
|
+
"install.sh": RunReport([SingleResult("alpine:3.20", DistroStatus.FAIL, 1, 0.1)]),
|
|
383
|
+
"scripts/setup.sh": RunReport([SingleResult("debian:12-slim", DistroStatus.TIMED_OUT, None, 5.0)]),
|
|
384
|
+
},
|
|
385
|
+
total_duration=5.1,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
payload = json.loads(format_multi_sarif(report))
|
|
389
|
+
results = payload["runs"][0]["results"]
|
|
390
|
+
assert len(results) == 2
|
|
391
|
+
paths = {
|
|
392
|
+
result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
|
|
393
|
+
for result in results
|
|
394
|
+
}
|
|
395
|
+
assert paths == {"install.sh", "scripts/setup.sh"}
|
|
396
|
+
|
|
397
|
+
|
|
303
398
|
def test_write_github_step_summary(tmp_path):
|
|
304
399
|
summary_file = tmp_path / "step_summary.md"
|
|
305
400
|
r1 = SingleResult("debian:12-slim", DistroStatus.PASS, 0, 1.0)
|
|
@@ -322,6 +417,14 @@ def test_parse_matrix_argument():
|
|
|
322
417
|
assert parse_matrix_argument("debian:12-slim, alpine:3.20") == ["debian:12-slim", "alpine:3.20"]
|
|
323
418
|
|
|
324
419
|
|
|
420
|
+
def test_parse_packages_argument():
|
|
421
|
+
assert parse_packages_argument(["curl, ca-certificates", "tar"]) == [
|
|
422
|
+
"curl", "ca-certificates", "tar"
|
|
423
|
+
]
|
|
424
|
+
with pytest.raises(ValueError, match="package"):
|
|
425
|
+
parse_packages_argument(["curl && touch pwned"])
|
|
426
|
+
|
|
427
|
+
|
|
325
428
|
def test_cli_help(capsys):
|
|
326
429
|
parser = build_parser()
|
|
327
430
|
with pytest.raises(SystemExit):
|
|
@@ -871,6 +974,27 @@ def test_integration_pass_basic():
|
|
|
871
974
|
assert report.results[0].status == DistroStatus.PASS
|
|
872
975
|
|
|
873
976
|
|
|
977
|
+
@pytest.mark.integration
|
|
978
|
+
@pytest.mark.skipif(not is_docker_daemon_available(), reason="Docker daemon is not running or accessible")
|
|
979
|
+
@pytest.mark.parametrize("image", DEFAULT_MATRIX)
|
|
980
|
+
def test_integration_package_setup(tmp_path, image):
|
|
981
|
+
script = tmp_path / "needs-curl.sh"
|
|
982
|
+
script.write_text(
|
|
983
|
+
"#!/usr/bin/env bash\nset -e\ncurl --version >/dev/null\ntar --version >/dev/null\n"
|
|
984
|
+
"test -s /etc/ssl/certs/ca-certificates.crt\n", encoding="utf-8"
|
|
985
|
+
)
|
|
986
|
+
report = run_matrix(
|
|
987
|
+
str(script),
|
|
988
|
+
matrix=[image],
|
|
989
|
+
packages=["bash", "curl", "ca-certificates", "tar"],
|
|
990
|
+
shell_mode="auto",
|
|
991
|
+
network="bridge",
|
|
992
|
+
timeout=120,
|
|
993
|
+
)
|
|
994
|
+
assert report.all_passed is True, report.to_dict()
|
|
995
|
+
assert report.results[0].status == DistroStatus.PASS
|
|
996
|
+
|
|
997
|
+
|
|
874
998
|
@pytest.mark.integration
|
|
875
999
|
@pytest.mark.skipif(not is_docker_daemon_available(), reason="Docker daemon is not running or accessible")
|
|
876
1000
|
def test_integration_fail_deps_alpine():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opsscript_gate-0.6.3 → opsscript_gate-0.8.0}/examples/alpine-incompatibility/apt_dependency.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|