diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71017c1..d78cf88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,26 @@ jobs: github-token: smoke-gh-token zhipu-api-key: smoke-zhipu-token + - name: Run regression-test-missing action + uses: ./regression-test-missing + 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: Run pr-checks action + uses: ./pr-checks + 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/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..40d241d --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,149 @@ +name: OpenCode PR Checks + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + setup: + name: Setup OpenCode + if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + outputs: + install-dir: ${{ steps.setup.outputs.install-dir }} + xdg-cache-home: ${{ steps.setup.outputs.xdg-cache-home }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Setup OpenCode + id: setup + uses: ./setup-opencode + + review: + name: Code Review + needs: [setup] + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + steps: + - name: Checkout PR head + uses: actions/checkout@v6 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Restore OpenCode cache + uses: ./setup-opencode + with: + cache: true + install-dir: ${{ needs.setup.outputs.install-dir }} + xdg-cache-home: ${{ needs.setup.outputs.xdg-cache-home }} + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Run OpenCode review + uses: ./review + with: + cache: false + install-dir: ${{ needs.setup.outputs.install-dir }} + xdg-cache-home: ${{ needs.setup.outputs.xdg-cache-home }} + model: ${{ vars.MODEL_NAME }} + github-token: ${{ secrets.GITHUB_TOKEN }} + zhipu-api-key: ${{ secrets.ZHIPU_API_KEY }} + deepseek-api-key: ${{ secrets.DEEPSEEK_API_KEY }} + opencode-go-api-key: ${{ secrets.OPENCODE_GO_API_KEY }} + + feature-missing: + name: Feature Missing + needs: [setup] + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: write + issues: write + steps: + - name: Checkout PR head + uses: actions/checkout@v6 + with: + clean: true + persist-credentials: true + token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref || github.event.pull_request.head.sha }} + + - name: Restore OpenCode cache + uses: ./setup-opencode + with: + cache: true + install-dir: ${{ needs.setup.outputs.install-dir }} + xdg-cache-home: ${{ needs.setup.outputs.xdg-cache-home }} + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Run feature missing audit + uses: ./feature-missing + with: + cache: false + install-dir: ${{ needs.setup.outputs.install-dir }} + xdg-cache-home: ${{ needs.setup.outputs.xdg-cache-home }} + model: ${{ vars.MODEL_NAME }} + github-token: ${{ secrets.GITHUB_TOKEN }} + zhipu-api-key: ${{ secrets.ZHIPU_API_KEY }} + deepseek-api-key: ${{ secrets.DEEPSEEK_API_KEY }} + opencode-go-api-key: ${{ secrets.OPENCODE_GO_API_KEY }} + + regression-test-missing: + name: Regression Test Missing + needs: [setup] + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: write + issues: write + steps: + - name: Checkout PR head + uses: actions/checkout@v6 + with: + clean: true + persist-credentials: true + token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref || github.event.pull_request.head.sha }} + + - name: Restore OpenCode cache + uses: ./setup-opencode + with: + cache: true + install-dir: ${{ needs.setup.outputs.install-dir }} + xdg-cache-home: ${{ needs.setup.outputs.xdg-cache-home }} + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Run regression test missing audit + uses: ./regression-test-missing + with: + cache: false + install-dir: ${{ needs.setup.outputs.install-dir }} + xdg-cache-home: ${{ needs.setup.outputs.xdg-cache-home }} + model: ${{ vars.MODEL_NAME }} + github-token: ${{ secrets.GITHUB_TOKEN }} + zhipu-api-key: ${{ secrets.ZHIPU_API_KEY }} + deepseek-api-key: ${{ secrets.DEEPSEEK_API_KEY }} + opencode-go-api-key: ${{ secrets.OPENCODE_GO_API_KEY }} diff --git a/.github/workflows/regression-test-missing.yml b/.github/workflows/regression-test-missing.yml new file mode 100644 index 0000000..90234bc --- /dev/null +++ b/.github/workflows/regression-test-missing.yml @@ -0,0 +1,42 @@ +name: OpenCode PR Regression Test Missing + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + regression-test-missing: + name: Regression Test Missing + if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: write + issues: write + steps: + - name: Checkout PR head + uses: actions/checkout@v6 + with: + clean: true + persist-credentials: true + token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.ref || github.event.pull_request.head.sha }} + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + # Configure git identity as defense-in-depth; some tools may implicitly + # invoke git requiring user identity even though the permission policy + # denies write operations. + + - name: Run regression test missing audit + uses: ./regression-test-missing + with: + model: ${{ vars.MODEL_NAME }} + github-token: ${{ secrets.GITHUB_TOKEN }} + zhipu-api-key: ${{ secrets.ZHIPU_API_KEY }} + deepseek-api-key: ${{ secrets.DEEPSEEK_API_KEY }} + opencode-go-api-key: ${{ secrets.OPENCODE_GO_API_KEY }} diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 10d388d..30684b8 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -36,3 +36,21 @@ jobs: attempts: "1" github-token: ${{ secrets.GITHUB_TOKEN }} continue-on-error: true + + - name: Test regression-test-missing action loads + uses: ./regression-test-missing + with: + model: test-model + timeout-seconds: "5" + attempts: "1" + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Test pr-checks action loads + uses: ./pr-checks + 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 b848ca4..6bb8cf3 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 +- `regression-test-missing`: detects PRs that fix bugs or modify behavior but lack regression tests - `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 @@ -118,19 +119,34 @@ Unlike `feature-missing` (which checks PR self-described scope), `spec-coverage` ```yaml - name: Run spec coverage audit uses: Svtter/opencode-actions/spec-coverage@v2 +``` + +## regression-test-missing + +Use this alongside `review` and `feature-missing` to detect PRs that fix bugs or modify existing behavior but lack regression tests. + +- classifies the PR as BUGFIX, BEHAVIOR_CHANGE, NEW_FEATURE, or CHORE +- only flags missing tests for BUGFIX and BEHAVIOR_CHANGE PRs +- suggests specific test cases that would catch regressions +- shares the same inputs and cache as `review`/`feature-missing` + +```yaml +- name: Run regression test missing audit + uses: Svtter/opencode-actions/regression-test-missing@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 +### 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 | +| `regression-test-missing` | PR diff + classification | Missing regression tests for bug fixes and behavior changes | ## setup-opencode @@ -185,6 +201,8 @@ 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/regression-test-missing@v2 +uses: Svtter/opencode-actions/pr-checks@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 +237,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: ./regression-test-missing`, and `uses: ./pr-checks` ## Release Policy @@ -234,7 +252,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 PR scope audit, `owner/repo/spec-coverage@v2` for spec coverage audit, `owner/repo/regression-test-missing@v2` for regression test audit, `owner/repo/pr-checks@v2` for combined PR checks, `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`. diff --git a/github-run-opencode/run-github-opencode.py b/github-run-opencode/run-github-opencode.py index d12166d..abb9d46 100755 --- a/github-run-opencode/run-github-opencode.py +++ b/github-run-opencode/run-github-opencode.py @@ -219,6 +219,9 @@ def main() -> int: os.environ[key] = value reasoning_effort = get_env("GITHUB_RUN_OPENCODE_REASONING_EFFORT", "") + if reasoning_effort and reasoning_effort not in ("low", "medium", "high", "max"): + print(f"reasoning-effort must be one of low, medium, high, max; got '{reasoning_effort}'", file=sys.stderr) + sys.exit(1) enable_thinking = get_env("GITHUB_RUN_OPENCODE_ENABLE_THINKING", "false") working_directory = get_env("GITHUB_RUN_OPENCODE_WORKING_DIRECTORY", "") @@ -230,6 +233,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("GITHUB_RUN_OPENCODE_PERMISSION must be a JSON object", file=sys.stderr) + sys.exit(1) needs_config = reasoning_effort or enable_thinking.lower() == "true" or permission if needs_config: diff --git a/pr-checks/action.yml b/pr-checks/action.yml new file mode 100644 index 0000000..1f3a91e --- /dev/null +++ b/pr-checks/action.yml @@ -0,0 +1,271 @@ +name: OpenCode PR Checks +description: Runs review, feature-missing, and regression-test-missing checks in a single opencode invocation with a combined prompt. + +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). 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 + 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" + 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: "" + prompt: + description: Value exported as PROMPT before running `opencode github run`. Defaults to a combined prompt that runs all three checks. + required: false + default: | + Run all three PR checks on this pull request (read-only mode, DO NOT modify any code). + + Execute these checks in order and combine the results: + + ## Check 1: Code Review + Review the code for: + - Code quality issues + - Potential bugs or logic errors + - Code style consistency + - Security concerns + - Performance issues + + ## Check 2: Feature Missing + 1. Find linked issues via `gh pr view --json closingIssuesReferences,title,body`. + 2. If linked issues exist, read each issue body as the feature spec. + 3. If no linked issues, extract requirements from the PR title and body. + 4. Compare the spec/requirements against the implementation. + Focus on: missing features, partial implementations, integration gaps, missing edge case handling. + Do NOT report: code style issues, bugs in existing code, suggestions beyond the spec. + + ## Check 3: Regression Test Missing + 1. Determine PR type: BUGFIX / BEHAVIOR_CHANGE / NEW_FEATURE / CHORE. + 2. For BUGFIX and BEHAVIOR_CHANGE PRs, check if regression tests exist. + 3. For NEW_FEATURE and CHORE PRs, no regression tests needed. + Focus on: bug fixes without reproducers, behavior changes without coverage, missing edge case tests. + Do NOT report: code style, missing features from spec, test suggestions for trivial changes. + + Please respond in Chinese. DO NOT modify any code, only provide analysis. + + Output format - three sections: + + ### 代码审查 + - Decision: 可合并 / 有条件合并 / 不可合并 + - Summary + - 阻塞项 (blocking issues, or "阻塞项:无") + - 建议项 (suggestions, or "建议项:无") + + ### 功能遗漏 + - Decision: 无遗漏 / 发现遗漏 + - PR type detected + - If gaps found, list each by severity (CRITICAL / MEDIUM / LOW) + + ### 回归测试 + - Decision: 无需回归测试 / 缺少回归测试 + - PR type detected + - If tests missing, list each gap with suggested test case description and severity (CRITICAL / MEDIUM / 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: "" + +runs: + using: composite + steps: + - if: ${{ runner.os != 'Linux' }} + shell: bash + run: | + set -euo pipefail + printf 'pr-checks 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: pr-checks-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_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_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_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: | + 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 diff --git a/regression-test-missing/action.yml b/regression-test-missing/action.yml new file mode 100644 index 0000000..fbb6bb5 --- /dev/null +++ b/regression-test-missing/action.yml @@ -0,0 +1,268 @@ +name: OpenCode Regression Test Missing +description: Detects when a PR fixes bugs or modifies behavior but lacks corresponding regression tests. + +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 + 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" + 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: "" + prompt: + description: Value exported as PROMPT before running `opencode github run`. + required: false + default: | + Audit this pull request for missing regression tests (read-only mode, DO NOT modify any code). + + Steps: + 1. Read the PR title, body, and labels to understand the intent (bug fix, refactor, behavior change, or pure new feature). + 2. If there are linked issues, read them via `gh issue view` to understand the original problem. + 3. Read the PR's changed files to understand what was modified. + 4. Check whether test files were included in the PR changes (look for files in test directories, files with test/spec in the name, or new test cases added to existing test files). + 5. For each bug fix or behavior change found, evaluate whether a regression test exists that would catch the same issue if it were reintroduced. + + Determine the PR type first: + - BUGFIX: The PR description or linked issues indicate a bug is being fixed (crashes, incorrect output, missing error handling, etc.) + - BEHAVIOR_CHANGE: The PR modifies existing behavior, changes APIs, or refactors core logic that could break existing functionality + - NEW_FEATURE: The PR only adds new functionality without modifying existing behavior + - CHORE: The PR only touches documentation, config files, or formatting + + Only flag missing tests for BUGFIX and BEHAVIOR_CHANGE PR types. + For NEW_FEATURE and CHORE PRs, respond with "无需回归测试" and explain why. + + Focus on finding: + - Bug fixes without a reproducer or regression test that verifies the fix + - Behavior changes without tests covering the old behavior being changed + - Error handling added without a test that triggers the error condition + - Edge case fixes without tests exercising those edge cases + - API changes without tests verifying backward compatibility (if applicable) + + Do NOT report: + - Code style, naming, or formatting issues (that's the review action's job) + - Missing features from the spec (that's the feature-missing action's job) + - Test coverage suggestions for brand new features with no existing behavior change + - Suggestions to add tests for trivially correct changes (e.g., typo fixes in comments) + + Please respond in Chinese. DO NOT modify any code, only provide analysis. + + Output format: + - First line: one of "无需回归测试" (no regression tests needed) or "缺少回归测试" (regression tests missing) + - Then state the detected PR type (BUGFIX / BEHAVIOR_CHANGE / NEW_FEATURE / CHORE) + - Then a short summary of the analysis + - If regression tests are missing, list each gap with: + - What bug/behavior change was made + - Why a regression test is needed + - Suggested test case description (what it should verify, not the actual code) + - Severity: CRITICAL (data loss, security, crash without test) / MEDIUM (incorrect behavior without test) / LOW (minor edge case without test) + - If no regression tests are needed, briefly explain why (e.g., "纯新增功能,未修改现有行为") + 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: "" + +runs: + using: composite + steps: + - if: ${{ runner.os != 'Linux' }} + shell: bash + run: | + set -euo pipefail + printf 'regression-test-missing 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: regression-test-missing-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_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 }} + GITHUB_RUN_OPENCODE_EXTRA_ENV: ${{ inputs.extra-env }} + 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_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 + exit 1 + fi + python3 ${{ github.action_path }}/../github-run-opencode/run-github-opencode.py