From 60896182889b5eeb69335e7bf7ca45e9edc3933b Mon Sep 17 00:00:00 2001 From: "opencode[bot]" Date: Fri, 1 May 2026 12:12:49 +0000 Subject: [PATCH 1/8] feat: add test-value-detector action for detecting low-value tests --- test-value-detector/action.yml | 245 +++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 test-value-detector/action.yml diff --git a/test-value-detector/action.yml b/test-value-detector/action.yml new file mode 100644 index 0000000..53db63e --- /dev/null +++ b/test-value-detector/action.yml @@ -0,0 +1,245 @@ +name: OpenCode Test Value Detector +description: Detects low-value tests in pull requests — empty assertions, hardcoded mocks, detached tests, duplicates, and missing edge-case coverage. + +inputs: + install-url: + description: Installer URL used to bootstrap OpenCode. + required: false + default: https://opencode.ai/install + install-dir: + description: Directory where the opencode binary should be installed. + required: false + default: "" + xdg-cache-home: + description: Dedicated XDG cache directory for OpenCode. + required: false + default: "" + cache: + description: Cache the install and XDG cache directories with actions/cache. + required: false + default: "true" + cache-key: + description: Cache key suffix used to invalidate installer-based caches. + required: false + default: v1 + install-attempts: + description: Number of installer retry attempts. + required: false + default: "3" + allow-preinstalled: + description: Reuse an existing opencode found on PATH instead of forcing installer bootstrap. + required: false + default: "false" + version: + description: Minimum required OpenCode version (semver). If the cached binary is older, it will be reinstalled. Defaults to the version in setup-opencode/default-version. Use "none" to disable version checking. + required: false + default: "" + working-directory: + description: Optional working directory before running opencode. + required: false + default: "" + attempts: + description: Total number of attempts before failing. + required: false + default: "3" + retry-profile: + description: Built-in retry profile, defaults to github-network for common GitHub usage. + required: false + default: github-network + retry-on-regex: + description: Retry only when stderr or stdout matches this regex. + required: false + default: "" + retry-delay-seconds: + description: Base delay between retries in seconds. + required: false + default: "15" + timeout-seconds: + description: Maximum execution time in seconds for `opencode github run`. Set 0 to disable. + required: false + default: "600" + model: + description: Value exported as MODEL before running `opencode github run`. + required: false + default: "" + fallback-models: + description: Optional comma- or newline-delimited fallback models tried after timeout or timeout-like failures. + required: false + default: "" + model-timeout-seconds: + description: Per-model timeout used before rotating to the next fallback model when fallback-models are configured. Set 0 to disable. + required: false + default: "300" + fallback-on-regex: + description: Rotate to the next fallback model when output matches this regex. + required: false + default: timed out|timeout|deadline exceeded|context deadline exceeded|operation timed out|connection timed out + prompt: + description: Value exported as PROMPT before running `opencode github run`. + required: false + default: | + 分析本 Pull Request 中新增或修改的测试代码(只读模式,请勿修改任何代码)。 + + 请检查以下低价值测试维度: + - 空断言/软断言:测试中没有任何 assert、expect、require 等断言调用,或者断言条件永远为真 + - 硬编码 mock:mock 的返回值与实际业务逻辑无关,测试无论业务正确与否都会通过 + - 测试与实现脱节:测试引用的函数、方法、类在对应源码中不存在,或调用签名与实际定义不匹配 + - 重复测试:与已有测试逻辑完全重复,没有增加额外的覆盖价值 + - 无边界/异常覆盖:仅覆盖 happy path,缺少边界值、空值、异常输入、错误处理等场景的测试 + + 请用中文回答。请勿修改任何代码,仅提供分析评论。 + 回复的第一行必须是以下二者之一: + - 测试全部有价值 + - 发现低价值测试 + + 判定规则: + - 仅当所有测试都具备有效断言、mock 合理、与实现一致、无重复、且覆盖边界与异常场景时,使用"测试全部有价值"。 + - 只要存在上述任一维度的问题,即使用"发现低价值测试"。 + + 仅审查最新 PR HEAD 的代码。 + 不要重复先前修订中发现但已在当前版本中修复的问题。 + 在列出任何问题前,请先在当前代码中验证该问题仍然存在。 + + 输出格式: + - 第一行:仅输出判定结果 + - 然后按 CRITICAL / MEDIUM / LOW 三个等级分类列出发现的问题 + - CRITICAL:空断言、与实现完全脱节的测试 + - MEDIUM:硬编码 mock、重复测试 + - LOW:缺少边界/异常覆盖 + - 每个问题需注明所在文件、行号范围及具体原因 + - 若某个等级没有问题,则写"无" + use-github-token: + description: Value exported as USE_GITHUB_TOKEN before running `opencode github run`. + required: false + default: "true" + github-token: + description: Value exported as GITHUB_TOKEN before running `opencode github run`. + required: false + default: "" + zhipu-api-key: + description: Value exported as ZHIPU_API_KEY before running `opencode github run`. + required: false + default: "" + opencode-go-api-key: + description: Value exported as OPENCODE_API_KEY before running `opencode github run`. The opencode-go provider shares this env var with OpenCode Zen. + required: false + default: "" + deepseek-api-key: + description: Value exported as DEEPSEEK_API_KEY before running `opencode github run`. + required: false + default: "" + reasoning-effort: + description: Reasoning effort level for the model agent. Allowed values are low, medium, high, max. + required: false + default: "max" + enable-thinking: + description: Enable thinking mode for the model agent. When true, generates opencode.json with thinking.type=enabled. + required: false + default: "true" + +runs: + using: composite + steps: + - if: ${{ runner.os != 'Linux' }} + shell: bash + run: | + set -euo pipefail + printf 'test-value-detector currently supports Linux runners only\n' >&2 + exit 1 + + - id: version + shell: bash + run: | + set -euo pipefail + effective="${{ inputs.version }}" + if [[ "$effective" == "none" ]]; then + effective="" + elif [[ -z "$effective" ]]; then + default_file="${{ github.action_path }}/../setup-opencode/default-version" + if [[ ! -f "$default_file" ]]; then + printf 'error: default-version file not found at %s\n' "$default_file" >&2 + exit 1 + fi + effective="$(tr -d '[:space:]' < "$default_file")" + if [[ ! "$effective" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + printf 'error: invalid version in %s: %s\n' "$default_file" "$(cat "$default_file")" >&2 + exit 1 + fi + fi + printf 'version=%s\n' "$effective" >>"$GITHUB_OUTPUT" + + - id: paths + shell: bash + env: + INPUT_INSTALL_DIR: ${{ inputs.install-dir }} + INPUT_XDG_CACHE_HOME: ${{ inputs.xdg-cache-home }} + run: | + set -euo pipefail + install_dir="$INPUT_INSTALL_DIR" + xdg_cache_home="$INPUT_XDG_CACHE_HOME" + + if [[ -z "$install_dir" ]]; then + install_dir="${RUNNER_TOOL_CACHE:-$HOME/.cache}/opencode/bin" + fi + + if [[ -z "$xdg_cache_home" ]]; then + xdg_cache_home="${RUNNER_TOOL_CACHE:-$HOME/.cache}/opencode/cache" + fi + + printf 'install_dir=%s\n' "$install_dir" >>"$GITHUB_OUTPUT" + printf 'xdg_cache_home=%s\n' "$xdg_cache_home" >>"$GITHUB_OUTPUT" + + - id: key + shell: bash + env: + INPUT_INSTALL_URL: ${{ inputs.install-url }} + run: | + set -euo pipefail + install_url_hash="$(printf '%s' "$INPUT_INSTALL_URL" | sha256sum | cut -d' ' -f1)" + printf 'install_url_hash=%s\n' "$install_url_hash" >>"$GITHUB_OUTPUT" + + - id: cache + if: ${{ inputs.cache == 'true' }} + uses: actions/cache@v5 + with: + path: | + ${{ steps.paths.outputs.install_dir }} + ${{ steps.paths.outputs.xdg_cache_home }} + key: test-value-detector-opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.key.outputs.install_url_hash }}-${{ steps.version.outputs.version }}-${{ inputs.cache-key }} + + - shell: bash + env: + OPENCODE_INSTALL_DIR: ${{ steps.paths.outputs.install_dir }} + XDG_CACHE_HOME: ${{ steps.paths.outputs.xdg_cache_home }} + OPENCODE_INSTALL_URL: ${{ inputs.install-url }} + OPENCODE_INSTALL_ATTEMPTS: ${{ inputs.install-attempts }} + OPENCODE_ALLOW_PREINSTALLED: ${{ inputs.allow-preinstalled }} + OPENCODE_MIN_VERSION: ${{ steps.version.outputs.version }} + run: ${{ github.action_path }}/../setup-opencode/install-opencode.sh + + - shell: bash + env: + GITHUB_RUN_OPENCODE_WORKING_DIRECTORY: ${{ inputs.working-directory }} + GITHUB_RUN_OPENCODE_ATTEMPTS: ${{ inputs.attempts }} + GITHUB_RUN_OPENCODE_RETRY_PROFILE: ${{ inputs.retry-profile }} + GITHUB_RUN_OPENCODE_RETRY_ON_REGEX: ${{ inputs.retry-on-regex }} + GITHUB_RUN_OPENCODE_RETRY_DELAY_SECONDS: ${{ inputs.retry-delay-seconds }} + GITHUB_RUN_OPENCODE_TIMEOUT_SECONDS: ${{ inputs.timeout-seconds }} + GITHUB_RUN_OPENCODE_MODEL: ${{ inputs.model }} + GITHUB_RUN_OPENCODE_FALLBACK_MODELS: ${{ inputs.fallback-models }} + GITHUB_RUN_OPENCODE_MODEL_TIMEOUT_SECONDS: ${{ inputs.model-timeout-seconds }} + GITHUB_RUN_OPENCODE_FALLBACK_ON_REGEX: ${{ inputs.fallback-on-regex }} + GITHUB_RUN_OPENCODE_PROMPT: ${{ inputs.prompt }} + GITHUB_RUN_OPENCODE_USE_GITHUB_TOKEN: ${{ inputs.use-github-token }} + GITHUB_RUN_OPENCODE_GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_RUN_OPENCODE_ZHIPU_API_KEY: ${{ inputs.zhipu-api-key }} + GITHUB_RUN_OPENCODE_OPENCODE_GO_API_KEY: ${{ inputs.opencode-go-api-key }} + GITHUB_RUN_OPENCODE_DEEPSEEK_API_KEY: ${{ inputs.deepseek-api-key }} + GITHUB_RUN_OPENCODE_REASONING_EFFORT: ${{ inputs.reasoning-effort }} + GITHUB_RUN_OPENCODE_ENABLE_THINKING: ${{ inputs.enable-thinking }} + run: | + if ! command -v python3 >/dev/null 2>&1; then + printf 'python3 is required but not installed on this runner\n' >&2 + exit 1 + fi + python3 ${{ github.action_path }}/../github-run-opencode/run-github-opencode.py From 871582ae192e9056fcb9b4f832b75ec4a31c5ad5 Mon Sep 17 00:00:00 2001 From: "opencode[bot]" Date: Fri, 1 May 2026 12:13:40 +0000 Subject: [PATCH 2/8] docs: add test-value-detector to README --- README.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b848ca4..a97a2e0 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ npx skills add sun-praise/opencode-actions - `review`: opinionated PR review wrapper with built-in prompt and model defaults - `feature-missing`: audits PR implementation against linked issue spec to find missing features - `spec-coverage`: cross-references project spec/task files against PR implementation to find planned but unimplemented features +- `test-value-detector`: detects low-value tests in PRs — empty assertions, hardcoded mocks, detached tests, duplicates, missing edge-case coverage - `github-run-opencode`: one-step wrapper for the common `opencode github run` workflow - `setup-opencode`: installs OpenCode, restores a dedicated cache, and exports the binary path - `run-opencode`: runs `opencode` with optional retry logic for flaky GitHub network failures @@ -124,6 +125,27 @@ Unlike `feature-missing` (which checks PR self-described scope), `spec-coverage` opencode-go-api-key: ${{ secrets.OPENCODE_GO_API_KEY }} ``` +## test-value-detector + +Use this to automatically detect low-value tests in pull requests — tests that pass CI but contribute nothing to code quality. + +- identifies empty assertions and always-true conditions +- flags hardcoded mocks that decouple tests from real logic +- detects tests referencing non-existent or mismatched functions +- spots duplicate tests with no additional coverage value +- highlights missing boundary, error, and edge-case coverage +- classifies findings by severity: CRITICAL, MEDIUM, LOW +- shares the same inputs and cache as `review`/`github-run-opencode` + +```yaml +- name: Run test value detection + uses: Svtter/opencode-actions/test-value-detector@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + zhipu-api-key: ${{ secrets.ZHIPU_API_KEY }} + opencode-go-api-key: ${{ secrets.OPENCODE_GO_API_KEY }} +``` + ### How the three review actions differ | Action | Scope source | What it catches | @@ -132,6 +154,10 @@ Unlike `feature-missing` (which checks PR self-described scope), `spec-coverage` | `feature-missing` | PR title/body + linked issues | PR self-described scope completeness | | `spec-coverage` | Project spec/task files | Full planned scope vs implementation | +### Customization + +Override the built-in prompt via the `prompt` input to adjust detection focus or severity thresholds for your project. + ## setup-opencode ### Inputs @@ -185,6 +211,7 @@ Public consumers should reference the subdirectory action path: uses: Svtter/opencode-actions/review@v2 uses: Svtter/opencode-actions/feature-missing@v2 uses: Svtter/opencode-actions/spec-coverage@v2 +uses: Svtter/opencode-actions/test-value-detector@v2 uses: Svtter/opencode-actions/github-run-opencode@v2 uses: Svtter/opencode-actions/setup-opencode@v2 uses: Svtter/opencode-actions/run-opencode@v2 @@ -219,7 +246,7 @@ This repository includes a CI workflow that: - runs `shellcheck` on every bundled shell script - runs the local shell-based regression suite -- smoke-tests all actions through `uses: ./setup-opencode`, `uses: ./run-opencode`, `uses: ./github-run-opencode`, `uses: ./review`, `uses: ./feature-missing`, and `uses: ./spec-coverage` +- smoke-tests all actions through `uses: ./setup-opencode`, `uses: ./run-opencode`, `uses: ./github-run-opencode`, `uses: ./review`, `uses: ./feature-missing`, `uses: ./spec-coverage`, and `uses: ./test-value-detector` ## Release Policy @@ -234,7 +261,7 @@ This repository includes a CI workflow that: 2. Verify `CI` passes on `main`. 3. Create a GitHub release with a semver tag such as `v1.0.0`. 4. Confirm the `Update Major Tag` workflow moved `v1` to that release. -5. Use `owner/repo/review@v2` for the simplest review setup, `owner/repo/feature-missing@v2` for PR scope audit, `owner/repo/spec-coverage@v2` for spec coverage audit, `owner/repo/github-run-opencode@v2` for generic `github run`, or `owner/repo/setup-opencode@v2` plus `owner/repo/run-opencode@v2` for more control. +5. Use `owner/repo/review@v2` for the simplest review setup, `owner/repo/feature-missing@v2` for spec coverage audit, `owner/repo/spec-coverage@v2` for spec coverage audit, `owner/repo/test-value-detector@v2` for low-value test detection, `owner/repo/github-run-opencode@v2` for generic `github run`, or `owner/repo/setup-opencode@v2` plus `owner/repo/run-opencode@v2` for more control. The initial release-notes template lives at `docs/releases/v1.0.0.md`. From 515040a119e4d0e8cc2824932d5b3c3d0f5c794b Mon Sep 17 00:00:00 2001 From: svtter Date: Thu, 14 May 2026 23:13:15 +0800 Subject: [PATCH 3/8] fix: address review feedback for test-value-detector - Add GITHUB_RUN_OPENCODE_PERMISSION security env var to prevent write ops - Add test-value-detector smoke test to ci.yml and smoke-test.yml - Update README table from 'three' to 'four' review actions with test-value-detector entry --- .github/workflows/ci.yml | 10 ++++++++++ .github/workflows/smoke-test.yml | 9 +++++++++ README.md | 3 ++- test-value-detector/action.yml | 2 ++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71017c1..e3c5f88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,16 @@ jobs: github-token: smoke-gh-token zhipu-api-key: smoke-zhipu-token + - name: Run test-value-detector convenience action + uses: ./test-value-detector + with: + install-url: http://127.0.0.1:8765/fake-installer.sh + cache: false + install-attempts: 1 + attempts: 1 + github-token: smoke-gh-token + zhipu-api-key: smoke-zhipu-token + - name: Stop fake installer server if: always() run: | diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 10d388d..4f08d2f 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -36,3 +36,12 @@ jobs: attempts: "1" github-token: ${{ secrets.GITHUB_TOKEN }} continue-on-error: true + + - name: Test test-value-detector action loads + uses: ./test-value-detector + with: + model: test-model + timeout-seconds: "5" + attempts: "1" + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true diff --git a/README.md b/README.md index a97a2e0..f8dea98 100644 --- a/README.md +++ b/README.md @@ -146,13 +146,14 @@ Use this to automatically detect low-value tests in pull requests — tests that opencode-go-api-key: ${{ secrets.OPENCODE_GO_API_KEY }} ``` -### How the three review actions differ +### How the four review actions differ | Action | Scope source | What it catches | | --- | --- | --- | | `review` | PR diff | Code quality, security, bugs | | `feature-missing` | PR title/body + linked issues | PR self-described scope completeness | | `spec-coverage` | Project spec/task files | Full planned scope vs implementation | +| `test-value-detector` | PR test code | Low-value test patterns (empty assertions, hardcoded mocks, detached tests, duplicates, missing edge-case coverage) | ### Customization diff --git a/test-value-detector/action.yml b/test-value-detector/action.yml index 53db63e..eecaaf5 100644 --- a/test-value-detector/action.yml +++ b/test-value-detector/action.yml @@ -237,6 +237,8 @@ runs: GITHUB_RUN_OPENCODE_DEEPSEEK_API_KEY: ${{ inputs.deepseek-api-key }} GITHUB_RUN_OPENCODE_REASONING_EFFORT: ${{ inputs.reasoning-effort }} GITHUB_RUN_OPENCODE_ENABLE_THINKING: ${{ inputs.enable-thinking }} + GITHUB_RUN_OPENCODE_PERMISSION: >- + {"edit":"deny","bash":{"git commit *":"deny","git push *":"deny","git add *":"deny","git stash *":"deny","git reset *":"deny","git checkout *":"deny"}} run: | if ! command -v python3 >/dev/null 2>&1; then printf 'python3 is required but not installed on this runner\n' >&2 From be3fbf069f4368aa8f02587a4d5f82df3abc9934 Mon Sep 17 00:00:00 2001 From: svtter Date: Thu, 14 May 2026 23:18:29 +0800 Subject: [PATCH 4/8] fix: add extra-env input to test-value-detector for consistency with review/spec-coverage actions --- test-value-detector/action.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test-value-detector/action.yml b/test-value-detector/action.yml index eecaaf5..21e0048 100644 --- a/test-value-detector/action.yml +++ b/test-value-detector/action.yml @@ -136,6 +136,13 @@ inputs: description: Enable thinking mode for the model agent. When true, generates opencode.json with thinking.type=enabled. required: false default: "true" + extra-env: + description: >- + Extra environment variables to pass to opencode runtime. + Multi-line KEY=VALUE pairs, one per line. Empty lines and lines + starting with '#' are ignored. + required: false + default: "" runs: using: composite @@ -237,6 +244,7 @@ runs: GITHUB_RUN_OPENCODE_DEEPSEEK_API_KEY: ${{ inputs.deepseek-api-key }} GITHUB_RUN_OPENCODE_REASONING_EFFORT: ${{ inputs.reasoning-effort }} GITHUB_RUN_OPENCODE_ENABLE_THINKING: ${{ inputs.enable-thinking }} + GITHUB_RUN_OPENCODE_EXTRA_ENV: ${{ inputs.extra-env }} GITHUB_RUN_OPENCODE_PERMISSION: >- {"edit":"deny","bash":{"git commit *":"deny","git push *":"deny","git add *":"deny","git stash *":"deny","git reset *":"deny","git checkout *":"deny"}} run: | From 9cf5a2b50b739fb63ee9e77611a132d01f98076c Mon Sep 17 00:00:00 2001 From: svtter Date: Thu, 14 May 2026 23:22:54 +0800 Subject: [PATCH 5/8] docs: clarify extra-env value parsing behavior in test-value-detector action.yml --- test-value-detector/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-value-detector/action.yml b/test-value-detector/action.yml index 21e0048..3737edd 100644 --- a/test-value-detector/action.yml +++ b/test-value-detector/action.yml @@ -140,7 +140,8 @@ inputs: description: >- Extra environment variables to pass to opencode runtime. Multi-line KEY=VALUE pairs, one per line. Empty lines and lines - starting with '#' are ignored. + starting with '#' are ignored. Values may contain '=' and '#' + characters; only lines starting with '#' are treated as comments. required: false default: "" From 9941c9e1f226c2ad9eb9d33c77a3b492a0f92f76 Mon Sep 17 00:00:00 2001 From: svtter Date: Thu, 14 May 2026 23:28:00 +0800 Subject: [PATCH 6/8] fix: address review feedback for test-value-detector and related actions - Add configurable permission input to test-value-detector/action.yml - Add missing extra-env input to feature-missing/action.yml - Add sensitive env var warning in extra-env parsing - Add permission JSON type validation (must be dict) - Stop stripping whitespace from extra-env values to preserve intent --- feature-missing/action.yml | 9 +++++++++ github-run-opencode/run-github-opencode.py | 6 +++++- test-value-detector/action.yml | 12 ++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/feature-missing/action.yml b/feature-missing/action.yml index c1b29ac..218cbfc 100644 --- a/feature-missing/action.yml +++ b/feature-missing/action.yml @@ -152,6 +152,14 @@ inputs: description: Rotate to the next fallback model when output matches this regex. required: false default: timed out|timeout|deadline exceeded|context deadline exceeded|operation timed out|connection timed out + extra-env: + description: >- + Extra environment variables to pass to opencode runtime. + Multi-line KEY=VALUE pairs, one per line. Empty lines and lines + starting with '#' are ignored. Values may contain '=' and '#' + characters; only lines starting with '#' are treated as comments. + required: false + default: "" runs: using: composite @@ -253,6 +261,7 @@ runs: GITHUB_RUN_OPENCODE_FALLBACK_MODELS: ${{ inputs.fallback-models }} GITHUB_RUN_OPENCODE_MODEL_TIMEOUT_SECONDS: ${{ inputs.model-timeout-seconds }} GITHUB_RUN_OPENCODE_FALLBACK_ON_REGEX: ${{ inputs.fallback-on-regex }} + GITHUB_RUN_OPENCODE_EXTRA_ENV: ${{ inputs.extra-env }} GITHUB_RUN_OPENCODE_PERMISSION: >- {"edit":"deny","bash":{"git commit *":"deny","git push *":"deny","git add *":"deny","git stash *":"deny","git reset *":"deny","git checkout *":"deny"}} run: | diff --git a/github-run-opencode/run-github-opencode.py b/github-run-opencode/run-github-opencode.py index d12166d..2c7817c 100755 --- a/github-run-opencode/run-github-opencode.py +++ b/github-run-opencode/run-github-opencode.py @@ -214,8 +214,9 @@ def main() -> int: continue key, _, value = line.partition("=") key = key.strip() - value = value.strip() if key: + if re.search(r'(API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)', key, re.IGNORECASE): + print(f"Warning: extra-env key '{key}' looks like a sensitive variable — make sure this is intentional", file=sys.stderr) os.environ[key] = value reasoning_effort = get_env("GITHUB_RUN_OPENCODE_REASONING_EFFORT", "") @@ -230,6 +231,9 @@ def main() -> int: except json.JSONDecodeError: print(f"Invalid JSON in GITHUB_RUN_OPENCODE_PERMISSION: {permission_raw}", file=sys.stderr) sys.exit(1) + if not isinstance(permission, dict): + print(f"GITHUB_RUN_OPENCODE_PERMISSION must be a JSON object, got {type(permission).__name__}", file=sys.stderr) + sys.exit(1) needs_config = reasoning_effort or enable_thinking.lower() == "true" or permission if needs_config: diff --git a/test-value-detector/action.yml b/test-value-detector/action.yml index 3737edd..1545018 100644 --- a/test-value-detector/action.yml +++ b/test-value-detector/action.yml @@ -144,6 +144,15 @@ inputs: characters; only lines starting with '#' are treated as comments. required: false default: "" + permission: + description: >- + JSON object for opencode agent-level permission overrides (merged into opencode.json). + When empty, uses the default read-only permission policy. + See https://opencode.ai/docs/permissions/ for available keys. + Example: '{"edit":"deny","bash":{"git commit *":"deny"}}' + required: false + default: >- + {"edit":"deny","bash":{"git commit *":"deny","git push *":"deny","git add *":"deny","git stash *":"deny","git reset *":"deny","git checkout *":"deny"}} runs: using: composite @@ -246,8 +255,7 @@ runs: GITHUB_RUN_OPENCODE_REASONING_EFFORT: ${{ inputs.reasoning-effort }} GITHUB_RUN_OPENCODE_ENABLE_THINKING: ${{ inputs.enable-thinking }} GITHUB_RUN_OPENCODE_EXTRA_ENV: ${{ inputs.extra-env }} - GITHUB_RUN_OPENCODE_PERMISSION: >- - {"edit":"deny","bash":{"git commit *":"deny","git push *":"deny","git add *":"deny","git stash *":"deny","git reset *":"deny","git checkout *":"deny"}} + GITHUB_RUN_OPENCODE_PERMISSION: ${{ inputs.permission }} run: | if ! command -v python3 >/dev/null 2>&1; then printf 'python3 is required but not installed on this runner\n' >&2 From 67738b3b38ff049b4cbccf29bd783c463cc917e6 Mon Sep 17 00:00:00 2001 From: svtter Date: Thu, 14 May 2026 23:31:16 +0800 Subject: [PATCH 7/8] fix: harden extra-env key validation and restore value.strip() - Restore value.strip() to prevent leading/trailing whitespace in env values - Add BLOCKED_ENV_KEYS denylist for critical variables (PATH, HOME, MODEL, etc.) - Validate key format with regex [A-Za-z_][A-Za-z0-9_]* - Use ::error:: / ::warning:: GitHub Actions annotation syntax --- github-run-opencode/run-github-opencode.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/github-run-opencode/run-github-opencode.py b/github-run-opencode/run-github-opencode.py index 2c7817c..d500025 100755 --- a/github-run-opencode/run-github-opencode.py +++ b/github-run-opencode/run-github-opencode.py @@ -203,6 +203,12 @@ def main() -> int: set_env("DEEPSEEK_API_KEY", get_env("GITHUB_RUN_OPENCODE_DEEPSEEK_API_KEY")) # Extra env vars from extra-env input + BLOCKED_ENV_KEYS = frozenset({ + "PATH", "HOME", "USER", "SHELL", "MODEL", "GITHUB_TOKEN", + "GITHUB_WORKSPACE", "GITHUB_EVENT_PATH", "GITHUB_SHA", + "GITHUB_REPOSITORY", "GITHUB_REF", "GITHUB_RUN_ID", + "GITHUB_ACTIONS", "LD_LIBRARY_PATH", "PYTHONPATH", + }) extra_env_raw = get_env("GITHUB_RUN_OPENCODE_EXTRA_ENV") if extra_env_raw: for line in extra_env_raw.splitlines(): @@ -214,10 +220,17 @@ def main() -> int: continue key, _, value = line.partition("=") key = key.strip() - if key: - if re.search(r'(API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)', key, re.IGNORECASE): - print(f"Warning: extra-env key '{key}' looks like a sensitive variable — make sure this is intentional", file=sys.stderr) - os.environ[key] = value + if not key: + continue + if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', key): + print(f"::error::extra-env key '{key}' is not a valid environment variable name (must match [A-Za-z_][A-Za-z0-9_]*)", file=sys.stderr) + sys.exit(1) + if key in BLOCKED_ENV_KEYS: + print(f"::error::extra-env key '{key}' is blocked — overriding this variable is not allowed", file=sys.stderr) + sys.exit(1) + if re.search(r'(API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)', key, re.IGNORECASE): + print(f"::warning::extra-env key '{key}' looks like a sensitive variable — make sure this is intentional") + os.environ[key] = value.strip() reasoning_effort = get_env("GITHUB_RUN_OPENCODE_REASONING_EFFORT", "") enable_thinking = get_env("GITHUB_RUN_OPENCODE_ENABLE_THINKING", "false") From eedc0c666fab742afb16b98b0aedea753c6be8d9 Mon Sep 17 00:00:00 2001 From: svtter Date: Thu, 14 May 2026 23:34:55 +0800 Subject: [PATCH 8/8] fix: correct feature-missing description in README publishing checklist --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8dea98..a88ef71 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,7 @@ This repository includes a CI workflow that: 2. Verify `CI` passes on `main`. 3. Create a GitHub release with a semver tag such as `v1.0.0`. 4. Confirm the `Update Major Tag` workflow moved `v1` to that release. -5. Use `owner/repo/review@v2` for the simplest review setup, `owner/repo/feature-missing@v2` for spec coverage audit, `owner/repo/spec-coverage@v2` for spec coverage audit, `owner/repo/test-value-detector@v2` for low-value test detection, `owner/repo/github-run-opencode@v2` for generic `github run`, or `owner/repo/setup-opencode@v2` plus `owner/repo/run-opencode@v2` for more control. +5. Use `owner/repo/review@v2` for the simplest review setup, `owner/repo/feature-missing@v2` for PR scope audit, `owner/repo/spec-coverage@v2` for spec coverage audit, `owner/repo/test-value-detector@v2` for low-value test detection, `owner/repo/github-run-opencode@v2` for generic `github run`, or `owner/repo/setup-opencode@v2` plus `owner/repo/run-opencode@v2` for more control. The initial release-notes template lives at `docs/releases/v1.0.0.md`.