opsscript-gate 0.9.4__tar.gz → 0.9.5__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 (65) hide show
  1. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/workflow-templates/opsscript-gate.yml +1 -1
  2. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/workflows/deploy-pages.yml +2 -0
  3. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/CHANGELOG.md +9 -0
  4. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/PKG-INFO +12 -7
  5. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/README.md +10 -6
  6. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/README.zh-CN.md +4 -4
  7. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/action.yml +17 -1
  8. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/docs/command-not-found.html +2 -1
  9. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5/docs}/index.html +49 -164
  10. opsscript_gate-0.9.5/docs/installer-smoke-test.html +73 -0
  11. opsscript_gate-0.9.5/docs/installer-smoke-test.md +55 -0
  12. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5/docs}/llms.txt +1 -0
  13. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/docs/sitemap.xml +1 -0
  14. opsscript_gate-0.9.5/examples/github-actions/README.md +11 -0
  15. opsscript_gate-0.9.5/examples/github-actions/installer-smoke.yml +54 -0
  16. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/examples/github-actions/workflow.yml +1 -1
  17. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/pyproject.toml +2 -1
  18. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/src/opsscript_gate/__init__.py +1 -1
  19. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/src/opsscript_gate/config.py +1 -1
  20. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/src/opsscript_gate/models.py +24 -0
  21. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/src/opsscript_gate/remediation.py +45 -24
  22. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/src/opsscript_gate/reporter.py +20 -4
  23. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/src/opsscript_gate/runner.py +180 -58
  24. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/tests/test_action_contract.py +3 -0
  25. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/tests/test_audit_coverage.py +146 -0
  26. opsscript_gate-0.9.5/tests/test_installer_example.py +76 -0
  27. opsscript_gate-0.9.4/examples/github-actions/README.md +0 -5
  28. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.gitattributes +0 -0
  29. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  30. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/ISSUE_TEMPLATE/distro_request.yml +0 -0
  31. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  32. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/assets/social-preview.png +0 -0
  33. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/pull_request_template.md +0 -0
  34. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/workflow-templates/opsscript-gate.properties.json +0 -0
  35. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/workflows/demo.yml +0 -0
  36. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/workflows/release.yml +0 -0
  37. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.github/workflows/test.yml +0 -0
  38. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/.gitignore +0 -0
  39. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/CITATION.cff +0 -0
  40. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/CONTRIBUTING.md +0 -0
  41. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/LICENSE +0 -0
  42. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/ROADMAP.md +0 -0
  43. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/SECURITY.md +0 -0
  44. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/demo.html +0 -0
  45. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/docs/command-not-found.md +0 -0
  46. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/docs/configuration.md +0 -0
  47. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/docs/gpt-oss.md +0 -0
  48. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/docs/robots.txt +0 -0
  49. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/docs/troubleshooting.md +0 -0
  50. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/examples/alpine-incompatibility/README.md +0 -0
  51. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/examples/alpine-incompatibility/apt_dependency.sh +0 -0
  52. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/examples/basic/README.md +0 -0
  53. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/examples/basic/clean_setup.sh +0 -0
  54. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/examples/interactive-hang/README.md +0 -0
  55. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/examples/interactive-hang/prompt_confirm.sh +0 -0
  56. {opsscript_gate-0.9.4/docs → opsscript_gate-0.9.5}/index.html +0 -0
  57. {opsscript_gate-0.9.4/docs → opsscript_gate-0.9.5}/llms.txt +0 -0
  58. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/src/opsscript_gate/cli.py +0 -0
  59. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/src/opsscript_gate/discovery.py +0 -0
  60. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/tests/conftest.py +0 -0
  61. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/tests/fixtures/fail_deps.sh +0 -0
  62. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/tests/fixtures/fail_hang.sh +0 -0
  63. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/tests/fixtures/pass_basic.sh +0 -0
  64. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/tests/test_onboarding.py +0 -0
  65. {opsscript_gate-0.9.4 → opsscript_gate-0.9.5}/tests/test_runner.py +0 -0
@@ -25,7 +25,7 @@ jobs:
25
25
  persist-credentials: false
26
26
 
27
27
  - name: Run OpsScript Gate
28
- uses: Mresyzz/opsscript-gate@v0.9.2
28
+ uses: Mresyzz/opsscript-gate@v0.9.4
29
29
  with:
30
30
  shell: auto
31
31
  network: none
@@ -25,6 +25,8 @@ jobs:
25
25
  uses: actions/checkout@v4
26
26
  - name: Setup Pages
27
27
  uses: actions/configure-pages@v5
28
+ - name: Stage downloadable installer workflow
29
+ run: cp examples/github-actions/installer-smoke.yml docs/installer-smoke.yml
28
30
  - name: Upload artifact
29
31
  uses: actions/upload-pages-artifact@v3
30
32
  with:
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.5] - 2026-11-08
4
+
5
+ ### Added
6
+ - Extended structured failure diagnostics to handle exit code 126 (`permission_denied` and `exec_format_error`) and exit code 137 (`oom_killed`) with targeted remediation suggestions.
7
+ - Added native distribution family detection for Amazon Linux (`amazonlinux`, `amzn`) and Oracle Linux (`oracle`, `ol:`) to support RHEL-family package recommendations.
8
+ - Added GitHub Composite Action `outputs` for `all-passed` and `report-file`, exporting execution status via `$GITHUB_OUTPUT`.
9
+ - Surfaced package installation recommendations and package names directly in Markdown compatibility tables, failure diagnostic log drawers, and terminal detail outputs.
10
+ - Added convenient counting properties (`passed_count`, `failed_count`, `total_count`) on `RunReport` and `MultiScriptReport`.
11
+
3
12
  ## [0.9.4] - 2026-10-11
4
13
 
5
14
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opsscript-gate
3
- Version: 0.9.4
3
+ Version: 0.9.5
4
4
  Summary: Cross-distro shell script compatibility and portability checker for Debian, Ubuntu, and Alpine. Runs real containers to diagnose command not found.
5
5
  Project-URL: Homepage, https://mresyzz.github.io/opsscript-gate/
6
6
  Project-URL: Demo, https://mresyzz.github.io/opsscript-gate/
@@ -28,6 +28,7 @@ Requires-Dist: docker>=7.0.0
28
28
  Provides-Extra: test
29
29
  Requires-Dist: markdown-it-py>=3.0.0; extra == 'test'
30
30
  Requires-Dist: pytest>=8.0.0; extra == 'test'
31
+ Requires-Dist: pyyaml>=6.0.0; extra == 'test'
31
32
  Description-Content-Type: text/markdown
32
33
 
33
34
  # OpsScript Gate — cross-distro shell script runtime compatibility checker
@@ -80,7 +81,7 @@ apt-get --version
80
81
  Run it locally with `opsscript-gate run ./install.sh`, or add the following step to CI:
81
82
 
82
83
  ```yaml
83
- - uses: Mresyzz/opsscript-gate@v0.9.2
84
+ - uses: Mresyzz/opsscript-gate@v0.9.4
84
85
  with:
85
86
  script-path: install.sh
86
87
  ```
@@ -89,7 +90,7 @@ The result identifies the failing distribution, exit code, line, missing command
89
90
 
90
91
  ---
91
92
 
92
- ## Current version: v0.9.2 (local candidate)
93
+ ## Current release: v0.9.4
93
94
 
94
95
  The current release includes a project config file, a dry-run plan, presets, exclusions,
95
96
  saved reports, changed-script selection for pull requests, and repeat runs for detecting
@@ -119,6 +120,10 @@ image's native package manager before testing. Package setup is opt-in and
119
120
  requires bridge networking. See [configuration and migration](docs/configuration.md) and
120
121
  [why a shell script works on Ubuntu but fails on Alpine](docs/troubleshooting.md).
121
122
 
123
+ Testing an installer? Use the [installation smoke test workflow](docs/installer-smoke-test.md)
124
+ to check the installed binary inside each container. A script's zero exit status
125
+ alone does not prove it installed successfully.
126
+
122
127
  ## Quickstart
123
128
 
124
129
  ### In GitHub Actions
@@ -143,7 +148,7 @@ jobs:
143
148
  - uses: actions/checkout@v7
144
149
  with:
145
150
  persist-credentials: false
146
- - uses: Mresyzz/opsscript-gate@v0.9.2
151
+ - uses: Mresyzz/opsscript-gate@v0.9.4
147
152
  ```
148
153
 
149
154
  This zero-configuration form discovers shell scripts in the repository. Use
@@ -154,7 +159,7 @@ repository has a single installer. For self-hosted model installers, see the
154
159
  Or test a specific script with custom execution modes:
155
160
 
156
161
  ```yaml
157
- - uses: Mresyzz/opsscript-gate@v0.9.2
162
+ - uses: Mresyzz/opsscript-gate@v0.9.4
158
163
  with:
159
164
  script-path: scripts/install.sh
160
165
  shell: auto
@@ -188,7 +193,7 @@ unrelated scripts:
188
193
  with:
189
194
  persist-credentials: false
190
195
  fetch-depth: 0
191
- - uses: Mresyzz/opsscript-gate@v0.9.2
196
+ - uses: Mresyzz/opsscript-gate@v0.9.4
192
197
  with:
193
198
  changed-since: ${{ github.event.pull_request.base.sha }}
194
199
  preset: minimal
@@ -279,7 +284,7 @@ Use `sarif` when the result should appear in GitHub Code Scanning or another
279
284
  SARIF-compatible viewer. Upload it explicitly with the official upload action:
280
285
 
281
286
  ```yaml
282
- - uses: Mresyzz/opsscript-gate@v0.9.2
287
+ - uses: Mresyzz/opsscript-gate@v0.9.4
283
288
  with:
284
289
  script-path: scripts/install.sh
285
290
  format: sarif
@@ -48,7 +48,7 @@ apt-get --version
48
48
  Run it locally with `opsscript-gate run ./install.sh`, or add the following step to CI:
49
49
 
50
50
  ```yaml
51
- - uses: Mresyzz/opsscript-gate@v0.9.2
51
+ - uses: Mresyzz/opsscript-gate@v0.9.4
52
52
  with:
53
53
  script-path: install.sh
54
54
  ```
@@ -57,7 +57,7 @@ The result identifies the failing distribution, exit code, line, missing command
57
57
 
58
58
  ---
59
59
 
60
- ## Current version: v0.9.2 (local candidate)
60
+ ## Current release: v0.9.4
61
61
 
62
62
  The current release includes a project config file, a dry-run plan, presets, exclusions,
63
63
  saved reports, changed-script selection for pull requests, and repeat runs for detecting
@@ -87,6 +87,10 @@ image's native package manager before testing. Package setup is opt-in and
87
87
  requires bridge networking. See [configuration and migration](docs/configuration.md) and
88
88
  [why a shell script works on Ubuntu but fails on Alpine](docs/troubleshooting.md).
89
89
 
90
+ Testing an installer? Use the [installation smoke test workflow](docs/installer-smoke-test.md)
91
+ to check the installed binary inside each container. A script's zero exit status
92
+ alone does not prove it installed successfully.
93
+
90
94
  ## Quickstart
91
95
 
92
96
  ### In GitHub Actions
@@ -111,7 +115,7 @@ jobs:
111
115
  - uses: actions/checkout@v7
112
116
  with:
113
117
  persist-credentials: false
114
- - uses: Mresyzz/opsscript-gate@v0.9.2
118
+ - uses: Mresyzz/opsscript-gate@v0.9.4
115
119
  ```
116
120
 
117
121
  This zero-configuration form discovers shell scripts in the repository. Use
@@ -122,7 +126,7 @@ repository has a single installer. For self-hosted model installers, see the
122
126
  Or test a specific script with custom execution modes:
123
127
 
124
128
  ```yaml
125
- - uses: Mresyzz/opsscript-gate@v0.9.2
129
+ - uses: Mresyzz/opsscript-gate@v0.9.4
126
130
  with:
127
131
  script-path: scripts/install.sh
128
132
  shell: auto
@@ -156,7 +160,7 @@ unrelated scripts:
156
160
  with:
157
161
  persist-credentials: false
158
162
  fetch-depth: 0
159
- - uses: Mresyzz/opsscript-gate@v0.9.2
163
+ - uses: Mresyzz/opsscript-gate@v0.9.4
160
164
  with:
161
165
  changed-since: ${{ github.event.pull_request.base.sha }}
162
166
  preset: minimal
@@ -247,7 +251,7 @@ Use `sarif` when the result should appear in GitHub Code Scanning or another
247
251
  SARIF-compatible viewer. Upload it explicitly with the official upload action:
248
252
 
249
253
  ```yaml
250
- - uses: Mresyzz/opsscript-gate@v0.9.2
254
+ - uses: Mresyzz/opsscript-gate@v0.9.4
251
255
  with:
252
256
  script-path: scripts/install.sh
253
257
  format: sarif
@@ -17,9 +17,9 @@ Bash 依赖、包管理器假设和交互式阻塞。它与 ShellCheck 互补。
17
17
 
18
18
  也可以直接打开 [OpsScript Gate 在线演示](https://mresyzz.github.io/opsscript-gate/),先查看 ShellCheck 与运行时验证的差异,再复制 workflow 到自己的仓库。
19
19
 
20
- ## 当前版本 v0.9.2
20
+ ## 当前版本 v0.9.4
21
21
 
22
- v0.9.2 是当前本地候选版本;如果你要从源码验证当前分支,也可以执行:
22
+ v0.9.4 是当前已发布版本;如果你要从源码验证当前分支,也可以执行:
23
23
 
24
24
  ```bash
25
25
  pip install -e .
@@ -35,7 +35,7 @@ opsscript-gate run --dry-run
35
35
  opsscript-gate run --format json --output reports/compatibility.json
36
36
  ```
37
37
 
38
- `init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.9.2`。
38
+ `init` 生成配置和 GitHub 工作流,不覆盖已有文件。工作流引用 `v0.9.4`。
39
39
  预览不需要 Docker;真实运行需要 Python 3.10+
40
40
  和可访问的 Linux Docker 引擎。
41
41
  `opsscript-gate doctor` 可以在真实运行前检查 Python 和 Docker。
@@ -74,7 +74,7 @@ Docker daemon 和可选的项目配置。
74
74
  - uses: actions/checkout@v7
75
75
  with:
76
76
  fetch-depth: 0
77
- - uses: Mresyzz/opsscript-gate@v0.9.2
77
+ - uses: Mresyzz/opsscript-gate@v0.9.4
78
78
  with:
79
79
  changed-since: ${{ github.event.pull_request.base.sha }}
80
80
  preset: minimal
@@ -80,6 +80,14 @@ inputs:
80
80
  required: false
81
81
  default: ''
82
82
 
83
+ outputs:
84
+ all-passed:
85
+ description: 'Whether all script compatibility checks passed (true or false)'
86
+ value: ${{ steps.gate.outputs.all-passed }}
87
+ report-file:
88
+ description: 'Path to saved report file if output input was set'
89
+ value: ${{ inputs.output }}
90
+
83
91
  runs:
84
92
  using: 'composite'
85
93
  steps:
@@ -94,6 +102,7 @@ runs:
94
102
  pip install "${{ github.action_path }}"
95
103
 
96
104
  - name: Run compatibility gate
105
+ id: gate
97
106
  shell: bash
98
107
  env:
99
108
  INPUT_SCRIPT_PATH: ${{ inputs.script-path }}
@@ -146,4 +155,11 @@ runs:
146
155
  echo "dry-run must be true or false" >&2
147
156
  exit 1
148
157
  fi
149
- python -m opsscript_gate.cli "${ARGS[@]}"
158
+ GATE_STATUS=0
159
+ python -m opsscript_gate.cli "${ARGS[@]}" || GATE_STATUS=$?
160
+ if [ "$GATE_STATUS" -eq 0 ]; then
161
+ echo "all-passed=true" >> "$GITHUB_OUTPUT"
162
+ else
163
+ echo "all-passed=false" >> "$GITHUB_OUTPUT"
164
+ fi
165
+ exit "$GATE_STATUS"
@@ -134,7 +134,7 @@ jobs:
134
134
  - uses: actions/checkout@v7
135
135
  with:
136
136
  persist-credentials: false
137
- - uses: Mresyzz/opsscript-gate@v0.9.2
137
+ - uses: Mresyzz/opsscript-gate@v0.9.4
138
138
  with:
139
139
  script-path: install.sh
140
140
  shell: auto
@@ -186,6 +186,7 @@ opsscript-gate run ./install.sh</code></pre>
186
186
  <a href="https://github.com/Mresyzz/opsscript-gate/blob/main/docs/troubleshooting.md">troubleshooting</a>,
187
187
  or <a href="https://github.com/Mresyzz/opsscript-gate/blob/main/SECURITY.md">security boundaries</a>.
188
188
  <a href="./">Open the interactive demo</a>.</footer>
189
+ <p>Installer returns zero but the program is missing? <a href="installer-smoke-test.html">Verify the installed binary in the same container.</a></p>
189
190
  </main>
190
191
  </body>
191
192
  </html>
@@ -253,7 +253,7 @@
253
253
  OpsScript Gate
254
254
  </span>
255
255
  <span class="font-mono text-[11px] px-1.5 py-0.5 rounded bg-[var(--surface-muted)] text-[var(--text-muted)] border border-[var(--border-light)]">
256
- v0.9.2
256
+ v0.9.4
257
257
  </span>
258
258
  </div>
259
259
  </div>
@@ -296,7 +296,7 @@
296
296
 
297
297
  <div class="pt-2 flex flex-wrap items-center gap-3 text-xs font-mono">
298
298
  <button onclick="copyCanonicalWorkflow(this)" class="px-4 py-2 rounded font-medium bg-[var(--brand)] text-white hover:bg-[var(--brand-hover)] shadow-sm transition-all">
299
- Copy 5-Line Action
299
+ Copy Workflow
300
300
  </button>
301
301
  <a href="./command-not-found.html" class="px-4 py-2 rounded font-medium bg-[var(--surface-elevated)] text-[var(--text-body)] border border-[var(--border-medium)] hover:border-[var(--brand)] hover:text-[var(--brand)] shadow-sm transition-all inline-flex items-center gap-1.5">
302
302
  Debug command not found
@@ -316,13 +316,13 @@
316
316
  <div class="flex flex-col sm:flex-row sm:items-baseline justify-between gap-1 border-b border-[var(--border-light)] pb-3">
317
317
  <div class="space-y-1">
318
318
  <div class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-[10px] font-mono uppercase tracking-wider bg-[var(--surface-muted)] text-[var(--text-muted)] border border-[var(--border-light)]">
319
- Section 01 &bull; Runtime Simulator
319
+ Section 01 &bull; Example walkthrough
320
320
  </div>
321
321
  <h2 class="font-serif text-xl text-[var(--text-hero)] font-medium">
322
- Live Discrepancy Verification
322
+ Explore compatibility examples
323
323
  </h2>
324
324
  <p class="text-xs text-[var(--text-muted)]">
325
- Contrast clean syntactic passes with isolated container failures in real time.
325
+ Illustrative results only. This page does not run ShellCheck or execute scripts. Use the Action to verify your script in real containers.
326
326
  </p>
327
327
  </div>
328
328
  </div>
@@ -359,7 +359,7 @@
359
359
  <!-- whitespace-pre prevents ugly breaking of code indentation -->
360
360
  <textarea id="script-input" oninput="evaluateInput()" class="w-full bg-[var(--code-bg)] text-[var(--code-text)] font-mono text-xs p-4 border-0 leading-[1.7] resize-y min-h-[200px] h-[200px] block focus:ring-0 whitespace-pre overflow-x-auto"></textarea>
361
361
  <div class="bg-[var(--surface-muted)] px-4 py-1.5 border-t border-[var(--border-light)] flex items-center justify-between text-[11px] font-mono text-[var(--text-subtle)]">
362
- <span>Editable interactive buffer &bull; changes simulate dynamically</span>
362
+ <span>Local editor &bull; edited scripts remain unverified</span>
363
363
  <span id="script-line-count">8 lines</span>
364
364
  </div>
365
365
  </div>
@@ -371,7 +371,7 @@
371
371
  <div class="panel-card p-4 flex flex-col justify-between space-y-3">
372
372
  <div class="space-y-2.5">
373
373
  <div class="flex items-center justify-between border-b border-[var(--border-light)] pb-2">
374
- <span class="font-mono text-xs font-medium text-[var(--text-muted)]">Static Linter (ShellCheck 0.10.0)</span>
374
+ <span class="font-mono text-xs font-medium text-[var(--text-muted)]">ShellCheck example output</span>
375
375
  <span id="sc-status" class="font-mono text-xs font-semibold px-2 py-0.5 rounded border">
376
376
  0 WARNINGS
377
377
  </span>
@@ -391,7 +391,7 @@
391
391
  <div class="panel-card p-4 flex flex-col justify-between space-y-3">
392
392
  <div class="space-y-2.5">
393
393
  <div class="flex items-center justify-between border-b border-[var(--border-light)] pb-2">
394
- <span class="font-mono text-xs font-medium text-[var(--text-muted)]">OpsScript Gate Runtime (v0.9.2)</span>
394
+ <span class="font-mono text-xs font-medium text-[var(--text-muted)]">OpsScript Gate example output</span>
395
395
  <span id="ops-status" class="font-mono text-xs font-semibold px-2 py-0.5 rounded border">
396
396
  1 DISTRO FAILED
397
397
  </span>
@@ -532,11 +532,17 @@
532
532
  </p>
533
533
  </div>
534
534
 
535
+ <p class="text-xs text-[var(--text-muted)]">
536
+ Testing a downloading installer? A zero exit code alone does not prove installation succeeded.
537
+ <a href="./installer-smoke-test.html" class="text-[var(--brand)] underline">Use the installation smoke test guide</a>
538
+ to check the installed binary inside each container.
539
+ </p>
540
+
535
541
  <div class="panel-card overflow-hidden">
536
542
  <div class="bg-[var(--surface-muted)] px-4 py-2.5 border-b border-[var(--border-light)] flex items-center justify-between text-xs font-mono">
537
543
  <div class="flex items-center gap-2 text-[var(--text-muted)]">
538
544
  <span class="font-medium text-[var(--text-hero)]">.github/workflows/gate.yml</span>
539
- <span class="text-[var(--text-subtle)] text-[11px] hidden sm:inline">&bull; 15 lines</span>
545
+ <span class="text-[var(--text-subtle)] text-[11px] hidden sm:inline">&bull; GitHub Actions</span>
540
546
  </div>
541
547
  <div class="flex items-center gap-2">
542
548
  <button onclick="toggleWorkflowExpansion()" id="expand-btn" class="text-xs text-[var(--text-muted)] hover:text-[var(--text-hero)] px-2 py-0.5 rounded hover:bg-[var(--surface-subtle)] transition-colors">
@@ -558,16 +564,18 @@
558
564
  <span class="font-semibold text-[var(--text-hero)]">compatibility:</span>
559
565
  <span class="font-semibold text-[var(--text-hero)]">runs-on:</span> ubuntu-latest
560
566
  <span class="font-semibold text-[var(--text-hero)]">steps:</span>
561
- - <span class="font-semibold text-[var(--text-hero)]">uses:</span> actions/checkout@v4
567
+ - <span class="font-semibold text-[var(--text-hero)]">uses:</span> actions/checkout@v7
568
+ <span class="font-semibold text-[var(--text-hero)]">with:</span>
569
+ <span class="text-[var(--text-body)]">persist-credentials:</span> false
562
570
  - <span class="font-semibold text-[var(--text-hero)]">name:</span> Validate scripts across distributions
563
- <span class="font-semibold text-[var(--brand)]">uses:</span> Mresyzz/opsscript-gate@v0.9.2
571
+ <span class="font-semibold text-[var(--brand)]">uses:</span> Mresyzz/opsscript-gate@v0.9.4
564
572
  <span class="font-semibold text-[var(--text-hero)]">with:</span>
565
573
  <span class="text-[var(--text-body)]">script-path:</span> ./install.sh
566
574
  <span class="text-[var(--text-body)]">shell:</span> auto
567
- <span class="text-[var(--text-body)]">timeout-seconds:</span> 120</code></pre>
575
+ <span class="text-[var(--text-body)]">timeout:</span> 120</code></pre>
568
576
  <div class="code-gradient-mask" id="workflow-mask" onclick="toggleWorkflowExpansion()">
569
577
  <button type="button" class="px-3.5 py-1.5 rounded-full bg-[var(--surface-elevated)] border border-[var(--border-medium)] text-[11px] font-mono text-[var(--text-body)] hover:text-[var(--brand)] hover:border-[var(--brand)] shadow-sm flex items-center gap-1.5 cursor-pointer transition-all">
570
- <span>Show full configuration (15 lines)</span>
578
+ <span>Show full configuration</span>
571
579
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"></polyline></svg>
572
580
  </button>
573
581
  </div>
@@ -579,7 +587,7 @@
579
587
  <section class="mt-16 sm:mt-20 pt-4">
580
588
  <div class="cta-invitation-card p-8 sm:p-11 text-center space-y-4">
581
589
  <div class="inline-flex items-center gap-2 px-2.5 py-0.5 rounded text-[11px] font-mono font-medium bg-[var(--brand-surface)] text-[var(--brand)] border border-[var(--brand-border)] mx-auto">
582
- Production Ready &bull; Zero Overhead
590
+ Runs in your GitHub Actions runner
583
591
  </div>
584
592
  <h3 class="font-serif text-2xl sm:text-3xl text-[var(--text-hero)] font-normal tracking-tight">
585
593
  Ready to secure your scripts across Linux distributions?
@@ -593,7 +601,7 @@
593
601
  <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
594
602
  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
595
603
  </svg>
596
- <span>Copy 5-Line Action</span>
604
+ <span>Copy Workflow</span>
597
605
  </button>
598
606
  <a href="https://github.com/Mresyzz/opsscript-gate" target="_blank" class="px-4 py-2.5 rounded font-medium bg-[var(--surface-elevated)] text-[var(--text-body)] border border-[var(--border-medium)] hover:border-[var(--brand)] hover:text-[var(--brand)] shadow-sm transition-all inline-flex items-center gap-1.5">
599
607
  <span>View on GitHub</span>
@@ -626,13 +634,15 @@ jobs:
626
634
  compatibility:
627
635
  runs-on: ubuntu-latest
628
636
  steps:
629
- - uses: actions/checkout@v4
637
+ - uses: actions/checkout@v7
638
+ with:
639
+ persist-credentials: false
630
640
  - name: Validate scripts across distributions
631
- uses: Mresyzz/opsscript-gate@v0.9.2
641
+ uses: Mresyzz/opsscript-gate@v0.9.4
632
642
  with:
633
643
  script-path: ./install.sh
634
644
  shell: auto
635
- timeout-seconds: 120`;
645
+ timeout: 120`;
636
646
 
637
647
  function updateThemeButtonText() {
638
648
  const isDark = document.documentElement.classList.contains("dark");
@@ -646,7 +656,7 @@ jobs:
646
656
  const isDark = document.documentElement.classList.toggle("dark");
647
657
  localStorage.setItem("theme", isDark ? "dark" : "light");
648
658
  updateThemeButtonText();
649
- selectCase(activeKey);
659
+ // Theme changes must preserve the edited script and its unverified state.
650
660
  }
651
661
 
652
662
  function toggleWorkflowExpansion() {
@@ -723,6 +733,7 @@ jobs:
723
733
  }
724
734
 
725
735
  function selectCase(key) {
736
+ clearTimeout(debounceTimer);
726
737
  activeKey = key;
727
738
  const c = CASES[key];
728
739
  const scriptInput = document.getElementById('script-input');
@@ -760,7 +771,7 @@ jobs:
760
771
 
761
772
  function renderCase(c) {
762
773
  const scStatus = document.getElementById('sc-status');
763
- scStatus.innerText = c.scStatus;
774
+ scStatus.innerText = "EXAMPLE: " + c.scStatus;
764
775
  scStatus.className = "font-mono text-xs font-semibold px-2 py-0.5 rounded border " + c.scClass;
765
776
 
766
777
  const scOutput = document.getElementById('sc-output');
@@ -768,7 +779,7 @@ jobs:
768
779
  scOutput.className = "bg-[var(--code-bg)] border border-[var(--border-light)] rounded p-3 text-xs font-mono h-[160px] overflow-y-auto whitespace-pre-wrap leading-[1.65] text-[var(--code-text)] break-words fade-switch";
769
780
 
770
781
  const opsStatus = document.getElementById('ops-status');
771
- opsStatus.innerText = c.opsStatus;
782
+ opsStatus.innerText = "EXAMPLE: " + c.opsStatus;
772
783
  opsStatus.className = "font-mono text-xs font-semibold px-2 py-0.5 rounded border " + c.opsClass;
773
784
 
774
785
  const opsOutput = document.getElementById('ops-output');
@@ -799,150 +810,22 @@ jobs:
799
810
  }, 160);
800
811
  }
801
812
 
802
- async function runRealEngineAnalysis(val) {
803
- try {
804
- const resp = await fetch('/api/analyze', {
805
- method: 'POST',
806
- headers: { 'Content-Type': 'application/json' },
807
- body: JSON.stringify({ script: val })
808
- });
809
- if (resp.ok) {
810
- const data = await resp.json();
811
- renderRealApiResponse(data);
812
- return;
813
- }
814
- } catch (err) {
815
- // Fallback for static hosting on GitHub Pages
816
- }
817
- clientSideAnalyzeFallback(val);
818
- }
819
-
820
- function clientSideAnalyzeFallback(val) {
821
- let isAlpineFail = false;
822
- let alpineReason = "";
823
- const hasBashism = val.includes('[[') || (val.includes('${') && val.includes(':'));
824
- const hasRead = val.includes('read');
825
- const hasSetE = val.includes('set -e') || val.includes('set -o errexit');
826
- const isBashShebang = val.startsWith('#!/bin/bash') || val.startsWith('#!/usr/bin/env bash');
827
- const isPosixShebang = val.startsWith('#!/bin/sh');
828
-
829
- // 1. Critical Deadlock & Abort Check: read under set -e
830
- const isSetEReadAbort = hasRead && hasSetE;
831
-
832
- // 2. Critical Dependency Check: Missing binaries in minimal containers
833
- // Minimal base images (debian-slim, ubuntu, alpine) strictly omit curl, jq, git, sudo, python, etc.
834
- const uninstalledBins = ["curl", "jq", "git", "sudo", "python", "python3", "docker"];
835
- let missingBin = null;
836
- let missingBinLine = 1;
837
-
838
- const lines = val.split('\n');
839
- for (let i = 0; i < lines.length; i++) {
840
- const line = lines[i].trim();
841
- if (line.startsWith('#') || !line) continue;
842
- for (const bin of uninstalledBins) {
843
- // Check if command is called directly or in a pipe
844
- const regex = new RegExp('(?:^|[|;&]\\s*)' + bin + '(?:\\s+|$)', 'i');
845
- if (regex.test(line)) {
846
- // Check if installed beforehand in script
847
- const hasInstall = val.includes('apt-get install') && val.includes(bin) || val.includes('apk add') && val.includes(bin);
848
- if (!hasInstall) {
849
- missingBin = bin;
850
- missingBinLine = i + 1;
851
- break;
852
- }
853
- }
854
- }
855
- if (missingBin) break;
813
+ function runRealEngineAnalysis(val) {
814
+ if (val === CASES[activeKey].code) {
815
+ renderCase(CASES[activeKey]);
816
+ return;
856
817
  }
857
-
858
- if (val.includes('apt-get') && !val.includes('apk')) {
859
- isAlpineFail = true;
860
- alpineReason = "apt-get: not found (line 4)";
861
- } else if (isBashShebang) {
862
- isAlpineFail = true;
863
- alpineReason = "/bin/bash: not found (line 1)";
864
- } else if (isPosixShebang && hasBashism) {
865
- isAlpineFail = true;
866
- alpineReason = "syntax error: unexpected [[ or expansion";
867
- }
868
-
869
- const scWarnings = [];
870
- if (hasRead && !val.includes('-r')) scWarnings.push("SC2162 (info): read without -r will mangle backslashes.");
871
- if (isPosixShebang && hasBashism) scWarnings.push("SC3010 (warning): In POSIX sh, [[ ]] is undefined.");
872
- if (val.includes('api_key') && !val.includes('$api_key')) scWarnings.push("SC2034 (warning): api_key appears unused. Verify use (or export if used externally).");
873
-
874
- const scText = scWarnings.length > 0
875
- ? "$ shellcheck script.sh\n" + scWarnings.join('\n')
876
- : "$ shellcheck script.sh\nNo issues detected. Syntax conforms to grammar.\n(Blind spot: linters verify syntax tokens, cannot check if binaries exist)";
877
-
878
- let logs = [];
879
- let overallStatus = "ALL DISTROS PASSED";
880
- let statusClass = "bg-[var(--status-pass-bg)] text-[var(--status-pass-text)] border-[var(--status-pass-border)]";
881
- let showDiag = false;
882
- let diagCode = "Exit Code 0";
883
- let diagMsg = "";
884
-
885
- if (missingBin) {
886
- // Missing command caught by container execution!
887
- overallStatus = `MISSING BINARY: ${missingBin} (ALL FAILED)`;
888
- statusClass = "bg-[var(--status-fail-bg)] text-[var(--status-fail-text)] border-[var(--status-fail-border)]";
889
- showDiag = true;
890
- diagCode = "Exit Code 127";
891
- diagMsg = `Standard minimal container images (debian-slim, ubuntu, alpine) do not include '${missingBin}' by default. ShellCheck verified the syntax AST, but OpsScript Gate detected that '${missingBin}' is missing at line ${missingBinLine}.`;
892
-
893
- const distros = [
894
- { d: "debian:12-slim", t: "0.15s" },
895
- { d: "ubuntu:22.04", t: "0.16s" },
896
- { d: "ubuntu:24.04", t: "0.16s" },
897
- { d: "alpine:3.20", t: "0.11s" }
898
- ];
899
- distros.forEach(item => {
900
- logs.push('<div class="my-1.5 px-2.5 py-1.5 rounded bg-[var(--status-fail-bg)] border-l-2 border-[var(--status-fail-text)] text-[var(--status-fail-text)]"><span class="font-semibold">[' + item.d + '] FAIL (' + item.t + ') - Exit 127</span><br><span class="opacity-95 font-mono text-[11px]"> /tmp/target_script.sh: line ' + missingBinLine + ': ' + missingBin + ': not found</span></div>');
901
- });
902
- } else if (isSetEReadAbort) {
903
- // True Unix behavior: read returns 1 on EOF (from </dev/null), set -e aborts script instantly!
904
- overallStatus = "STDIN DEADLOCK INTERCEPTED (ALL FAILED)";
905
- statusClass = "bg-[var(--status-fail-bg)] text-[var(--status-fail-text)] border-[var(--status-fail-border)]";
906
- showDiag = true;
907
- diagCode = "Exit Code 1 (set -e)";
908
- diagMsg = "Script called 'read' under unattended runner. OpsScript Gate bound stdin to </dev/null; read returned EOF (exit 1), causing 'set -e' to terminate execution immediately before reaching subsequent steps.";
909
-
910
- const distros = ["debian:12-slim", "ubuntu:22.04", "ubuntu:24.04", "alpine:3.20"];
911
- distros.forEach(d => {
912
- logs.push('<div class="my-1.5 px-2.5 py-1.5 rounded bg-[var(--status-fail-bg)] border-l-2 border-[var(--status-fail-text)] text-[var(--status-fail-text)]"><span class="font-semibold">[' + d + '] FAIL (0.12s) - Exit 1</span><br><span class="opacity-95 font-mono text-[11px]"> read: end of file reached via </dev/null (aborted by set -e)</span></div>');
913
- });
914
- } else {
915
- logs = [
916
- '<div class="text-[var(--status-pass-text)] font-mono text-xs">[debian:12-slim] PASS (0.22s)' + (hasRead ? ' - stdin bound to </dev/null' : '') + '</div>',
917
- '<div class="text-[var(--status-pass-text)] font-mono text-xs">[ubuntu:22.04] PASS (0.24s)' + (hasRead ? ' - stdin bound to </dev/null' : '') + '</div>',
918
- '<div class="text-[var(--status-pass-text)] font-mono text-xs">[ubuntu:24.04] PASS (0.26s)' + (hasRead ? ' - stdin bound to </dev/null' : '') + '</div>'
919
- ];
920
-
921
- if (isAlpineFail) {
922
- overallStatus = "MATRIX DISCREPANCY DETECTED";
923
- statusClass = "bg-[var(--status-fail-bg)] text-[var(--status-fail-text)] border-[var(--status-fail-border)]";
924
- showDiag = true;
925
- diagCode = "Exit Code 127";
926
- diagMsg = "Discrepancy detected on Alpine minimal container: " + alpineReason;
927
- logs.push('<div class="my-1.5 px-2.5 py-1.5 rounded bg-[var(--status-fail-bg)] border-l-2 border-[var(--status-fail-text)] text-[var(--status-fail-text)]"><span class="font-semibold">[alpine:3.20] FAIL (0.11s) - Exit 127</span><br><span class="opacity-95 font-mono text-[11px]"> /tmp/target_script.sh: ' + alpineReason + '</span></div>');
928
- } else {
929
- logs.push('<div class="text-[var(--status-pass-text)] font-mono text-xs">[alpine:3.20] PASS (0.18s)</div>');
930
- }
931
- }
932
-
933
818
  renderRealApiResponse({
934
819
  shellcheck: {
935
- status_text: scWarnings.length > 0 ? (scWarnings.length + " WARNINGS") : "0 WARNINGS",
936
- warning_count: scWarnings.length,
937
- output: scText
820
+ status_text: "NOT RUN",
821
+ warning_count: 0,
822
+ output: "This page does not run ShellCheck. Run shellcheck on your script locally or in CI."
938
823
  },
939
824
  opsscript: {
940
- overall_status: overallStatus,
941
- status_class: statusClass,
942
- terminal_html: logs.join('\n'),
943
- show_diagnostic: showDiag,
944
- diagnostic_code: diagCode,
945
- diagnostic_msg: diagMsg
825
+ overall_status: "UNKNOWN — NOT EXECUTED",
826
+ status_class: "bg-[var(--surface-muted)] text-[var(--text-muted)] border-[var(--border-medium)]",
827
+ terminal_html: "No container execution has taken place. Copy the workflow below to test your script across distributions.",
828
+ show_diagnostic: false
946
829
  }
947
830
  });
948
831
  }
@@ -951,10 +834,12 @@ jobs:
951
834
  const sc = data.shellcheck;
952
835
  const ops = data.opsscript;
953
836
 
954
- // Real ShellCheck output
837
+ // Render the analysis state
955
838
  const scStatus = document.getElementById('sc-status');
956
839
  scStatus.innerText = sc.status_text;
957
- scStatus.className = sc.warning_count > 0
840
+ scStatus.className = sc.status_text === "NOT RUN"
841
+ ? "font-mono text-xs font-semibold px-2 py-0.5 rounded border bg-[var(--surface-muted)] text-[var(--text-muted)] border-[var(--border-medium)]"
842
+ : sc.warning_count > 0
958
843
  ? "font-mono text-xs font-semibold px-2 py-0.5 rounded border bg-[var(--status-warn-bg)] text-[var(--status-warn-text)] border-[var(--status-warn-border)]"
959
844
  : "font-mono text-xs font-semibold px-2 py-0.5 rounded border bg-[var(--status-pass-bg)] text-[var(--status-pass-text)] border-[var(--status-pass-border)]";
960
845
 
@@ -962,7 +847,7 @@ jobs:
962
847
  scOutput.innerText = sc.output;
963
848
  scOutput.className = "bg-[var(--code-bg)] border border-[var(--border-light)] rounded p-3 text-xs font-mono h-[160px] overflow-y-auto whitespace-pre-wrap leading-[1.65] text-[var(--code-text)] break-words fade-switch";
964
849
 
965
- // Real OpsScript Gate output
850
+ // Render the runtime state
966
851
  const opsStatus = document.getElementById('ops-status');
967
852
  opsStatus.innerText = ops.overall_status;
968
853
  opsStatus.className = "font-mono text-xs font-semibold px-2 py-0.5 rounded border " + ops.status_class;
@@ -971,7 +856,7 @@ jobs:
971
856
  opsOutput.innerHTML = ops.terminal_html;
972
857
  opsOutput.className = "bg-[var(--code-bg)] border border-[var(--border-light)] rounded p-3 text-xs font-mono h-[160px] overflow-y-auto whitespace-pre-wrap leading-[1.65] text-[var(--code-text)] break-words fade-switch";
973
858
 
974
- // Real Diagnostic Panel
859
+ // Render diagnostic details
975
860
  const panel = document.getElementById('diagnostic-panel');
976
861
  if (ops.show_diagnostic) {
977
862
  panel.style.display = 'block';