opsscript-gate 0.7.0__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.7.0/.github/workflow-templates/properties.json → opsscript_gate-0.8.0/.github/workflow-templates/opsscript-gate.properties.json +2 -1
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.github/workflow-templates/opsscript-gate.yml +1 -1
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/CHANGELOG.md +5 -2
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/CITATION.cff +2 -2
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/PKG-INFO +10 -7
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/README.md +9 -6
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/README.zh-CN.md +8 -5
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/SECURITY.md +2 -2
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/action.yml +6 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/docs/configuration.md +13 -4
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/docs/gpt-oss.md +1 -1
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/examples/github-actions/README.md +1 -1
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/examples/github-actions/workflow.yml +1 -1
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/pyproject.toml +1 -1
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/src/opsscript_gate/__init__.py +1 -1
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/src/opsscript_gate/cli.py +27 -2
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/src/opsscript_gate/config.py +22 -2
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/src/opsscript_gate/runner.py +48 -2
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/tests/test_action_contract.py +2 -2
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/tests/test_onboarding.py +17 -1
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/tests/test_runner.py +75 -1
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.gitattributes +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.github/ISSUE_TEMPLATE/distro_request.yml +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.github/assets/social-preview.png +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.github/pull_request_template.md +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.github/workflows/demo.yml +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.github/workflows/release.yml +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.github/workflows/test.yml +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/.gitignore +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/CONTRIBUTING.md +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/LICENSE +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/ROADMAP.md +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/docs/troubleshooting.md +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/examples/alpine-incompatibility/README.md +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/examples/alpine-incompatibility/apt_dependency.sh +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/examples/basic/README.md +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/examples/basic/clean_setup.sh +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/examples/interactive-hang/README.md +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/examples/interactive-hang/prompt_confirm.sh +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/src/opsscript_gate/discovery.py +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/src/opsscript_gate/models.py +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/src/opsscript_gate/remediation.py +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/src/opsscript_gate/reporter.py +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/tests/fixtures/fail_deps.sh +0 -0
- {opsscript_gate-0.7.0 → opsscript_gate-0.8.0}/tests/fixtures/fail_hang.sh +0 -0
- {opsscript_gate-0.7.0 → 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,14 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.8.0] - 2026-10-02
|
|
4
4
|
|
|
5
5
|
### Added
|
|
6
|
-
- 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.
|
|
7
9
|
|
|
8
10
|
## [0.7.0] - 2026-10-01
|
|
9
11
|
|
|
10
12
|
### Added
|
|
11
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.
|
|
12
15
|
|
|
13
16
|
### Changed
|
|
14
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
|
|
@@ -56,7 +56,7 @@ Try the browser scanner: [InstallReady](https://mresyzz.github.io/InstallReady/?
|
|
|
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
|
|
@@ -104,7 +107,7 @@ jobs:
|
|
|
104
107
|
- uses: actions/checkout@v7
|
|
105
108
|
with:
|
|
106
109
|
persist-credentials: false
|
|
107
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
110
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
108
111
|
```
|
|
109
112
|
|
|
110
113
|
This zero-configuration form discovers shell scripts in the repository. Use
|
|
@@ -115,7 +118,7 @@ repository has a single installer. For self-hosted model installers, see the
|
|
|
115
118
|
Or test a specific script with custom execution modes:
|
|
116
119
|
|
|
117
120
|
```yaml
|
|
118
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
121
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
119
122
|
with:
|
|
120
123
|
script-path: scripts/install.sh
|
|
121
124
|
shell: auto
|
|
@@ -147,7 +150,7 @@ unrelated scripts:
|
|
|
147
150
|
with:
|
|
148
151
|
persist-credentials: false
|
|
149
152
|
fetch-depth: 0
|
|
150
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
153
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
151
154
|
with:
|
|
152
155
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
153
156
|
preset: minimal
|
|
@@ -230,7 +233,7 @@ Use `sarif` when the result should appear in GitHub Code Scanning or another
|
|
|
230
233
|
SARIF-compatible viewer. Upload it explicitly with the official upload action:
|
|
231
234
|
|
|
232
235
|
```yaml
|
|
233
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
236
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
234
237
|
with:
|
|
235
238
|
script-path: scripts/install.sh
|
|
236
239
|
format: sarif
|
|
@@ -26,7 +26,7 @@ Try the browser scanner: [InstallReady](https://mresyzz.github.io/InstallReady/?
|
|
|
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
|
|
@@ -74,7 +77,7 @@ jobs:
|
|
|
74
77
|
- uses: actions/checkout@v7
|
|
75
78
|
with:
|
|
76
79
|
persist-credentials: false
|
|
77
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
80
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
This zero-configuration form discovers shell scripts in the repository. Use
|
|
@@ -85,7 +88,7 @@ repository has a single installer. For self-hosted model installers, see the
|
|
|
85
88
|
Or test a specific script with custom execution modes:
|
|
86
89
|
|
|
87
90
|
```yaml
|
|
88
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
91
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
89
92
|
with:
|
|
90
93
|
script-path: scripts/install.sh
|
|
91
94
|
shell: auto
|
|
@@ -117,7 +120,7 @@ unrelated scripts:
|
|
|
117
120
|
with:
|
|
118
121
|
persist-credentials: false
|
|
119
122
|
fetch-depth: 0
|
|
120
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
123
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
121
124
|
with:
|
|
122
125
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
123
126
|
preset: minimal
|
|
@@ -200,7 +203,7 @@ Use `sarif` when the result should appear in GitHub Code Scanning or another
|
|
|
200
203
|
SARIF-compatible viewer. Upload it explicitly with the official upload action:
|
|
201
204
|
|
|
202
205
|
```yaml
|
|
203
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
206
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
204
207
|
with:
|
|
205
208
|
script-path: scripts/install.sh
|
|
206
209
|
format: sarif
|
|
@@ -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,7 +28,7 @@ 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。
|
|
@@ -39,7 +39,10 @@ opsscript-gate run --format json --output reports/compatibility.json
|
|
|
39
39
|
|
|
40
40
|
生成的配置默认排除 `tests/*`、`examples/*`,使用 Debian + Alpine,尊重
|
|
41
41
|
脚本 shebang,并关闭网络。请根据实际脚本审核配置;需要下载文件时启用
|
|
42
|
-
`--network bridge
|
|
42
|
+
`--network bridge`。安装器如果需要 `curl`、证书或其他明确工具,可以配置
|
|
43
|
+
`packages`;运行器会用镜像自带的 `apt-get` 或 `apk` 安装它们。包安装是可选的,
|
|
44
|
+
只接受包名,不接受 Shell 命令,并且必须使用 `bridge` 网络。保留现有无配置行为:
|
|
45
|
+
四个发行版、POSIX shell、bridge 网络。
|
|
43
46
|
|
|
44
47
|
## 本轮改进
|
|
45
48
|
|
|
@@ -62,7 +65,7 @@ Docker daemon 和可选的项目配置。
|
|
|
62
65
|
- uses: actions/checkout@v7
|
|
63
66
|
with:
|
|
64
67
|
fetch-depth: 0
|
|
65
|
-
- uses: Mresyzz/opsscript-gate@v0.
|
|
68
|
+
- uses: Mresyzz/opsscript-gate@v0.8.0
|
|
66
69
|
with:
|
|
67
70
|
changed-since: ${{ github.event.pull_request.base.sha }}
|
|
68
71
|
preset: minimal
|
|
@@ -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,7 +9,7 @@ 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,
|
|
@@ -121,6 +121,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
121
121
|
default="bridge",
|
|
122
122
|
help="Network mode for containers: 'bridge' or 'none' (default: bridge)",
|
|
123
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
|
+
)
|
|
124
130
|
run_parser.add_argument(
|
|
125
131
|
"--changed-since",
|
|
126
132
|
help="Only test shell scripts changed between this Git revision and HEAD",
|
|
@@ -143,6 +149,19 @@ def parse_matrix_argument(matrix_raw: str | None) -> list[str]:
|
|
|
143
149
|
return images if images else DEFAULT_MATRIX
|
|
144
150
|
|
|
145
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
|
+
|
|
146
165
|
def main(argv: list[str] | None = None) -> int:
|
|
147
166
|
"""Main CLI entrypoint."""
|
|
148
167
|
parser = build_parser()
|
|
@@ -215,6 +234,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
215
234
|
raise ValueError(f"{key.replace('_', '-')} must be positive")
|
|
216
235
|
if args.network not in ("bridge", "none"):
|
|
217
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")
|
|
218
240
|
if args.matrix is not None and not any(x.strip() for x in args.matrix.split(",")):
|
|
219
241
|
raise ValueError("matrix must contain at least one image")
|
|
220
242
|
except ValueError as err:
|
|
@@ -263,13 +285,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
263
285
|
if args.dry_run:
|
|
264
286
|
plan = {"dry_run": True, "scripts": target_scripts, "matrix": matrix,
|
|
265
287
|
"executions": len(target_scripts) * len(matrix), "shell": args.shell,
|
|
266
|
-
"network": args.network, "timeout": args.timeout}
|
|
288
|
+
"network": args.network, "packages": args.packages, "timeout": args.timeout}
|
|
267
289
|
if changed_selection:
|
|
268
290
|
plan["changed_since"] = args.changed_since
|
|
269
291
|
output = json.dumps(plan, indent=2) if args.format == "json" else (
|
|
270
292
|
"Execution preview (no scripts executed)\n"
|
|
271
293
|
+ "\n".join(f" {path}" for path in target_scripts)
|
|
272
294
|
+ f"\nImages: {', '.join(matrix)}\nContainer executions: {plan['executions']}"
|
|
295
|
+
+ (f"\nPackages: {', '.join(args.packages)}" if args.packages else "")
|
|
273
296
|
+ f"\nShell: {args.shell} | Network: {args.network} | Timeout: {args.timeout}s"
|
|
274
297
|
)
|
|
275
298
|
return 0 if publish_output(output, args.output) else 1
|
|
@@ -292,6 +315,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
292
315
|
mem_limit=args.mem_limit,
|
|
293
316
|
pids_limit=args.pids_limit,
|
|
294
317
|
network=args.network,
|
|
318
|
+
packages=args.packages,
|
|
295
319
|
)
|
|
296
320
|
except DockerDaemonError as err:
|
|
297
321
|
sys.stderr.write(f"Docker Error: {err}\n")
|
|
@@ -336,6 +360,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
336
360
|
mem_limit=args.mem_limit,
|
|
337
361
|
pids_limit=args.pids_limit,
|
|
338
362
|
network=args.network,
|
|
363
|
+
packages=args.packages,
|
|
339
364
|
)
|
|
340
365
|
except DockerDaemonError as err:
|
|
341
366
|
sys.stderr.write(f"Docker Error: {err}\n")
|
|
@@ -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
|
""",
|
|
@@ -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,11 +7,11 @@ 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
17
|
assert "json, or sarif" in text
|
|
@@ -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
|
|
@@ -43,6 +43,7 @@ from opsscript_gate.runner import (
|
|
|
43
43
|
run_matrix,
|
|
44
44
|
run_on_distro,
|
|
45
45
|
sanitize_diagnostic_text,
|
|
46
|
+
_wrap_with_package_setup,
|
|
46
47
|
)
|
|
47
48
|
|
|
48
49
|
|
|
@@ -197,6 +198,50 @@ def test_run_on_distro_pass_mock(tmp_path):
|
|
|
197
198
|
mock_container.remove.assert_called_once_with(force=True)
|
|
198
199
|
|
|
199
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
|
+
|
|
200
245
|
def test_run_on_distro_fail_mock(tmp_path):
|
|
201
246
|
script_file = tmp_path / "test_fail.sh"
|
|
202
247
|
script_file.write_text("#!/bin/sh\napt-get: not found\nexit 127\n", encoding="utf-8")
|
|
@@ -372,6 +417,14 @@ def test_parse_matrix_argument():
|
|
|
372
417
|
assert parse_matrix_argument("debian:12-slim, alpine:3.20") == ["debian:12-slim", "alpine:3.20"]
|
|
373
418
|
|
|
374
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
|
+
|
|
375
428
|
def test_cli_help(capsys):
|
|
376
429
|
parser = build_parser()
|
|
377
430
|
with pytest.raises(SystemExit):
|
|
@@ -921,6 +974,27 @@ def test_integration_pass_basic():
|
|
|
921
974
|
assert report.results[0].status == DistroStatus.PASS
|
|
922
975
|
|
|
923
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
|
+
|
|
924
998
|
@pytest.mark.integration
|
|
925
999
|
@pytest.mark.skipif(not is_docker_daemon_available(), reason="Docker daemon is not running or accessible")
|
|
926
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.7.0 → 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
|
|
File without changes
|