opsscript-gate 0.5.1__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. opsscript_gate-0.6.1/.github/workflow-templates/opsscript-gate.yml +41 -0
  2. opsscript_gate-0.6.1/.github/workflow-templates/properties.json +6 -0
  3. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/CHANGELOG.md +17 -0
  4. opsscript_gate-0.6.1/CITATION.cff +12 -0
  5. opsscript_gate-0.5.1/README.md → opsscript_gate-0.6.1/PKG-INFO +67 -8
  6. opsscript_gate-0.5.1/PKG-INFO → opsscript_gate-0.6.1/README.md +38 -36
  7. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/README.zh-CN.md +23 -4
  8. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/ROADMAP.md +8 -1
  9. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/action.yml +6 -0
  10. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/docs/configuration.md +17 -2
  11. opsscript_gate-0.6.1/docs/gpt-oss.md +80 -0
  12. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/examples/github-actions/README.md +1 -1
  13. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/examples/github-actions/workflow.yml +1 -1
  14. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/pyproject.toml +9 -2
  15. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/src/opsscript_gate/__init__.py +1 -1
  16. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/src/opsscript_gate/cli.py +26 -2
  17. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/src/opsscript_gate/config.py +1 -1
  18. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/src/opsscript_gate/discovery.py +64 -0
  19. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/tests/test_action_contract.py +2 -1
  20. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/tests/test_onboarding.py +35 -1
  21. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.gitattributes +0 -0
  22. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  23. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.github/ISSUE_TEMPLATE/distro_request.yml +0 -0
  24. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  25. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.github/assets/social-preview.png +0 -0
  26. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.github/pull_request_template.md +0 -0
  27. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.github/workflows/demo.yml +0 -0
  28. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.github/workflows/release.yml +0 -0
  29. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.github/workflows/test.yml +0 -0
  30. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/.gitignore +0 -0
  31. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/CONTRIBUTING.md +0 -0
  32. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/LICENSE +0 -0
  33. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/SECURITY.md +0 -0
  34. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/docs/troubleshooting.md +0 -0
  35. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/examples/alpine-incompatibility/README.md +0 -0
  36. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/examples/alpine-incompatibility/apt_dependency.sh +0 -0
  37. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/examples/basic/README.md +0 -0
  38. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/examples/basic/clean_setup.sh +0 -0
  39. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/examples/interactive-hang/README.md +0 -0
  40. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/examples/interactive-hang/prompt_confirm.sh +0 -0
  41. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/src/opsscript_gate/models.py +0 -0
  42. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/src/opsscript_gate/remediation.py +0 -0
  43. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/src/opsscript_gate/reporter.py +0 -0
  44. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/src/opsscript_gate/runner.py +0 -0
  45. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/tests/fixtures/fail_deps.sh +0 -0
  46. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/tests/fixtures/fail_hang.sh +0 -0
  47. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/tests/fixtures/pass_basic.sh +0 -0
  48. {opsscript_gate-0.5.1 → opsscript_gate-0.6.1}/tests/test_runner.py +0 -0
@@ -0,0 +1,41 @@
1
+ name: Shell compatibility
2
+
3
+ on:
4
+ pull_request:
5
+ paths:
6
+ - '**/*.sh'
7
+ - '**/*.bash'
8
+ push:
9
+ branches: [main]
10
+ paths:
11
+ - '**/*.sh'
12
+ - '**/*.bash'
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ shell-compatibility:
19
+ name: Debian, Ubuntu and Alpine
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+ with:
25
+ persist-credentials: false
26
+
27
+ - name: Run OpsScript Gate
28
+ uses: Mresyzz/opsscript-gate@v0.6.0
29
+ with:
30
+ shell: auto
31
+ network: none
32
+ format: markdown
33
+ output: reports/opsscript-gate.md
34
+
35
+ - name: Upload compatibility report
36
+ if: always()
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: opsscript-gate-report
40
+ path: reports/opsscript-gate.md
41
+ if-no-files-found: ignore
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "OpsScript Gate shell compatibility",
3
+ "description": "Run shell scripts in Debian, Ubuntu and Alpine containers on changes to shell files.",
4
+ "iconName": "octicon-shield-check",
5
+ "categories": ["Continuous integration", "Testing"]
6
+ }
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.1] - 2026-10-01
4
+
5
+ ### Added
6
+ - Added a zero-configuration GitHub workflow starter that runs only when shell files change.
7
+ - Added a copyable compatibility report artifact to the starter workflow.
8
+ - Added citation metadata and clearer PyPI keywords for installer and bootstrap use cases.
9
+
10
+ ### Changed
11
+ - Marked the project Beta now that the default matrix, safety controls, reports, presets, and changed-script mode are released.
12
+ - Updated the roadmap to show the v0.5 and v0.6 controls as delivered.
13
+
14
+ ## [0.6.0] - 2026-09-30
15
+
16
+ ### Added
17
+ - Added `--changed-since` and the matching Action input to test only shell scripts changed since a Git revision.
18
+ - Changed-script selection uses the existing discovery safety checks and passes when a change contains no shell scripts.
19
+
3
20
  ## [0.5.1] - 2026-09-30
4
21
 
5
22
  ### Changed
@@ -0,0 +1,12 @@
1
+ cff-version: 1.2.0
2
+ title: OpsScript Gate
3
+ message: If this project helps your shell compatibility testing, please cite it.
4
+ type: software
5
+ authors:
6
+ - family-names: Mresyzz
7
+ given-names: Mresy
8
+ version: 0.6.0
9
+ date-released: 2026-09-30
10
+ repository-code: https://github.com/Mresyzz/opsscript-gate
11
+ url: https://github.com/Mresyzz/opsscript-gate
12
+ license: MIT
@@ -1,6 +1,35 @@
1
+ Metadata-Version: 2.5
2
+ Name: opsscript-gate
3
+ Version: 0.6.1
4
+ Summary: Run Linux shell scripts in Debian, Ubuntu and Alpine containers to catch runtime compatibility failures before merge
5
+ Project-URL: Homepage, https://github.com/Mresyzz/opsscript-gate
6
+ Project-URL: Repository, https://github.com/Mresyzz/opsscript-gate
7
+ Project-URL: Issues, https://github.com/Mresyzz/opsscript-gate/issues
8
+ Project-URL: Changelog, https://github.com/Mresyzz/opsscript-gate/blob/main/CHANGELOG.md
9
+ Project-URL: Documentation, https://github.com/Mresyzz/opsscript-gate/tree/main/docs
10
+ Author: Mresyzz
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: alpine,bootstrap,ci,compatibility,cross-distro,debian,devops,docker,github-actions,installer,linux,posix,shell,shellcheck-alternative,testing,ubuntu
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Classifier: Topic :: System :: Systems Administration
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: docker>=7.0.0
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=8.0.0; extra == 'test'
28
+ Description-Content-Type: text/markdown
29
+
1
30
  # OpsScript Gate — shell script compatibility testing
2
31
 
3
- [简体中文](README.zh-CN.md) · [Configuration](docs/configuration.md) · [Troubleshooting](docs/troubleshooting.md)
32
+ [简体中文](README.zh-CN.md) · [Configuration](docs/configuration.md) · [Troubleshooting](docs/troubleshooting.md) · [Self-hosted model installer guide](docs/gpt-oss.md)
4
33
 
5
34
  > ShellCheck analyzes shell syntax. OpsScript Gate executes scripts in Linux containers.
6
35
 
@@ -17,13 +46,18 @@
17
46
 
18
47
  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
48
 
49
+ If your repository contains an installer, bootstrap script, or shell entrypoint, the fastest path is to
50
+ copy the [zero-configuration workflow](.github/workflow-templates/opsscript-gate.yml). It only runs when
51
+ shell files change, uses offline containers by default, and uploads a report that can be attached to a PR.
52
+ No account, API key, or service signup is required.
53
+
20
54
  ---
21
55
 
22
- ## Current release: v0.5.1
56
+ ## Current release: v0.6.0
23
57
 
24
58
  The current release includes a project config file, a dry-run plan, presets, exclusions,
25
- and saved reports. These controls were introduced in v0.5.0; projects on v0.4.1 and
26
- earlier do not include them.
59
+ saved reports, and changed-script selection for pull requests. The project controls
60
+ were introduced in v0.5.0; projects on v0.4.1 and earlier do not include them.
27
61
 
28
62
  ```bash
29
63
  opsscript-gate init
@@ -47,6 +81,9 @@ See [configuration and migration](docs/configuration.md) and
47
81
 
48
82
  Add this workflow to `.github/workflows/shell-compat.yml`:
49
83
 
84
+ You can also select **OpsScript Gate shell compatibility** from GitHub's workflow templates, or copy the
85
+ maintained [starter workflow](.github/workflow-templates/opsscript-gate.yml).
86
+
50
87
  You can copy the maintained example from [`examples/github-actions/workflow.yml`](examples/github-actions/workflow.yml)
51
88
  and change the `script-path`, or use the minimal workflow below.
52
89
 
@@ -59,16 +96,18 @@ jobs:
59
96
  runs-on: ubuntu-latest
60
97
  steps:
61
98
  - uses: actions/checkout@v7
62
- - uses: Mresyzz/opsscript-gate@v0.5.1
99
+ - uses: Mresyzz/opsscript-gate@v0.6.0
63
100
  ```
64
101
 
65
- If `script-path` is omitted, the action discovers shell scripts in the repository.
66
- Use `opsscript-gate run --dry-run` to inspect the selection before running it.
102
+ This zero-configuration form discovers shell scripts in the repository. Use
103
+ `opsscript-gate run --dry-run` to inspect the selection, or set `script-path` when the
104
+ repository has a single installer. For self-hosted model installers, see the
105
+ [installer guide](docs/gpt-oss.md).
67
106
 
68
107
  Or test a specific script with custom execution modes:
69
108
 
70
109
  ```yaml
71
- - uses: Mresyzz/opsscript-gate@v0.5.1
110
+ - uses: Mresyzz/opsscript-gate@v0.6.0
72
111
  with:
73
112
  script-path: scripts/install.sh
74
113
  shell: auto
@@ -90,6 +129,25 @@ opsscript-gate run ./scripts/install.sh
90
129
  opsscript-gate run
91
130
  ```
92
131
 
132
+ ### Test only scripts changed in a pull request
133
+
134
+ Fetch the comparison revision and pass the pull request base SHA to avoid running
135
+ unrelated scripts:
136
+
137
+ ```yaml
138
+ - uses: actions/checkout@v7
139
+ with:
140
+ fetch-depth: 0
141
+ - uses: Mresyzz/opsscript-gate@v0.6.0
142
+ with:
143
+ changed-since: ${{ github.event.pull_request.base.sha }}
144
+ preset: minimal
145
+ ```
146
+
147
+ If the change does not include a shell script, the check passes without starting a
148
+ container. The same selection can be previewed locally with
149
+ `opsscript-gate run --changed-since origin/main --dry-run`.
150
+
93
151
  ### Example: package-manager mismatch
94
152
 
95
153
  Create `install.sh`:
@@ -312,6 +370,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
312
370
  | `--mem-limit` | String | `256m` | Memory limit per container (e.g. `256m`, `512m`) |
313
371
  | `--pids-limit`| Integer | `128` | Maximum number of processes per container |
314
372
  | `--network` | Choice | `bridge` | Container network mode: `bridge` or `none` |
373
+ | `--changed-since` | Git revision | - | Test shell scripts changed between this revision and `HEAD` |
315
374
  | `--version` | Flag | - | Show version number |
316
375
  | `-h, --help` | Flag | - | Show argument help |
317
376
 
@@ -1,34 +1,6 @@
1
- Metadata-Version: 2.5
2
- Name: opsscript-gate
3
- Version: 0.5.1
4
- Summary: Run Linux shell scripts in Debian, Ubuntu and Alpine containers to catch runtime compatibility failures before merge
5
- Project-URL: Homepage, https://github.com/Mresyzz/opsscript-gate
6
- Project-URL: Repository, https://github.com/Mresyzz/opsscript-gate
7
- Project-URL: Issues, https://github.com/Mresyzz/opsscript-gate/issues
8
- Project-URL: Changelog, https://github.com/Mresyzz/opsscript-gate/blob/main/CHANGELOG.md
9
- Author: Mresyzz
10
- License: MIT
11
- License-File: LICENSE
12
- Keywords: ci,compatibility,cross-distro,devops,docker,github-actions,linux,posix,shell,testing
13
- Classifier: Development Status :: 3 - Alpha
14
- Classifier: Intended Audience :: Developers
15
- Classifier: Intended Audience :: System Administrators
16
- Classifier: License :: OSI Approved :: MIT License
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Topic :: Software Development :: Testing
22
- Classifier: Topic :: System :: Systems Administration
23
- Requires-Python: >=3.10
24
- Requires-Dist: docker>=7.0.0
25
- Provides-Extra: test
26
- Requires-Dist: pytest>=8.0.0; extra == 'test'
27
- Description-Content-Type: text/markdown
28
-
29
1
  # OpsScript Gate — shell script compatibility testing
30
2
 
31
- [简体中文](README.zh-CN.md) · [Configuration](docs/configuration.md) · [Troubleshooting](docs/troubleshooting.md)
3
+ [简体中文](README.zh-CN.md) · [Configuration](docs/configuration.md) · [Troubleshooting](docs/troubleshooting.md) · [Self-hosted model installer guide](docs/gpt-oss.md)
32
4
 
33
5
  > ShellCheck analyzes shell syntax. OpsScript Gate executes scripts in Linux containers.
34
6
 
@@ -45,13 +17,18 @@ Description-Content-Type: text/markdown
45
17
 
46
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.
47
19
 
20
+ If your repository contains an installer, bootstrap script, or shell entrypoint, the fastest path is to
21
+ copy the [zero-configuration workflow](.github/workflow-templates/opsscript-gate.yml). It only runs when
22
+ shell files change, uses offline containers by default, and uploads a report that can be attached to a PR.
23
+ No account, API key, or service signup is required.
24
+
48
25
  ---
49
26
 
50
- ## Current release: v0.5.1
27
+ ## Current release: v0.6.0
51
28
 
52
29
  The current release includes a project config file, a dry-run plan, presets, exclusions,
53
- and saved reports. These controls were introduced in v0.5.0; projects on v0.4.1 and
54
- earlier do not include them.
30
+ saved reports, and changed-script selection for pull requests. The project controls
31
+ were introduced in v0.5.0; projects on v0.4.1 and earlier do not include them.
55
32
 
56
33
  ```bash
57
34
  opsscript-gate init
@@ -75,6 +52,9 @@ See [configuration and migration](docs/configuration.md) and
75
52
 
76
53
  Add this workflow to `.github/workflows/shell-compat.yml`:
77
54
 
55
+ You can also select **OpsScript Gate shell compatibility** from GitHub's workflow templates, or copy the
56
+ maintained [starter workflow](.github/workflow-templates/opsscript-gate.yml).
57
+
78
58
  You can copy the maintained example from [`examples/github-actions/workflow.yml`](examples/github-actions/workflow.yml)
79
59
  and change the `script-path`, or use the minimal workflow below.
80
60
 
@@ -87,16 +67,18 @@ jobs:
87
67
  runs-on: ubuntu-latest
88
68
  steps:
89
69
  - uses: actions/checkout@v7
90
- - uses: Mresyzz/opsscript-gate@v0.5.1
70
+ - uses: Mresyzz/opsscript-gate@v0.6.0
91
71
  ```
92
72
 
93
- If `script-path` is omitted, the action discovers shell scripts in the repository.
94
- Use `opsscript-gate run --dry-run` to inspect the selection before running it.
73
+ This zero-configuration form discovers shell scripts in the repository. Use
74
+ `opsscript-gate run --dry-run` to inspect the selection, or set `script-path` when the
75
+ repository has a single installer. For self-hosted model installers, see the
76
+ [installer guide](docs/gpt-oss.md).
95
77
 
96
78
  Or test a specific script with custom execution modes:
97
79
 
98
80
  ```yaml
99
- - uses: Mresyzz/opsscript-gate@v0.5.1
81
+ - uses: Mresyzz/opsscript-gate@v0.6.0
100
82
  with:
101
83
  script-path: scripts/install.sh
102
84
  shell: auto
@@ -118,6 +100,25 @@ opsscript-gate run ./scripts/install.sh
118
100
  opsscript-gate run
119
101
  ```
120
102
 
103
+ ### Test only scripts changed in a pull request
104
+
105
+ Fetch the comparison revision and pass the pull request base SHA to avoid running
106
+ unrelated scripts:
107
+
108
+ ```yaml
109
+ - uses: actions/checkout@v7
110
+ with:
111
+ fetch-depth: 0
112
+ - uses: Mresyzz/opsscript-gate@v0.6.0
113
+ with:
114
+ changed-since: ${{ github.event.pull_request.base.sha }}
115
+ preset: minimal
116
+ ```
117
+
118
+ If the change does not include a shell script, the check passes without starting a
119
+ container. The same selection can be previewed locally with
120
+ `opsscript-gate run --changed-since origin/main --dry-run`.
121
+
121
122
  ### Example: package-manager mismatch
122
123
 
123
124
  Create `install.sh`:
@@ -340,6 +341,7 @@ usage: opsscript-gate run [-h] [--matrix MATRIX] [-j JOBS] [--timeout TIMEOUT]
340
341
  | `--mem-limit` | String | `256m` | Memory limit per container (e.g. `256m`, `512m`) |
341
342
  | `--pids-limit`| Integer | `128` | Maximum number of processes per container |
342
343
  | `--network` | Choice | `bridge` | Container network mode: `bridge` or `none` |
344
+ | `--changed-since` | Git revision | - | Test shell scripts changed between this revision and `HEAD` |
343
345
  | `--version` | Flag | - | Show version number |
344
346
  | `-h, --help` | Flag | - | Show argument help |
345
347
 
@@ -1,6 +1,6 @@
1
1
  # OpsScript Gate:Shell 脚本跨发行版运行检查
2
2
 
3
- [English](README.md) · [配置说明](docs/configuration.md) · [常见问题](docs/troubleshooting.md)
3
+ [English](README.md) · [配置说明](docs/configuration.md) · [常见问题](docs/troubleshooting.md) · [自托管模型安装脚本指南](docs/gpt-oss.md)
4
4
 
5
5
  在 Debian、Ubuntu、Alpine 容器里实际运行 Shell 脚本,提前发现缺失命令、
6
6
  Bash 依赖、包管理器假设和交互式阻塞。它与 ShellCheck 互补。
@@ -8,9 +8,9 @@ Bash 依赖、包管理器假设和交互式阻塞。它与 ShellCheck 互补。
8
8
  适合独立安装脚本、容器入口脚本和发布脚本。每次只挂载目标脚本,
9
9
  不挂载整个仓库;依赖其他仓库文件的脚本需要现有项目测试配合。
10
10
 
11
- ## 当前版本 v0.5.1
11
+ ## 当前版本 v0.6.0
12
12
 
13
- v0.5.1 已作为正式版本发布;如果你要从源码验证当前分支,也可以执行:
13
+ v0.6.0 已作为正式版本发布;如果你要从源码验证当前分支,也可以执行:
14
14
 
15
15
  ```bash
16
16
  pip install -e .
@@ -25,7 +25,7 @@ opsscript-gate run --dry-run
25
25
  opsscript-gate run --format json --output reports/compatibility.json
26
26
  ```
27
27
 
28
- `init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.5.1`。
28
+ `init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.6.0`。
29
29
  预览不需要 Docker;真实运行需要 Python 3.10+
30
30
  和可访问的 Linux Docker 引擎。
31
31
 
@@ -45,6 +45,25 @@ opsscript-gate run --format json --output reports/compatibility.json
45
45
  返回码 `0` 表示全部通过,`1` 表示检查失败或发生错误。`--dry-run` 成功只表示
46
46
  执行计划生成成功。容器不是不可信代码的安全沙箱。
47
47
 
48
+ 在 Pull Request 中只检查本次修改的脚本,可以先获取完整 Git 历史,再传入目标分支提交:
49
+
50
+ ```yaml
51
+ - uses: actions/checkout@v7
52
+ with:
53
+ fetch-depth: 0
54
+ - uses: Mresyzz/opsscript-gate@v0.6.0
55
+ with:
56
+ changed-since: ${{ github.event.pull_request.base.sha }}
57
+ preset: minimal
58
+ ```
59
+
60
+ 如果本次修改没有 Shell 脚本,检查会直接通过,不启动容器。 本地可以用
61
+ `opsscript-gate run --changed-since origin/main --dry-run` 预览选择结果。
62
+
63
+ 如果仓库维护自托管模型的安装或启动脚本,可以参考
64
+ [自托管模型安装脚本指南](docs/gpt-oss.md)。它只验证 Shell 和发行版兼容性,
65
+ 不代替 GPU、模型权重或推理质量测试。
66
+
48
67
  ## 示例:为什么 Alpine 上会失败
49
68
 
50
69
  ```sh
@@ -25,7 +25,14 @@ This roadmap tracks features delivered in recent releases and areas under consid
25
25
 
26
26
  ---
27
27
 
28
- ## Planned for Future Releases (v0.5.0+)
28
+ ## Delivered in v0.5.0 and v0.6.0
29
+
30
+ - [x] Project configuration with strict validation and CLI/Action precedence.
31
+ - [x] Dry-run plans, `minimal` and `ubuntu` presets, exclusions, and saved reports.
32
+ - [x] Bounded discovery with symlink and oversized-file protection.
33
+ - [x] Changed-script selection with `--changed-since` for pull requests.
34
+
35
+ ## Planned for Future Releases
29
36
 
30
37
  ### 1. Matrix & Distribution Presets
31
38
  - [ ] **RPM-based distributions**: Evaluate support for Enterprise Linux baselines (e.g. Rocky Linux 9, AlmaLinux, Fedora).
@@ -67,6 +67,10 @@ inputs:
67
67
  description: 'Save a report file for upload-artifact or other tooling'
68
68
  required: false
69
69
  default: ''
70
+ changed-since:
71
+ description: 'Only test shell scripts changed between this Git revision and HEAD'
72
+ required: false
73
+ default: ''
70
74
 
71
75
  runs:
72
76
  using: 'composite'
@@ -99,6 +103,7 @@ runs:
99
103
  INPUT_MAX_SCRIPTS: ${{ inputs.max-scripts }}
100
104
  INPUT_DRY_RUN: ${{ inputs.dry-run }}
101
105
  INPUT_OUTPUT: ${{ inputs.output }}
106
+ INPUT_CHANGED_SINCE: ${{ inputs.changed-since }}
102
107
  run: |
103
108
  ARGS=("run")
104
109
  if [ -n "$INPUT_SCRIPT_PATH" ]; then
@@ -117,6 +122,7 @@ runs:
117
122
  [ -z "$INPUT_PRESET" ] || ARGS+=(--preset "$INPUT_PRESET")
118
123
  [ -z "$INPUT_MAX_SCRIPTS" ] || ARGS+=(--max-scripts "$INPUT_MAX_SCRIPTS")
119
124
  [ -z "$INPUT_OUTPUT" ] || ARGS+=(--output "$INPUT_OUTPUT")
125
+ [ -z "$INPUT_CHANGED_SINCE" ] || ARGS+=(--changed-since "$INPUT_CHANGED_SINCE")
120
126
  while IFS= read -r pattern; do
121
127
  pattern="${pattern%$'\r'}"
122
128
  [ -z "$pattern" ] || ARGS+=("--exclude" "$pattern")
@@ -11,7 +11,7 @@ opsscript-gate run --dry-run
11
11
  ```
12
12
 
13
13
  `init` refuses to overwrite either `.opsscript-gate.json` or
14
- `.github/workflows/opsscript-gate.yml`. The generated workflow uses the v0.5.1 tag.
14
+ `.github/workflows/opsscript-gate.yml`. The generated workflow uses the v0.6.0 tag.
15
15
 
16
16
  The generated config uses the two-image `minimal` preset, honors recognized shell
17
17
  shebangs, disables networking, and excludes `tests/*` and `examples/*`. Review these
@@ -89,8 +89,23 @@ test result. Docker startup failures may occur before a report can be produced.
89
89
  The Action accepts matching `config`, `preset`, `max-scripts`, `dry-run`, and `output`
90
90
  inputs; `exclude` is a multiline list. Omitted execution inputs preserve config values.
91
91
 
92
+ For pull requests, set `changed-since` to the base commit and fetch full Git history:
93
+
94
+ ```yaml
95
+ - uses: actions/checkout@v7
96
+ with:
97
+ fetch-depth: 0
98
+ - uses: Mresyzz/opsscript-gate@v0.6.0
99
+ with:
100
+ changed-since: ${{ github.event.pull_request.base.sha }}
101
+ preset: minimal
102
+ ```
103
+
104
+ The Action passes when no changed shell scripts are selected. This keeps unrelated
105
+ documentation or application changes from starting container jobs.
106
+
92
107
  ```yaml
93
- - uses: Mresyzz/opsscript-gate@v0.5.1
108
+ - uses: Mresyzz/opsscript-gate@v0.6.0
94
109
  with:
95
110
  config: .opsscript-gate.json
96
111
  format: json
@@ -0,0 +1,80 @@
1
+ # Test self-hosted model installer scripts
2
+
3
+ OpsScript Gate fits the installation and release layer of a self-hosted model project. It
4
+ executes shell scripts in Debian, Ubuntu, and Alpine containers to catch distribution
5
+ differences, missing commands, package-manager assumptions, and interactive hangs.
6
+
7
+ This guide uses [OpenAI's gpt-oss open-weight models](https://openai.com/index/introducing-gpt-oss/)
8
+ as a concrete example. The checks cover installer scripts, startup scripts, and container
9
+ entrypoints. They do **not** test GPU drivers, CUDA/ROCm, model weights, inference speed,
10
+ output quality, or the memory requirements of a particular machine. Keeping this boundary
11
+ explicit prevents a script-compatibility check from being mistaken for a complete model
12
+ deployment test.
13
+
14
+ ## Check changed scripts in pull requests
15
+
16
+ Assume the repository contains `scripts/bootstrap-gpt-oss.sh` or another shell installer:
17
+
18
+ ```yaml
19
+ name: Model installer compatibility
20
+
21
+ on:
22
+ pull_request:
23
+
24
+ permissions:
25
+ contents: read
26
+
27
+ jobs:
28
+ shell-runtime:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v7
32
+ with:
33
+ fetch-depth: 0
34
+ persist-credentials: false
35
+
36
+ - uses: Mresyzz/opsscript-gate@v0.6.0
37
+ with:
38
+ changed-since: ${{ github.event.pull_request.base.sha }}
39
+ preset: minimal
40
+ shell: auto
41
+ network: none
42
+ ```
43
+
44
+ `changed-since` selects only shell files changed in the commit range. Documentation,
45
+ Python, model configuration, and frontend changes do not start containers. If no shell
46
+ file changed, the check passes without doing container work.
47
+
48
+ If the installer must download packages or model files, change `network` to `bridge` and
49
+ keep downloads in a dedicated test script. Once network access is enabled, upstream
50
+ mirrors and package indexes can affect repeatability.
51
+
52
+ ## Preview and run locally
53
+
54
+ Confirm the selection before starting Docker:
55
+
56
+ ```bash
57
+ pip install opsscript-gate
58
+ opsscript-gate run --changed-since origin/main --preset minimal --shell auto --dry-run
59
+ opsscript-gate run scripts/bootstrap-gpt-oss.sh --preset minimal --shell auto --network none
60
+ ```
61
+
62
+ If an installer depends on other repository files, an explicit `script-path` still checks
63
+ only that script; dependencies are not mounted into the container. Put the required logic
64
+ behind a standalone test entrypoint, or use the project's integration tests for file-level
65
+ collaboration and use OpsScript Gate for the cross-distribution runtime layer.
66
+
67
+ ## What to inspect when a check fails
68
+
69
+ - `apt-get` is missing on Alpine: choose `apk` for that distribution, or detect it and
70
+ return a clear message.
71
+ - `bash` is missing: declare `#!/usr/bin/env bash` for Bash syntax, use `shell: auto`, and
72
+ confirm that the selected test image includes Bash.
73
+ - The script waits for input: remove interactive prompts or return a clear non-interactive
74
+ error.
75
+ - A download fails: separate script failures from network or package-index instability;
76
+ use `network: bridge` only when needed and pin versions with checksums.
77
+
78
+ Model loading, GPU drivers, and inference quality belong in the project's hardware
79
+ integration tests. OpsScript Gate makes the installer and startup scripts pass a small,
80
+ repeatable shell-runtime check before those expensive tests run.
@@ -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.1 Action release.
5
+ Copy `workflow.yml` into your repository at `.github/workflows/script-gate.yml` and point `script-path` to the script you want to validate. The example uses the current v0.6.0 Action release.
@@ -19,7 +19,7 @@ jobs:
19
19
  persist-credentials: false
20
20
 
21
21
  - name: Run OpsScript Gate
22
- uses: Mresyzz/opsscript-gate@v0.5.1
22
+ uses: Mresyzz/opsscript-gate@v0.6.0
23
23
  with:
24
24
  script-path: 'scripts/deploy.sh'
25
25
  shell: auto
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "opsscript-gate"
7
- version = "0.5.1"
7
+ version = "0.6.1"
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" }
@@ -23,9 +23,15 @@ keywords = [
23
23
  "cross-distro",
24
24
  "posix",
25
25
  "testing",
26
+ "installer",
27
+ "bootstrap",
28
+ "debian",
29
+ "ubuntu",
30
+ "alpine",
31
+ "shellcheck-alternative",
26
32
  ]
27
33
  classifiers = [
28
- "Development Status :: 3 - Alpha",
34
+ "Development Status :: 4 - Beta",
29
35
  "Intended Audience :: Developers",
30
36
  "Intended Audience :: System Administrators",
31
37
  "License :: OSI Approved :: MIT License",
@@ -45,6 +51,7 @@ Homepage = "https://github.com/Mresyzz/opsscript-gate"
45
51
  Repository = "https://github.com/Mresyzz/opsscript-gate"
46
52
  Issues = "https://github.com/Mresyzz/opsscript-gate/issues"
47
53
  Changelog = "https://github.com/Mresyzz/opsscript-gate/blob/main/CHANGELOG.md"
54
+ Documentation = "https://github.com/Mresyzz/opsscript-gate/tree/main/docs"
48
55
 
49
56
  [project.optional-dependencies]
50
57
  test = [
@@ -1,4 +1,4 @@
1
1
  """OpsScript Gate: Lightweight cross-distro compatibility pre-check for Linux ops scripts."""
2
2
 
3
- __version__ = "0.5.1"
3
+ __version__ = "0.6.1"
4
4
  __all__ = ["__version__"]
@@ -8,7 +8,7 @@ import time
8
8
  from pathlib import Path
9
9
 
10
10
  from opsscript_gate import __version__
11
- from opsscript_gate.discovery import discover_scripts
11
+ from opsscript_gate.discovery import discover_changed_scripts, discover_scripts
12
12
  from opsscript_gate.config import DEFAULTS, PRESETS, init_project, load_config
13
13
  from opsscript_gate.models import MultiScriptReport, RunReport
14
14
  from opsscript_gate.reporter import (
@@ -108,6 +108,10 @@ def build_parser() -> argparse.ArgumentParser:
108
108
  default="bridge",
109
109
  help="Network mode for containers: 'bridge' or 'none' (default: bridge)",
110
110
  )
111
+ run_parser.add_argument(
112
+ "--changed-since",
113
+ help="Only test shell scripts changed between this Git revision and HEAD",
114
+ )
111
115
 
112
116
  run_parser.add_argument("--config", help="Project JSON configuration (default: .opsscript-gate.json)")
113
117
  run_parser.add_argument("--preset", choices=list(PRESETS), help="Named distribution matrix")
@@ -172,18 +176,30 @@ def main(argv: list[str] | None = None) -> int:
172
176
  sys.stderr.write(f"Error: {err}\n")
173
177
  return 1
174
178
  # Resolve target script(s)
179
+ if args.script_path and args.changed_since:
180
+ sys.stderr.write("Error: use either a script path or --changed-since, not both\n")
181
+ return 1
182
+ changed_selection = bool(args.changed_since)
175
183
  if args.script_path:
176
184
  if not os.path.isfile(args.script_path):
177
185
  sys.stderr.write(f"Error: Script file not found: {args.script_path}\n")
178
186
  return 1
179
187
  target_scripts = [args.script_path]
188
+ elif changed_selection:
189
+ try:
190
+ target_scripts = discover_changed_scripts(
191
+ ".", since=args.changed_since, max_scripts=args.max_scripts, exclude=args.exclude
192
+ )
193
+ except ValueError as err:
194
+ sys.stderr.write(f"Error: {err}\n")
195
+ return 1
180
196
  else:
181
197
  try:
182
198
  target_scripts = discover_scripts(".", max_scripts=args.max_scripts, exclude=args.exclude)
183
199
  except ValueError as err:
184
200
  sys.stderr.write(f"Error: {err}\n")
185
201
  return 1
186
- if not target_scripts:
202
+ if not target_scripts and not changed_selection:
187
203
  sys.stderr.write(
188
204
  "No shell scripts discovered in the current directory. "
189
205
  "Please specify a script path explicitly.\n"
@@ -203,6 +219,8 @@ def main(argv: list[str] | None = None) -> int:
203
219
  plan = {"dry_run": True, "scripts": target_scripts, "matrix": matrix,
204
220
  "executions": len(target_scripts) * len(matrix), "shell": args.shell,
205
221
  "network": args.network, "timeout": args.timeout}
222
+ if changed_selection:
223
+ plan["changed_since"] = args.changed_since
206
224
  output = json.dumps(plan, indent=2) if args.format == "json" else (
207
225
  "Execution preview (no scripts executed)\n"
208
226
  + "\n".join(f" {path}" for path in target_scripts)
@@ -211,6 +229,12 @@ def main(argv: list[str] | None = None) -> int:
211
229
  )
212
230
  return 0 if publish_output(output, args.output) else 1
213
231
 
232
+ if not target_scripts:
233
+ output = (
234
+ f"No changed shell scripts since {args.changed_since}; skipping compatibility checks."
235
+ )
236
+ return 0 if publish_output(output, args.output) else 1
237
+
214
238
  if len(target_scripts) == 1:
215
239
  script_path = target_scripts[0]
216
240
  try:
@@ -63,7 +63,7 @@ jobs:
63
63
  - uses: actions/checkout@v7
64
64
  with:
65
65
  persist-credentials: false
66
- - uses: Mresyzz/opsscript-gate@v0.5.1
66
+ - uses: Mresyzz/opsscript-gate@v0.6.0
67
67
  with:
68
68
  config: .opsscript-gate.json
69
69
  """,
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import fnmatch
5
+ import subprocess
5
6
  from pathlib import Path
6
7
 
7
8
  # Directories to ignore during automatic script discovery
@@ -117,3 +118,66 @@ def discover_scripts(
117
118
  # Sort alphabetically for stable, deterministic ordering
118
119
  discovered.sort()
119
120
  return discovered
121
+
122
+
123
+ def discover_changed_scripts(
124
+ root_dir: str = ".",
125
+ since: str = "HEAD^",
126
+ max_scripts: int = MAX_DISCOVERED_SCRIPTS,
127
+ max_size_bytes: int = MAX_SCRIPT_SIZE_BYTES,
128
+ exclude: list[str] | None = None,
129
+ ) -> list[str]:
130
+ """Discover shell scripts changed between ``since`` and ``HEAD``.
131
+
132
+ Git provides the candidate paths; the same file, size, shebang, exclusion,
133
+ and overflow checks used by normal discovery still apply. A missing Git
134
+ repository or unavailable revision is reported as a user-facing error.
135
+ """
136
+ root_path = Path(root_dir).resolve()
137
+ if not since or not since.strip() or since.startswith("-") or any(char.isspace() for char in since):
138
+ raise ValueError("changed-since must name a Git revision")
139
+
140
+ try:
141
+ result = subprocess.run(
142
+ ["git", "diff", "--name-only", "--diff-filter=ACMR", "-z",
143
+ f"{since}...HEAD", "--"],
144
+ cwd=root_path,
145
+ check=True,
146
+ stdout=subprocess.PIPE,
147
+ stderr=subprocess.PIPE,
148
+ )
149
+ except FileNotFoundError as exc:
150
+ raise ValueError("changed-since requires Git to be installed") from exc
151
+ except subprocess.CalledProcessError as exc:
152
+ detail = exc.stderr.decode("utf-8", errors="replace").strip()
153
+ raise ValueError(
154
+ f"Cannot compare Git revision {since!r} with HEAD"
155
+ + (f": {detail}" if detail else "")
156
+ ) from exc
157
+
158
+ changed_paths = [
159
+ item.decode("utf-8", errors="surrogateescape")
160
+ for item in result.stdout.split(b"\0")
161
+ if item
162
+ ]
163
+ discovered: list[str] = []
164
+ for relative_name in changed_paths:
165
+ candidate = root_path / Path(relative_name)
166
+ try:
167
+ resolved = candidate.resolve()
168
+ rel_path = resolved.relative_to(root_path).as_posix()
169
+ except (OSError, ValueError):
170
+ continue
171
+ if any(fnmatch.fnmatchcase(rel_path, pattern) for pattern in (exclude or [])):
172
+ continue
173
+ if not is_shell_script(candidate, max_size_bytes=max_size_bytes):
174
+ continue
175
+ normalized = rel_path if "/" in rel_path else f"./{rel_path}"
176
+ discovered.append(normalized)
177
+ if len(discovered) > max_scripts:
178
+ raise ValueError(
179
+ f"Changed files include more than {max_scripts} shell scripts, exceeding limit. "
180
+ "Use --exclude or increase --max-scripts; no scripts were executed."
181
+ )
182
+
183
+ return sorted(set(discovered))
@@ -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"):
10
+ for name in ("config", "preset", "exclude", "max-scripts", "dry-run", "output", "changed-since"):
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
14
  "--network", "--matrix", "--jobs", "--config", "--preset",
15
15
  "--max-scripts", "--output", "--exclude"):
16
16
  assert flag in run_text
17
+ assert "--changed-since" in run_text
17
18
 
18
19
 
19
20
  def test_test_workflow_has_valid_yaml_and_python_indent():
@@ -6,7 +6,7 @@ import pytest
6
6
 
7
7
  from opsscript_gate.cli import main
8
8
  from opsscript_gate.config import CONFIG_NAME, init_project, load_config
9
- from opsscript_gate.discovery import discover_scripts, is_shell_script
9
+ from opsscript_gate.discovery import discover_changed_scripts, discover_scripts, is_shell_script
10
10
  from opsscript_gate.models import RunReport, SingleResult, DistroStatus
11
11
 
12
12
 
@@ -96,6 +96,40 @@ def test_discovery_stops_at_first_overflow(tmp_path, monkeypatch):
96
96
  assert len(checked) == 3
97
97
 
98
98
 
99
+ def test_changed_discovery_uses_git_paths_and_filters_candidates(tmp_path):
100
+ (tmp_path / "install.sh").write_text("#!/bin/sh\necho ok\n")
101
+ (tmp_path / "README.md").write_text("docs\n")
102
+ completed = type("Completed", (), {"stdout": b"install.sh\0README.md\0", "stderr": b""})()
103
+ with patch("opsscript_gate.discovery.subprocess.run", return_value=completed) as run:
104
+ result = discover_changed_scripts(str(tmp_path), since="origin/main")
105
+ assert result == ["./install.sh"]
106
+ run.assert_called_once()
107
+ assert "origin/main...HEAD" in run.call_args.args[0]
108
+
109
+
110
+ @pytest.mark.parametrize("revision", ["", "--output=bad", "origin/main bad"])
111
+ def test_changed_discovery_rejects_invalid_revision(tmp_path, revision):
112
+ with pytest.raises(ValueError, match="changed-since"):
113
+ discover_changed_scripts(str(tmp_path), since=revision)
114
+
115
+
116
+ def test_changed_discovery_reports_empty_selection_as_success(project, monkeypatch, capsys):
117
+ monkeypatch.setattr("opsscript_gate.cli.discover_changed_scripts", lambda *args, **kwargs: [])
118
+ assert main(["run", "--changed-since", "origin/main"]) == 0
119
+ assert "No changed shell scripts" in capsys.readouterr().out
120
+
121
+
122
+ def test_changed_discovery_preview_includes_revision(project, monkeypatch, capsys):
123
+ monkeypatch.setattr(
124
+ "opsscript_gate.cli.discover_changed_scripts",
125
+ lambda *args, **kwargs: ["./install.sh"],
126
+ )
127
+ assert main(["run", "--changed-since", "origin/main", "--dry-run", "--format", "json"]) == 0
128
+ plan = json.loads(capsys.readouterr().out)
129
+ assert plan["changed_since"] == "origin/main"
130
+ assert plan["scripts"] == ["./install.sh"]
131
+
132
+
99
133
  @pytest.mark.parametrize("flags", [["--jobs", "0"], ["--timeout", "-1"],
100
134
  ["--pids-limit", "0"], ["--network", "host"], ["--matrix", ","],
101
135
  ["--matrix", "alpine", "--preset", "minimal"], ["--config", "missing.json"]])
File without changes