diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..3bd41f4 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,6 @@ +--- +paths: + .github/workflows/opencode-*.yml: + ignore: + # actionlint does not yet recognize GitHub's $/ self repository reference. + - 'specifying action "\$/\." in invalid format because ref is missing' diff --git a/.github/workflows/opencode-bot.yml b/.github/workflows/opencode-bot.yml index b77f79e..ad84a60 100644 --- a/.github/workflows/opencode-bot.yml +++ b/.github/workflows/opencode-bot.yml @@ -99,6 +99,12 @@ on: GH_TOKEN: required: false description: GitHub token for repository access + issue_comment: + types: + - created + pull_request_review_comment: + types: + - created permissions: contents: read pull-requests: write @@ -111,12 +117,19 @@ jobs: ( (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) + && ( + inputs.model != '' + || startsWith(github.event.comment.body, '/oc') + || contains(github.event.comment.body, ' /oc') + || startsWith(github.event.comment.body, '/opencode') + || contains(github.event.comment.body, ' /opencode') + ) ) || ( github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' && inputs.prompt != '' ) - runs-on: ${{ inputs.runs-on }} + runs-on: ${{ inputs.runs-on || 'ubuntu-latest' }} steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -125,7 +138,7 @@ jobs: token: ${{ secrets.GH_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] caller-provided secret persist-credentials: false - name: Run OpenCode - uses: dceoy/opencode-action@v0 # zizmor: ignore[unpinned-uses] maintained by this repository's major release tag + uses: $/. env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -140,14 +153,14 @@ jobs: MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} GITHUB_TOKEN: ${{ secrets.GH_TOKEN || github.token }} with: - model: ${{ inputs.model }} - agent: ${{ inputs.agent }} + model: ${{ inputs.model || 'sakura/preview/Kimi-K2.7-Code' }} + agent: ${{ inputs.agent || 'build' }} share: ${{ inputs.share }} prompt: ${{ inputs.prompt }} use-github-token: ${{ inputs.use-github-token }} - mentions: ${{ inputs.mentions }} + mentions: ${{ inputs.mentions || '/opencode,/oc' }} variant: ${{ inputs.variant }} - oidc-base-url: ${{ inputs.oidc-base-url }} - opencode-version: ${{ inputs.opencode-version }} - use-bundled-toolkit: ${{ inputs.use-bundled-toolkit }} - timeout-minutes: ${{ inputs.timeout-minutes }} + oidc-base-url: ${{ inputs.oidc-base-url || 'https://api.opencode.ai' }} + opencode-version: ${{ inputs.opencode-version || 'latest' }} + use-bundled-toolkit: ${{ inputs.model == '' || inputs.use-bundled-toolkit }} + timeout-minutes: ${{ inputs.timeout-minutes || 60 }} diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index a91a42a..629dd60 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -99,6 +99,10 @@ on: GH_TOKEN: required: false description: GitHub token for repository access + pull_request: + types: + - opened + - ready_for_review permissions: contents: read pull-requests: write @@ -107,8 +111,18 @@ permissions: actions: read jobs: opencode-review: - if: github.event_name == 'pull_request' - runs-on: ${{ inputs.runs-on }} + if: > + github.event_name == 'pull_request' + && ( + inputs.model != '' + || ( + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) + && (! github.event.pull_request.draft) + && (! startsWith(github.head_ref, 'dependabot/')) + && (! startsWith(github.head_ref, 'renovate/')) + ) + ) + runs-on: ${{ inputs.runs-on || 'ubuntu-latest' }} steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -117,7 +131,7 @@ jobs: token: ${{ secrets.GH_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] caller-provided secret persist-credentials: false - name: Run OpenCode - uses: dceoy/opencode-action@v0 # zizmor: ignore[unpinned-uses] maintained by this repository's major release tag + uses: $/. env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -132,14 +146,14 @@ jobs: MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} GITHUB_TOKEN: ${{ secrets.GH_TOKEN || github.token }} with: - model: ${{ inputs.model }} - agent: ${{ inputs.agent }} + model: ${{ inputs.model || 'sakura/preview/Kimi-K2.7-Code' }} + agent: ${{ inputs.agent || 'build' }} share: ${{ inputs.share }} - prompt: ${{ inputs.prompt }} + prompt: ${{ inputs.prompt || '/review-pr' }} use-github-token: ${{ inputs.use-github-token }} - mentions: ${{ inputs.mentions }} + mentions: ${{ inputs.mentions || '/opencode,/oc' }} variant: ${{ inputs.variant }} - oidc-base-url: ${{ inputs.oidc-base-url }} - opencode-version: ${{ inputs.opencode-version }} - use-bundled-toolkit: ${{ inputs.use-bundled-toolkit }} - timeout-minutes: ${{ inputs.timeout-minutes }} + oidc-base-url: ${{ inputs.oidc-base-url || 'https://api.opencode.ai' }} + opencode-version: ${{ inputs.opencode-version || 'latest' }} + use-bundled-toolkit: ${{ inputs.model == '' || inputs.use-bundled-toolkit }} + timeout-minutes: ${{ inputs.timeout-minutes || 60 }} diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml deleted file mode 100644 index 2b82a7d..0000000 --- a/.github/workflows/opencode.yml +++ /dev/null @@ -1,77 +0,0 @@ ---- -name: Pull request review and mention bot using OpenCode -on: - pull_request: - types: - - opened - - ready_for_review - issue_comment: - types: - - created - pull_request_review_comment: - types: - - created -permissions: - contents: read -jobs: - opencode-review: - if: > - github.event_name == 'pull_request' - && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) - && (! github.event.pull_request.draft) - && (! startsWith(github.head_ref, 'dependabot/')) - && (! startsWith(github.head_ref, 'renovate/')) - permissions: - contents: read - pull-requests: write - issues: write - id-token: write - actions: read - uses: ./.github/workflows/opencode-review.yml - with: - model: sakura/preview/Kimi-K2.7-Code - secrets: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - SAKURA_AI_ENGINE_API_KEY: ${{ secrets.SAKURA_AI_ENGINE_API_KEY }} - GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }} - DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} - XAI_API_KEY: ${{ secrets.XAI_API_KEY }} - GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} - CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} - MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} - opencode-bot: - if: > - (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) - && ( - startsWith(github.event.comment.body, '/oc') - || contains(github.event.comment.body, ' /oc') - || startsWith(github.event.comment.body, '/opencode') - || contains(github.event.comment.body, ' /opencode') - ) - permissions: - contents: read - pull-requests: write - issues: write - id-token: write - actions: read - uses: ./.github/workflows/opencode-bot.yml - with: - model: sakura/preview/Kimi-K2.7-Code - secrets: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - SAKURA_AI_ENGINE_API_KEY: ${{ secrets.SAKURA_AI_ENGINE_API_KEY }} - GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }} - DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} - XAI_API_KEY: ${{ secrets.XAI_API_KEY }} - GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} - CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} - MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index f1b722d..da7d7c0 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -16,7 +16,7 @@ jobs: permissions: contents: write uses: dceoy/gha-for-devops/.github/workflows/github-major-version-tag.yml@main # zizmor: ignore[unpinned-uses] - readme: + release-pins: permissions: contents: write pull-requests: write @@ -44,25 +44,55 @@ jobs: fi fi tee -a "${GITHUB_OUTPUT}" <<< "tag=${tag}" - - name: Update README + - name: Set actions/checkout release + id: checkout + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + tag="$(gh release view --repo actions/checkout --json tagName --jq '.tagName')" + sha="$(gh api "repos/actions/checkout/commits/${tag}" --jq '.sha')" + if [[ -z "${tag}" || ! "${sha}" =~ ^[0-9a-f]{40}$ ]]; then + echo 'Failed to resolve the latest actions/checkout release commit.' >&2 + exit 1 + fi + tee -a "${GITHUB_OUTPUT}" <<< "tag=${tag}" + tee -a "${GITHUB_OUTPUT}" <<< "sha=${sha}" + - name: Update release pins env: RELEASE_TAG: ${{ steps.release.outputs.tag }} + CHECKOUT_TAG: ${{ steps.checkout.outputs.tag }} + CHECKOUT_SHA: ${{ steps.checkout.outputs.sha }} run: | - match_count="$(grep -Ec 'uses: dceoy/opencode-action@[0-9a-f]{40}' README.md || true)" - if [[ "${match_count}" != 1 ]]; then - echo "Expected exactly one pinned dceoy/opencode-action reference in README.md, found ${match_count}." >&2 + release_sha="$(git rev-parse "${RELEASE_TAG}^{commit}")" + + readme_action_match_count="$(grep -Ec 'uses: dceoy/opencode-action@[0-9a-f]{40}' README.md || true)" + if [[ "${readme_action_match_count}" != 1 ]]; then + echo "Expected exactly one pinned dceoy/opencode-action reference in README.md, found ${readme_action_match_count}." >&2 + exit 1 + fi + + readme_checkout_match_count="$(grep -Ec 'uses: actions/checkout@[0-9a-f]{40}' README.md || true)" + if [[ "${readme_checkout_match_count}" != 1 ]]; then + echo "Expected exactly one pinned actions/checkout reference in README.md, found ${readme_checkout_match_count}." >&2 + exit 1 + fi + + workflow_match_count="$(grep -Ec 'uses: dceoy/opencode-action/\.github/workflows/opencode-(bot|review)\.yml@[0-9a-f]{40}' docs/reusable-workflows.md || true)" + if [[ "${workflow_match_count}" != 2 ]]; then + echo "Expected exactly two pinned reusable workflow references in docs/reusable-workflows.md, found ${workflow_match_count}." >&2 exit 1 - else - release_sha="$(git rev-parse "${RELEASE_TAG}^{commit}")" - sed -iE "s|(uses: dceoy/opencode-action@)[0-9a-f]{40}( +#.*)?|\1${release_sha} # ${RELEASE_TAG}|" README.md fi + + sed -E -i "s|(uses: dceoy/opencode-action@)[0-9a-f]{40}( +#.*)?|\1${release_sha} # ${RELEASE_TAG}|" README.md + sed -E -i "s|(uses: actions/checkout@)[0-9a-f]{40}( +#.*)?|\1${CHECKOUT_SHA} # ${CHECKOUT_TAG}|" README.md + sed -E -i "s#(uses: dceoy/opencode-action/\.github/workflows/opencode-(bot|review)\.yml@)[0-9a-f]{40}#\1${release_sha}#g" docs/reusable-workflows.md - name: Create pull request uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} # zizmor: ignore[secrets-outside-env] caller-provided secret - commit-message: Update README for ${{ steps.release.outputs.tag }} - title: Update README for ${{ steps.release.outputs.tag }} - branch: github-actions/readme + commit-message: Update release pins for ${{ steps.release.outputs.tag }} + title: Update release pins for ${{ steps.release.outputs.tag }} + branch: github-actions/release-pins base: ${{ github.event.repository.default_branch }} - body: Update the README action pin for ${{ steps.release.outputs.tag }}. + body: Update the README action and actions/checkout pins plus reusable workflow documentation pins for ${{ steps.release.outputs.tag }}. delete-branch: true diff --git a/docs/reusable-workflows.md b/docs/reusable-workflows.md index ad9b785..b50af84 100644 --- a/docs/reusable-workflows.md +++ b/docs/reusable-workflows.md @@ -2,7 +2,7 @@ `opencode-action` publishes two reusable GitHub Actions workflows under `.github/workflows`. Call them as jobs with `uses`, then pass action configuration through `with` and provider credentials through `secrets`. -The examples below pin the reusable workflow definition to the commit that introduced these workflows. The called workflow currently invokes `dceoy/opencode-action@v0` internally, so this fixes the workflow definition without making the nested action reference immutable. +The examples below pin the reusable workflow definition to a full commit SHA. Inside the called workflow, `uses: $/.` references the action at the repository root from the same repository and running commit, so the workflow reference also pins the action implementation without a second checkout or a separate action revision input. ## Mention bot @@ -25,7 +25,7 @@ jobs: pull-requests: write id-token: write actions: read - uses: dceoy/opencode-action/.github/workflows/opencode-bot.yml@7c392aad14ab1281630ae0c93e81d727f76b3e92 + uses: dceoy/opencode-action/.github/workflows/opencode-bot.yml@de968892b27dd42727fa175cd00610a54d091bf9 with: model: opencode-go/kimi-k3 secrets: @@ -53,7 +53,7 @@ jobs: pull-requests: write id-token: write actions: read - uses: dceoy/opencode-action/.github/workflows/opencode-review.yml@7c392aad14ab1281630ae0c93e81d727f76b3e92 + uses: dceoy/opencode-action/.github/workflows/opencode-review.yml@de968892b27dd42727fa175cd00610a54d091bf9 with: model: openrouter/openrouter/free secrets: @@ -83,14 +83,16 @@ Both reusable workflows expose the action configuration plus a runner input: | `timeout-minutes` | `60` | Maximum OpenCode runtime in minutes. | | `runs-on` | `ubuntu-latest` | Runner label for the called job. | +GitHub.com's `$/path` self repository syntax resolves to the repository and commit of the workflow where it appears, including when that workflow is called from another repository. These workflows use `$/.` because the action is defined at the repository root. GitHub Enterprise Server does not support this syntax. + ## Secrets -Pass only the provider secret needed by the selected model. The reusable workflows accept `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `OPENCODE_API_KEY`, and `SAKURA_AI_ENGINE_API_KEY`. +Pass only the provider secret needed by the selected model. The reusable workflows accept `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `OPENCODE_API_KEY`, `SAKURA_AI_ENGINE_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `DEEPSEEK_API_KEY`, `XAI_API_KEY`, `GROQ_API_KEY`, `CEREBRAS_API_KEY`, and `MOONSHOT_API_KEY`. -`GH_TOKEN` is optional. When omitted, the reusable workflow falls back to the caller's `github.token`. If `use-github-token: true`, ensure the caller grants the permissions required by the requested operation. +`GH_TOKEN` is optional. When omitted, the reusable workflow falls back to the caller's `github.token`. With `use-github-token: true`, that fallback is limited to `contents: read` by the called workflow even if the caller grants `contents: write`. For code-writing operations such as `/oc fix this`, pass a separately write-scoped `GH_TOKEN`; otherwise GitHub API writes to repository contents fail with `403`. ## Permissions -The reusable workflows request `contents: read`, `pull-requests: write`, `issues: write`, `id-token: write`, and `actions: read`. A called workflow cannot elevate the `GITHUB_TOKEN` permissions granted by its caller, so the calling job must grant the permissions needed by the selected mode. +The reusable workflows request `contents: read`, `pull-requests: write`, `issues: write`, `id-token: write`, and `actions: read`. A called workflow can only maintain or reduce the caller's `GITHUB_TOKEN` permissions: the caller must grant the requested permissions, but its higher `contents` permission cannot override the called workflow's `contents: read` ceiling. A separately supplied `GH_TOKEN` is not governed by that `GITHUB_TOKEN` permission ceiling. The examples keep `permissions`, `with`, and `secrets` under the calling job so their scopes are explicit: `permissions` controls the caller token, `with` configures the reusable workflow inputs, and `secrets` passes credentials.