mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
feat(action): add self-serve GitHub review bot setup (CW-6869) [skip ci]
Generate a workflow with immutable Action refs, exact release/model selection, separate read-only analysis and credential-isolated publication, and optional existing GitHub App identity. Reuse the Engine review command and installer. Accept exact review mentions from current write/maintain/admin users; recheck original and rerunning actors, comment identity/body and current revisions. Opt-in inert fork diffs only through trusted default-branch manual or issue comment triggers. Mentions require the separate publisher. No candidate code, hooks or project configuration is executed. Share provider-key metadata with the generator and redact every supported key, including OrcaRouter. Local verification: existing node review tests 8 passed, 0 failed; Node syntax checks and actionlint pass for default-token and App workflows. Direct offline execution completed mention -> findings -> inline publisher, refused 10 unsafe request cases, refused fork analysis without opt-in and cross-attempt findings, and confirmed provider-key isolation/redaction. No new tests or code comments. Live GitHub/provider acceptance follows separately; no release or moving tag.
This commit is contained in:
1 parent
cce6d3a84d
commit
c962177c79
10 files changed
+389
-159
No files matched your search
@@ -203,7 +203,7 @@ and operate other apps. It stays off until you review its access and enable it
|
||||
| `codewhale` | The interactive terminal app |
|
||||
| `codewhale exec "…"` | One task without the interface. On its own it answers without using tools; `--auto` enables tools with Full Access by default, `--sandbox workspace-write` restricts them, and `--output-format stream-json` streams events as JSON |
|
||||
| `codewhale web` | A local web UI on `127.0.0.1` ([guide](docs/WEB.md)) |
|
||||
| `codewhale review --pr N` | An advisory review of a GitHub pull request. It posts nothing unless you add `--post`; the [GitHub Action](docs/GITHUB_ACTION.md) runs the same review in CI |
|
||||
| `codewhale review --pr N` | An advisory review of a GitHub pull request. The [GitHub bot setup](docs/GITHUB_ACTION.md#set-up-your-own-bot) generates a pinned workflow for automatic reviews and `@codewhale review`, using your own model key and optional App identity |
|
||||
| `codewhale app-server --http` | The [Runtime API](docs/RUNTIME_API.md), a local HTTP API for threads, events, approvals, and each turn's file changes |
|
||||
| `codewhale serve --acp` | The Agent Client Protocol over stdio, for editors such as Zed |
|
||||
|
||||
|
||||
+13
-2
@@ -1,5 +1,8 @@
|
||||
name: Codewhale review
|
||||
description: Review a pinned pull request with a checksummed Codewhale release and an explicit outcome receipt.
|
||||
name: Codewhale
|
||||
description: Review pull requests automatically or on an authorized @codewhale review request, using your chosen model.
|
||||
branding:
|
||||
icon: code
|
||||
color: blue
|
||||
inputs:
|
||||
version:
|
||||
description: Exact released CLI tag (vX.Y.Z). Must support review --json.
|
||||
@@ -15,6 +18,12 @@ inputs:
|
||||
default: ${{ github.token }}
|
||||
pr-number:
|
||||
description: PR number for workflow_dispatch; pull_request events use the event snapshot.
|
||||
trigger-phrase:
|
||||
description: Bot mention used for a standalone @name review line in a new PR comment.
|
||||
default: '@codewhale'
|
||||
allow-forks:
|
||||
description: Allow inert fork review only on a write-authorized issue_comment or manual run from the default branch.
|
||||
default: 'false'
|
||||
max-chars:
|
||||
description: Maximum diff characters per complete pass.
|
||||
default: '200000'
|
||||
@@ -53,6 +62,8 @@ runs:
|
||||
CW_PROVIDER: ${{ inputs.provider }}
|
||||
CW_MODEL: ${{ inputs.model }}
|
||||
CW_PR_NUMBER: ${{ inputs.pr-number }}
|
||||
CW_TRIGGER_PHRASE: ${{ inputs.trigger-phrase }}
|
||||
CW_ALLOW_FORKS: ${{ inputs.allow-forks }}
|
||||
CW_MAX_CHARS: ${{ inputs.max-chars }}
|
||||
CW_MAX_PASSES: ${{ inputs.max-passes }}
|
||||
CW_MAX_OUTPUT_TOKENS: ${{ inputs.max-output-tokens }}
|
||||
|
||||
+98
-123
@@ -1,4 +1,4 @@
|
||||
# GitHub PR reviews
|
||||
# GitHub review bot
|
||||
|
||||
The root `action.yml` runs the existing `codewhale review` command with an
|
||||
exact, checksummed release. It needs Node.js 22+, `gh`, Git, and a macOS or
|
||||
@@ -11,11 +11,77 @@ exports a validated findings artifact. The publisher validates its repository,
|
||||
PR, workflow run and attempt, and current base/head before submitting a
|
||||
`COMMENT` review. It never approves, requests changes, commits, or runs a model.
|
||||
|
||||
Review mode only. Autonomous mention/fix mode is not implemented in this
|
||||
Action. The original direct-review Action has been in release tags since
|
||||
`v0.10.1`; the findings output and separate publisher require a commit containing
|
||||
both features. The Action ref and the exact released CLI `version` are separate
|
||||
pins. Adding these source files does not enable a workflow.
|
||||
Automatic reviews and `@codewhale review` comments use the same review path.
|
||||
The bot posts an advisory summary and validated inline findings. It never
|
||||
approves or requests changes. Coding tasks that push a branch or open a PR
|
||||
remain part of the hosted agent; this Action handles code review.
|
||||
|
||||
## Set up your own bot
|
||||
|
||||
You need a GitHub repository with Actions enabled and one model API key.
|
||||
Node.js 22+ and Git are needed only to generate the workflow locally; GitHub's
|
||||
Ubuntu runner provides the review tools. No server or GitHub App is required.
|
||||
|
||||
1. From a clean Codewhale source checkout, generate a workflow in your project:
|
||||
|
||||
```sh
|
||||
node /path/to/Codewhale/scripts/github-action/setup.mjs \
|
||||
--directory /path/to/your-project \
|
||||
--provider deepseek --model deepseek-flash
|
||||
```
|
||||
|
||||
Select your own provider and exact model ID. The generator pins both
|
||||
Codewhale actions to the source checkout's full commit and the CLI to
|
||||
`v0.10.2`. It refuses to overwrite an existing workflow. Use `--print` to
|
||||
inspect the YAML, `--version vX.Y.Z` to choose another released CLI, or
|
||||
`--action-ref FULL_COMMIT_SHA` with a separately downloaded `setup.mjs`.
|
||||
The generated workflow is `.github/workflows/codewhale-review.yml`.
|
||||
|
||||
2. Store only the selected provider's API key as a repository Actions secret:
|
||||
|
||||
```sh
|
||||
gh secret set DEEPSEEK_API_KEY --repo OWNER/REPO
|
||||
```
|
||||
|
||||
This command prompts for the key. DeepSeek is the example; the provider
|
||||
table below lists the other supported secrets. Never put a key in YAML.
|
||||
|
||||
3. Commit the workflow to your repository's default branch and push it.
|
||||
Same-repository PRs are reviewed automatically when opened or updated.
|
||||
For any open, non-draft PR, a maintainer can add exactly this comment,
|
||||
or choose **Actions → Codewhale PR Review → Run workflow**
|
||||
and enter its number:
|
||||
|
||||
```text
|
||||
@codewhale review
|
||||
```
|
||||
|
||||
The generated workflow uses `github-actions[bot]`. To use your own existing
|
||||
App, add `--app-login 'YOUR-APP[bot]'` when generating, set the
|
||||
`CODEWHALE_APP_ID` variable and `CODEWHALE_APP_PRIVATE_KEY` secret, and install
|
||||
the App on the repository. The publisher requests only contents read and pull
|
||||
requests write. A missing App configuration fails; it does not switch identity.
|
||||
See [GitHub App setup](GITHUB_APP.md) for creating a new App.
|
||||
|
||||
`--trigger-phrase @my-bot` changes the mention to `@my-bot review`. A mention
|
||||
is an exact standalone comment; quoted text, arbitrary instructions, issue-only
|
||||
comments, edited comments and bot authors do not trigger inference. New PR
|
||||
review comments also work for same-repository PRs. Both the original actor
|
||||
and an actor rerunning the workflow must still have write, maintain or admin
|
||||
access when GitHub is queried. Association labels alone grant no access.
|
||||
A deleted, edited or mismatched triggering comment is rejected.
|
||||
|
||||
Fork PRs do not receive automatic model reviews. The generated workflow opts
|
||||
into reviewing their inert diffs only through an authorized issue comment or
|
||||
manual run from the default branch. It reads pinned Git objects without
|
||||
checking out PR files, reading project configuration or executing candidate
|
||||
code. The provider key belongs to that trusted job; no fork workflow receives
|
||||
it. Leave `allow-forks: false` on both actions to disable this opt-in.
|
||||
|
||||
The Action ref and exact CLI release are separate pins. Existing release tags
|
||||
contain the original review action; mention support and the generator require
|
||||
a commit containing these features. A moving major tag is not published by
|
||||
this setup. Source installation does not prove a live provider review.
|
||||
|
||||
## Account configuration
|
||||
|
||||
@@ -31,124 +97,31 @@ below.
|
||||
|
||||
## Use your own provider key
|
||||
|
||||
Pick `orcarouter`, `deepseek`, `anthropic`, `openrouter`, `zai`, or `modelstudio-token-plan`,
|
||||
an exact model ID, and a released CLI tag. Store the matching provider key as
|
||||
an Actions secret. Pass only that provider's key to the analysis job. The
|
||||
Action never chooses a different credential after an authentication or payment
|
||||
failure. The example illustrates an explicitly selected OrcaRouter connection;
|
||||
it does not set the Codewhale account default. Replace its provider and secret
|
||||
together when choosing another route.
|
||||
Choose a provider and exact model, then pass only its key to the analysis job:
|
||||
|
||||
Create or connect your own [OrcaRouter account](https://www.orcarouter.ai/ref/ref_df843907ad10d887b867),
|
||||
then store its API key as `ORCAROUTER_API_KEY`. Codewhale earns a referral fee
|
||||
from OrcaRouter. `orcarouter/free` is an available starting model when listed
|
||||
for your workspace; it still requires an API key. A direct no-key request on
|
||||
2026-10-26 returned HTTP 401. OrcaRouter's [free-route documentation](https://www.orcarouter.ai/models/orcarouter/free)
|
||||
describes zero-priced usage with request-rate limits. A limit or authentication
|
||||
failure does not authorize a paid fallback. GitHub runner compute has its own
|
||||
cost and limits.
|
||||
| Provider | Actions secret |
|
||||
| --- | --- |
|
||||
| `deepseek` | `DEEPSEEK_API_KEY` |
|
||||
| `anthropic` | `ANTHROPIC_API_KEY` |
|
||||
| `openrouter` | `OPENROUTER_API_KEY` |
|
||||
| `orcarouter` | `ORCAROUTER_API_KEY` |
|
||||
| `zai` | `ZAI_API_KEY` |
|
||||
| `modelstudio-token-plan` | `MODELSTUDIO_API_KEY` |
|
||||
| `codewhale` | `CODEWHALE_API_KEY`, with a separately verified account machine-key route |
|
||||
|
||||
Set `CODEWHALE_REVIEW_VERSION` to an exact `vX.Y.Z` release and
|
||||
`CODEWHALE_REVIEW_MODEL` to the chosen provider's exact model ID (for example
|
||||
`orcarouter/free`). Replace both
|
||||
`REPLACE_WITH_REVIEWED_COMMIT_SHA` placeholders with the same reviewed Action
|
||||
commit containing the publisher. Pin third-party actions to reviewed commits
|
||||
under your repository's Actions policy.
|
||||
The Action never switches credentials after an authentication, rate-limit or
|
||||
payment failure. OrcaRouter is optional; its advertised free route still needs
|
||||
a key and has provider limits. GitHub runner usage has its own cost and limits.
|
||||
|
||||
```yaml
|
||||
name: Codewhale review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr-number:
|
||||
description: Same-repository PR number
|
||||
required: true
|
||||
type: string
|
||||
permissions: {}
|
||||
concurrency:
|
||||
group: codewhale-review-${{ github.event.pull_request.number || inputs.pr-number }}
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
findings: ${{ steps.review.outputs.findings }}
|
||||
steps:
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
- uses: codewhale-hq/CodeWhale@REPLACE_WITH_REVIEWED_COMMIT_SHA
|
||||
id: review
|
||||
with:
|
||||
version: ${{ vars.CODEWHALE_REVIEW_VERSION }}
|
||||
provider: orcarouter
|
||||
model: ${{ vars.CODEWHALE_REVIEW_MODEL }}
|
||||
pr-number: ${{ inputs.pr-number }}
|
||||
post: 'false'
|
||||
env:
|
||||
ORCAROUTER_API_KEY: ${{ (github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.ORCAROUTER_API_KEY || '' }}
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: steps.review.outputs.findings != ''
|
||||
with:
|
||||
name: codewhale-findings-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.review.outputs.findings }}
|
||||
retention-days: 1
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: always() && steps.review.outputs.receipt != ''
|
||||
with:
|
||||
name: codewhale-review-receipt-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.review.outputs.receipt }}
|
||||
retention-days: 14
|
||||
publish:
|
||||
needs: review
|
||||
if: needs.review.outputs.findings != ''
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: codewhale-findings-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: findings
|
||||
- uses: codewhale-hq/CodeWhale/scripts/github-action/publish@REPLACE_WITH_REVIEWED_COMMIT_SHA
|
||||
id: publish
|
||||
with:
|
||||
findings: findings/findings.json
|
||||
pr-number: ${{ inputs.pr-number }}
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: always() && steps.publish.outputs.receipt != ''
|
||||
with:
|
||||
name: codewhale-publication-receipt-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.publish.outputs.receipt }}
|
||||
retention-days: 14
|
||||
```
|
||||
|
||||
No repository checkout is needed in either job. The publishing job must use
|
||||
trusted Action code and must not run files from the findings artifact. Keep it
|
||||
on a separate ephemeral runner; a later step on the analysis runner is not the
|
||||
same credential boundary. The analysis CLI still receives a read-only GitHub
|
||||
token for PR metadata and Git objects, alongside its selected provider key.
|
||||
|
||||
Never use `pull_request_target` with these actions. Fork PRs and drafts are
|
||||
ineligible even on a manual run. A skipped event is not a clean review. This
|
||||
publisher accepts artifacts only from the current workflow run and attempt;
|
||||
cross-workflow `workflow_run` publication is not implemented.
|
||||
|
||||
To publish as a dedicated GitHub App, create an installation token only in the
|
||||
publisher job, pass it as `github-token`, and set `reviewer-login` to that App's
|
||||
exact bot login, such as `codewhale-agent[bot]`. The publisher verifies the
|
||||
returned review author; a mismatch is an uncertain publication, not permission
|
||||
to retry. See [App identity setup](GITHUB_APP.md).
|
||||
The [workflow generator](../scripts/github-action/setup.mjs) is the complete
|
||||
copy-ready example. It uses two ephemeral jobs, immutable action references,
|
||||
read permissions for analysis, and write permission only for publication.
|
||||
Neither job checks out repository code. The publishing job must use trusted
|
||||
Action code and must not run files from the findings artifact. Do not combine
|
||||
these jobs on a persistent runner: a later step on the analysis runner is not
|
||||
a credential boundary. The publisher accepts only the current workflow run
|
||||
and attempt; cross-workflow `workflow_run` publication is not implemented.
|
||||
Never use `pull_request_target` with these actions.
|
||||
|
||||
### Account machine keys
|
||||
|
||||
@@ -169,7 +142,9 @@ your account's machine-key route has been separately verified. The Action runs
|
||||
| `max-passes` | 1 | Complete ordered passes, maximum 64 |
|
||||
| `max-output-tokens` | CLI automatic | Optional per-pass ceiling, 8192–1000000 |
|
||||
| `timeout-seconds` | 600 | Model/publication deadline, 30–1200 |
|
||||
| `post` | `true` | Legacy direct publication; set `false` for the two-job setup |
|
||||
| `post` | `true` | Legacy direct publication; generated workflows use `false` |
|
||||
| `trigger-phrase` | `@codewhale` | Exact mention name, followed by a standalone `review` command |
|
||||
| `allow-forks` | `false` | Maintainer-triggered inert fork analysis from the default branch; set the same value on the publisher |
|
||||
|
||||
These bounds do not guarantee a dollar cost. Model prices and reasoning
|
||||
accounting vary. Raising the pass count authorizes more model requests.
|
||||
@@ -224,7 +199,7 @@ review, not proof that the PR contains no bugs.
|
||||
| `incomplete` | Coverage incomplete; revise scope or limits |
|
||||
| `publication_uncertain` | Check the PR before retrying; the write may have succeeded |
|
||||
| `superseded` | Base or head changed; analyze the current revision |
|
||||
| `not_eligible` | Fork, draft, closed PR or unsupported event; no model run/write |
|
||||
| `not_eligible` | Unapproved fork, draft, closed PR, unsupported event, stale comment or missing write access; no model run/write |
|
||||
|
||||
Failures fail the optional job; whether it is a required merge check is the
|
||||
repository owner's policy. A provider failure is not a negative verdict about
|
||||
|
||||
+23
-18
@@ -21,9 +21,11 @@ Related docs:
|
||||
|
||||
Use [the reusable GitHub Action setup](GITHUB_ACTION.md) for the workflow,
|
||||
account machine key, exact model, release pin, limits, outcomes and retries.
|
||||
The repository workflow is now a thin caller of that action. It uses the
|
||||
Codewhale account relay and a checksummed release; it does not compile a PR's
|
||||
candidate source. BYOK is an explicit option in a user's own workflow.
|
||||
The repository workflow is a thin caller of the same analysis and publication
|
||||
actions. Select your own provider and model; the checksummed CLI release does
|
||||
not compile or execute a PR's candidate source. An App controls the publishing
|
||||
identity independently of the model provider. The workflow generator can use
|
||||
an existing App with `--app-login 'YOUR-APP[bot]'`.
|
||||
|
||||
## Review evidence and precision
|
||||
|
||||
@@ -82,7 +84,7 @@ same-repository pull requests can post reviews as the App.
|
||||
| Secret | `CODEWHALE_APP_PRIVATE_KEY` | the full `.pem` file contents |
|
||||
| Secret | `CODEWHALE_API_KEY` | a Codewhale machine key for this repository workflow |
|
||||
| Variable | `CODEWHALE_REVIEW_MODEL` | exact account catalog `provider/model` id (required) |
|
||||
| Variable | `CODEWHALE_REVIEW_VERSION` | exact released CLI tag; default v0.10.0 |
|
||||
| Variable | `CODEWHALE_REVIEW_VERSION` | exact released CLI tag; generator default v0.10.2 |
|
||||
|
||||
App settings control identity. Model access separately requires a review
|
||||
key and, for account mode, the catalog model. Optional budget variables are
|
||||
@@ -92,18 +94,21 @@ same-repository pull requests can post reviews as the App.
|
||||
## How the pieces connect
|
||||
|
||||
[The review workflow](../.github/workflows/codewhale-review.yml) uses
|
||||
`pull_request` and a manual `workflow_dispatch` recovery trigger. The action
|
||||
reads the PR's exact Git objects in a fresh repository without checking them
|
||||
out. Fork events receive no model key. Before any inference, the action
|
||||
rejects fork, draft and closed PRs and verifies their revisions, including
|
||||
on manual runs.
|
||||
`pull_request` and a manual `workflow_dispatch` recovery trigger. The analysis action reads the PR's exact Git objects in a fresh repository
|
||||
without checking them out. Automatic fork events receive no model key. Draft
|
||||
and closed PRs are rejected. With `allow-forks: true`, an authorized
|
||||
maintainer can review a fork diff through a PR issue comment or manual run
|
||||
from the default branch. Both analysis and publication recheck permissions,
|
||||
triggering comments and revisions.
|
||||
|
||||
When both `CODEWHALE_APP_ID` and `CODEWHALE_APP_PRIVATE_KEY` are present, the
|
||||
workflow mints a short-lived installation token restricted to contents:read
|
||||
and pull_requests:write. Otherwise it uses `github.token`. The action emits
|
||||
one COMMENT review, never approval or a request for changes. CODEOWNERS stays
|
||||
the human authority. Setup or provider failures fail the optional review job
|
||||
and save a sanitized receipt; they do not post additional status comments.
|
||||
When generated with an App login, the publisher mints a short-lived
|
||||
installation token restricted to contents:read and pull_requests:write.
|
||||
App credentials and model credentials are kept in separate jobs. The publisher
|
||||
verifies the returned author against that exact login. Without App setup,
|
||||
generated workflows use `github.token` and `github-actions[bot]`. The action
|
||||
emits one COMMENT review, never approval or a request for changes. CODEOWNERS
|
||||
stays the human authority. Setup or provider failures fail the optional review
|
||||
job and save a sanitized receipt; they do not post additional status comments.
|
||||
|
||||
The Actions-only App setup above does not describe the managed hosted App.
|
||||
Do not disable the webhook on an existing App that also serves hosted mentions.
|
||||
@@ -136,9 +141,9 @@ token (posts as the App). The `--post` flag is always opt-in.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Review posts as you, not the bot.** The variable or the private-key secret
|
||||
is missing/empty; the job silently falls back to `github.token`. Check both
|
||||
names character-for-character.
|
||||
- **Review posts as you, not the bot.** Generate the workflow with the exact App
|
||||
login, and set both App ID and private-key settings. App-enabled generated
|
||||
workflows fail on missing credentials instead of changing the author.
|
||||
- **No model review completed.** Read the outcome receipt and the
|
||||
[repair guide](GITHUB_ACTION.md#outcomes-and-recovery).
|
||||
- **Account model or provider error.** Set the provider to `codewhale` (or
|
||||
|
||||
+1
-1
@@ -871,7 +871,7 @@ owner = "crates/tui/src/project_context/budget.rs"
|
||||
[[feature]]
|
||||
id = "github-action-review"
|
||||
name = "GitHub Action review"
|
||||
summary = "Review pull requests with the Engine and publish validated findings from a separate trusted job."
|
||||
summary = "Generate a pinned GitHub review bot workflow for automatic and authorized mention reviews, with separate analysis and publication jobs."
|
||||
status = "experimental"
|
||||
since = "unreleased"
|
||||
surfaces = { tui = "preview" }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { KEYS } from './setup.mjs';
|
||||
|
||||
const SHA = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/i;
|
||||
const REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
||||
@@ -86,8 +87,7 @@ export function parseArtifact(raw) {
|
||||
}
|
||||
|
||||
export function findingsArtifact(data, receipt, env) {
|
||||
const secrets = ['GH_TOKEN', 'CODEWHALE_API_KEY', 'DEEPSEEK_API_KEY', 'ANTHROPIC_API_KEY',
|
||||
'OPENROUTER_API_KEY', 'ZAI_API_KEY', 'MODELSTUDIO_API_KEY'].map(key => env[key]).filter(Boolean);
|
||||
const secrets = ['GH_TOKEN', ...Object.values(KEYS)].map(key => env[key]).filter(Boolean);
|
||||
const clean = value => typeof value === 'string'
|
||||
? secrets.reduce((result, secret) => result.split(secret).join('[REDACTED]'), value)
|
||||
: Array.isArray(value) ? value.map(clean)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fingerprint, issueBody, parseArtifact, renderSummary, repositoryPath } from './findings.mjs';
|
||||
import { target, validateSnapshot } from './review.mjs';
|
||||
import { authorizeRequest, target, validateSnapshot } from './review.mjs';
|
||||
|
||||
function stop(outcome, reason) {
|
||||
throw Object.assign(new Error(reason), { outcome, reason });
|
||||
@@ -120,11 +120,12 @@ export function publish(env = process.env, execute = command) {
|
||||
};
|
||||
const snapshot = () => {
|
||||
const pr = api(`repos/${repo}/pulls/${number}`);
|
||||
validateSnapshot(pr, repo, event);
|
||||
validateSnapshot(pr, repo, event, env);
|
||||
if (pr.head.sha !== artifact.head_sha || pr.base.sha !== artifact.base_sha) stop('superseded', 'revision_changed');
|
||||
return pr;
|
||||
};
|
||||
const pr = snapshot();
|
||||
authorizeRequest(env, event, repo, number, api);
|
||||
const files = api(`repos/${repo}/pulls/${number}/files?per_page=100`, true);
|
||||
if (!Number.isSafeInteger(pr.changed_files) || files.length !== pr.changed_files
|
||||
|| new Set(files.map(item => item.filename)).size !== files.length
|
||||
@@ -135,6 +136,7 @@ export function publish(env = process.env, execute = command) {
|
||||
const reviews = api(`repos/${repo}/pulls/${number}/reviews?per_page=100`, true);
|
||||
const plan = planPublication(artifact, files, reviews, reviewer);
|
||||
snapshot();
|
||||
authorizeRequest(env, event, repo, number, api);
|
||||
if (!plan) receipt = { ...receipt, outcome: 'already_published', reason: '', publication: 'deduplicated' };
|
||||
else {
|
||||
const posted = api(`repos/${repo}/pulls/${number}/reviews`, false, plan);
|
||||
|
||||
@@ -9,6 +9,12 @@ inputs:
|
||||
default: ${{ github.token }}
|
||||
pr-number:
|
||||
description: PR number for workflow_dispatch; pull_request events use the event snapshot.
|
||||
trigger-phrase:
|
||||
description: Same bot mention as the analysis job.
|
||||
default: '@codewhale'
|
||||
allow-forks:
|
||||
description: Same trusted-event fork policy as the analysis job.
|
||||
default: 'false'
|
||||
reviewer-login:
|
||||
description: Exact GitHub login used by github-token; only this author's Codewhale markers deduplicate reviews.
|
||||
default: github-actions[bot]
|
||||
@@ -28,6 +34,8 @@ runs:
|
||||
CW_ACTION_PATH: ${{ github.action_path }}
|
||||
CW_FINDINGS: ${{ inputs.findings }}
|
||||
CW_PR_NUMBER: ${{ inputs.pr-number }}
|
||||
CW_TRIGGER_PHRASE: ${{ inputs.trigger-phrase }}
|
||||
CW_ALLOW_FORKS: ${{ inputs.allow-forks }}
|
||||
CW_REVIEWER_LOGIN: ${{ inputs.reviewer-login }}
|
||||
GH_TOKEN: ${{ inputs.github-token }}
|
||||
run: node "$CW_ACTION_PATH/../publish.mjs"
|
||||
@@ -1,7 +1,7 @@
|
||||
// GitHub orchestration only: the released CLI owns diff coverage, prompts,
|
||||
// model routing, anchor validation and the final pre-publication head check.
|
||||
// No repository checkout, project configuration, hooks, or PR code is executed.
|
||||
// Known limits: review only (mentions use the hosted App); no dollar-budget
|
||||
// Known limits: review only, including explicit review mentions; no dollar-budget
|
||||
// guarantee or automatic retry. Legacy direct publication has no cross-run deduplication.
|
||||
import { appendFileSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
@@ -9,15 +9,11 @@ import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { findingsArtifact } from './findings.mjs';
|
||||
import { KEYS } from './setup.mjs';
|
||||
|
||||
const SHA = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/i;
|
||||
const REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
||||
const MODEL = /^[A-Za-z0-9][A-Za-z0-9_./:@+-]{0,199}$/;
|
||||
const KEYS = {
|
||||
deepseek: 'DEEPSEEK_API_KEY', anthropic: 'ANTHROPIC_API_KEY',
|
||||
openrouter: 'OPENROUTER_API_KEY', orcarouter: 'ORCAROUTER_API_KEY', zai: 'ZAI_API_KEY',
|
||||
'modelstudio-token-plan': 'MODELSTUDIO_API_KEY',
|
||||
};
|
||||
|
||||
class Stop extends Error {
|
||||
constructor(outcome, reason) { super(reason); this.outcome = outcome; this.reason = reason; }
|
||||
@@ -41,7 +37,7 @@ export function settings(env) {
|
||||
if (provider === 'codewhale') {
|
||||
if (!env.CODEWHALE_API_KEY) stop('configuration_missing', 'account_key_required');
|
||||
if (!/^[A-Za-z0-9_.-]+\/.+/.test(model)) stop('configuration_missing', 'account_catalog_model_required');
|
||||
} else if (!KEYS[provider] || !env[KEYS[provider]]) {
|
||||
} else if (!Object.hasOwn(KEYS, provider) || !env[KEYS[provider]]) {
|
||||
stop('configuration_missing', 'provider_key_required');
|
||||
}
|
||||
if (!env.GH_TOKEN) stop('configuration_missing', 'github_token_required');
|
||||
@@ -68,6 +64,19 @@ export function target(env, event) {
|
||||
number = event.number;
|
||||
} else if (env.GITHUB_EVENT_NAME === 'workflow_dispatch') {
|
||||
number = env.CW_PR_NUMBER;
|
||||
} else if (['issue_comment', 'pull_request_review_comment'].includes(env.GITHUB_EVENT_NAME)) {
|
||||
if (event.action !== 'created') stop('not_eligible', 'unsupported_comment_action');
|
||||
if (event.comment?.user?.type !== 'User' || event.sender?.type !== 'User'
|
||||
|| event.comment.user.login !== env.GITHUB_ACTOR || event.sender.login !== env.GITHUB_ACTOR
|
||||
|| !Number.isSafeInteger(event.comment.id) || event.comment.id < 1) stop('not_eligible', 'untrusted_actor');
|
||||
const trigger = env.CW_TRIGGER_PHRASE || '@codewhale';
|
||||
if (!/^@[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(trigger)) stop('configuration_missing', 'invalid_trigger_phrase');
|
||||
if (typeof event.comment.body !== 'string'
|
||||
|| event.comment.body.trim().toLowerCase() !== `${trigger.toLowerCase()} review`) stop('not_eligible', 'review_mention_required');
|
||||
if (env.GITHUB_EVENT_NAME === 'issue_comment') {
|
||||
if (!event.issue?.pull_request) stop('not_eligible', 'pull_request_required');
|
||||
number = event.issue.number;
|
||||
} else number = event.pull_request?.number;
|
||||
} else {
|
||||
// In particular, pull_request_target must not gain an inference path.
|
||||
stop('not_eligible', 'unsupported_event');
|
||||
@@ -76,9 +85,10 @@ export function target(env, event) {
|
||||
return { repo: env.GITHUB_REPOSITORY, number: Number(number) };
|
||||
}
|
||||
|
||||
export function validateSnapshot(pr, repo, event) {
|
||||
export function validateSnapshot(pr, repo, event, env = {}) {
|
||||
if (pr?.base?.repo?.full_name !== repo) stop('configuration_missing', 'repository_mismatch');
|
||||
if (pr?.head?.repo?.full_name !== repo) stop('not_eligible', 'fork');
|
||||
if (pr?.head?.repo?.full_name !== repo && !(env.CW_ALLOW_FORKS === 'true'
|
||||
&& ['issue_comment', 'workflow_dispatch'].includes(env.GITHUB_EVENT_NAME))) stop('not_eligible', 'fork');
|
||||
if (pr.state !== 'open' || pr.draft) stop('not_eligible', 'closed_or_draft');
|
||||
if (!SHA.test(pr.head.sha) || !SHA.test(pr.base.sha)) stop('configuration_missing', 'invalid_revision');
|
||||
if (event.pull_request && (event.pull_request.head.sha !== pr.head.sha || event.pull_request.base.sha !== pr.base.sha)) {
|
||||
@@ -86,6 +96,35 @@ export function validateSnapshot(pr, repo, event) {
|
||||
}
|
||||
}
|
||||
|
||||
export function authorizeRequest(env, event, repo, number, api) {
|
||||
if (!['true', 'false'].includes(env.CW_ALLOW_FORKS || 'false')) stop('configuration_missing', 'invalid_allow_forks');
|
||||
const mention = ['issue_comment', 'pull_request_review_comment'].includes(env.GITHUB_EVENT_NAME);
|
||||
if (!mention && !(env.GITHUB_EVENT_NAME === 'workflow_dispatch' && env.CW_ALLOW_FORKS === 'true')) return;
|
||||
const actor = env.GITHUB_ACTOR;
|
||||
const rerunner = env.GITHUB_TRIGGERING_ACTOR || actor;
|
||||
if (event.sender?.type !== 'User' || event.sender.login !== actor
|
||||
|| ![actor, rerunner].every(login => /^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(login || ''))) stop('not_eligible', 'untrusted_actor');
|
||||
for (const login of new Set([actor, rerunner])) {
|
||||
const permission = api(`repos/${repo}/collaborators/${login}/permission`);
|
||||
if (permission.user?.login !== login || permission.user?.type !== 'User'
|
||||
|| !['admin', 'maintain', 'write'].includes(permission.permission)) stop('not_eligible', 'write_access_required');
|
||||
}
|
||||
if (env.CW_ALLOW_FORKS === 'true') {
|
||||
const repository = api(`repos/${repo}`);
|
||||
if (repository.full_name !== repo || !repository.default_branch
|
||||
|| env.GITHUB_REF !== `refs/heads/${repository.default_branch}`) stop('not_eligible', 'default_branch_required');
|
||||
}
|
||||
if (mention) {
|
||||
const kind = env.GITHUB_EVENT_NAME === 'issue_comment' ? 'issues' : 'pulls';
|
||||
const comment = api(`repos/${repo}/${kind}/comments/${event.comment.id}`);
|
||||
const url = env.GITHUB_EVENT_NAME === 'issue_comment' ? comment.issue_url : comment.pull_request_url;
|
||||
const expected = `https://api.github.com/repos/${repo}/${env.GITHUB_EVENT_NAME === 'issue_comment' ? 'issues' : 'pulls'}/${number}`;
|
||||
if (comment.id !== event.comment.id || comment.user?.login !== actor || comment.user?.type !== 'User'
|
||||
|| comment.body !== event.comment.body || comment.updated_at !== event.comment.updated_at
|
||||
|| url !== expected) stop('not_eligible', 'comment_changed_or_mismatched');
|
||||
}
|
||||
}
|
||||
|
||||
// Do not persist raw stderr, prompts, findings, PR titles, provider messages,
|
||||
// or arbitrary JSON fields: they can contain credentials or Actions commands.
|
||||
export function classify(result, head, post, base) {
|
||||
@@ -133,6 +172,8 @@ export function run(env = process.env, execute = command) {
|
||||
const event = JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, 'utf8'));
|
||||
const { repo, number } = target(env, event);
|
||||
const config = settings(env);
|
||||
if (config.post && (['issue_comment', 'pull_request_review_comment'].includes(env.GITHUB_EVENT_NAME)
|
||||
|| env.CW_ALLOW_FORKS === 'true')) stop('configuration_missing', 'separate_publisher_required');
|
||||
receipt = { ...receipt, repository: repo, pr_number: number, version: config.version,
|
||||
provider: config.provider, model: config.model, limits: { max_chars: config.maxChars,
|
||||
max_passes: config.maxPasses, timeout_seconds: config.timeout / 1000, max_output_tokens: config.outputTokens } };
|
||||
@@ -154,7 +195,11 @@ export function run(env = process.env, execute = command) {
|
||||
let pr;
|
||||
try { pr = JSON.parse(checked('gh', ['api', `repos/${repo}/pulls/${number}`], 'github_snapshot_unavailable')); }
|
||||
catch (error) { if (error instanceof Stop) throw error; stop('failed', 'invalid_github_response'); }
|
||||
validateSnapshot(pr, repo, event);
|
||||
validateSnapshot(pr, repo, event, env);
|
||||
authorizeRequest(env, event, repo, number, path => {
|
||||
try { return JSON.parse(checked('gh', ['api', path], 'github_authorization_unavailable')); }
|
||||
catch (error) { if (error instanceof Stop) throw error; stop('failed', 'invalid_github_response'); }
|
||||
});
|
||||
receipt.base_sha = pr.base.sha;
|
||||
receipt.head_sha = pr.head.sha;
|
||||
prUrl = `https://github.com/${repo}/pull/${number}`;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { lstatSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
export const KEYS = {
|
||||
deepseek: 'DEEPSEEK_API_KEY', anthropic: 'ANTHROPIC_API_KEY',
|
||||
openrouter: 'OPENROUTER_API_KEY', orcarouter: 'ORCAROUTER_API_KEY', zai: 'ZAI_API_KEY',
|
||||
'modelstudio-token-plan': 'MODELSTUDIO_API_KEY', codewhale: 'CODEWHALE_API_KEY',
|
||||
};
|
||||
|
||||
const HELP = `Create a Codewhale review bot workflow in your repository.
|
||||
|
||||
node setup.mjs --provider deepseek --model MODEL_ID --directory /path/to/repo
|
||||
|
||||
--provider NAME ${Object.keys(KEYS).join(', ')}
|
||||
--model ID Exact model ID; account mode requires provider/model
|
||||
--directory PATH Destination repository (default: current directory)
|
||||
--version vX.Y.Z Checksummed CLI release (default: v0.10.2)
|
||||
--action-ref SHA Reviewed Action commit (default: this clean source checkout)
|
||||
--trigger-phrase NAME Mention to listen for (default: @codewhale)
|
||||
--app-login LOGIN Optional existing GitHub App login, including [bot]
|
||||
--print Print YAML without writing files
|
||||
--help Show this help
|
||||
|
||||
Creates .github/workflows/codewhale-review.yml without overwriting existing files.
|
||||
Set the selected provider secret, then commit the workflow to the default branch.
|
||||
Automatic reviews cover same-repository PRs. Maintainers can review fork PRs
|
||||
with a manual run or an @codewhale review comment; PR code is never executed.
|
||||
`;
|
||||
|
||||
export function workflow({ provider, model, version = 'v0.10.2', actionRef, triggerPhrase = '@codewhale', appLogin }) {
|
||||
if (!Object.hasOwn(KEYS, provider)) throw new Error('Choose a supported --provider.');
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9_./:@+-]{0,199}$/.test(model || '')
|
||||
|| (provider === 'codewhale' && !/^[A-Za-z0-9_.-]+\/.+/.test(model))) throw new Error('Choose an exact --model ID.');
|
||||
if (!/^v\d+\.\d+\.\d+$/.test(version)) throw new Error('--version must be an exact vX.Y.Z release.');
|
||||
if (!/^[a-f0-9]{40}$/i.test(actionRef || '')) throw new Error('--action-ref must be a full reviewed commit SHA.');
|
||||
if (!/^@[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(triggerPhrase)) throw new Error('--trigger-phrase must be @ followed by a GitHub-style name.');
|
||||
if (appLogin && !/^[a-zA-Z0-9][a-zA-Z0-9-]{0,99}\[bot\]$/.test(appLogin)) throw new Error('--app-login must include [bot].');
|
||||
const action = `codewhale-hq/Codewhale@${actionRef}`;
|
||||
const secret = KEYS[provider];
|
||||
const expression = value => '${{ ' + value + ' }}';
|
||||
return `name: Codewhale PR Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr-number:
|
||||
description: Pull request number
|
||||
required: true
|
||||
type: string
|
||||
permissions: {}
|
||||
concurrency:
|
||||
group: codewhale-review-${expression('github.event.pull_request.number || github.event.issue.number || inputs.pr-number')}
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
review:
|
||||
if: >-
|
||||
(github.event_name == 'workflow_dispatch') ||
|
||||
(github.event_name == 'pull_request' && !github.event.pull_request.draft && github.event.pull_request.head.repo.full_name == github.repository) ||
|
||||
((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
|
||||
(github.event.issue.pull_request || github.event.pull_request) &&
|
||||
github.event.comment.user.type == 'User' && contains(github.event.comment.body, '${triggerPhrase}'))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
findings: ${expression('steps.review.outputs.findings')}
|
||||
steps:
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
|
||||
with:
|
||||
node-version: '22'
|
||||
- uses: ${action}
|
||||
id: review
|
||||
with:
|
||||
version: '${version}'
|
||||
provider: '${provider}'
|
||||
model: '${model}'
|
||||
pr-number: ${expression('inputs.pr-number')}
|
||||
trigger-phrase: '${triggerPhrase}'
|
||||
allow-forks: 'true'
|
||||
post: 'false'
|
||||
env:
|
||||
${secret}: ${expression("(github.event_name == 'workflow_dispatch' || github.event_name == 'issue_comment' || github.event.pull_request.head.repo.full_name == github.repository) && secrets." + secret + " || ''")}
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
|
||||
if: steps.review.outputs.findings != ''
|
||||
with:
|
||||
name: codewhale-findings-${expression('github.run_id')}-${expression('github.run_attempt')}
|
||||
path: ${expression('steps.review.outputs.findings')}
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
|
||||
if: always() && steps.review.outputs.receipt != ''
|
||||
with:
|
||||
name: codewhale-review-receipt-${expression('github.run_id')}-${expression('github.run_attempt')}
|
||||
path: ${expression('steps.review.outputs.receipt')}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
publish:
|
||||
needs: review
|
||||
if: needs.review.outputs.findings != ''
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
|
||||
with:
|
||||
node-version: '22'
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
|
||||
with:
|
||||
name: codewhale-findings-${expression('github.run_id')}-${expression('github.run_attempt')}
|
||||
path: findings
|
||||
${appLogin ? ` - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${expression('vars.CODEWHALE_APP_ID')}
|
||||
private-key: ${expression('secrets.CODEWHALE_APP_PRIVATE_KEY')}
|
||||
permission-contents: read
|
||||
permission-pull-requests: write
|
||||
` : ''} - uses: codewhale-hq/Codewhale/scripts/github-action/publish@${actionRef}
|
||||
id: publish
|
||||
with:
|
||||
findings: findings/findings.json
|
||||
pr-number: ${expression('inputs.pr-number')}
|
||||
trigger-phrase: '${triggerPhrase}'
|
||||
allow-forks: 'true'
|
||||
${appLogin ? ` github-token: ${expression('steps.app-token.outputs.token')}
|
||||
reviewer-login: '${appLogin}'
|
||||
` : ''} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
|
||||
if: always() && steps.publish.outputs.receipt != ''
|
||||
with:
|
||||
name: codewhale-publication-receipt-${expression('github.run_id')}-${expression('github.run_attempt')}
|
||||
path: ${expression('steps.publish.outputs.receipt')}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
`;
|
||||
}
|
||||
|
||||
export function setup(args = process.argv.slice(2)) {
|
||||
const { values } = parseArgs({ args, options: {
|
||||
provider: { type: 'string' }, model: { type: 'string' }, directory: { type: 'string' },
|
||||
version: { type: 'string' }, 'action-ref': { type: 'string' }, 'trigger-phrase': { type: 'string' },
|
||||
'app-login': { type: 'string' }, print: { type: 'boolean' }, help: { type: 'boolean' },
|
||||
} });
|
||||
if (values.help) { console.log(HELP); return; }
|
||||
let actionRef = values['action-ref'];
|
||||
if (!actionRef) {
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const git = args => spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
||||
const tracked = git(['cat-file', '-e', 'HEAD:scripts/github-action/setup.mjs']);
|
||||
const clean = git(['diff', '--quiet', 'HEAD', '--', 'action.yml', 'scripts/github-action']);
|
||||
const head = git(['rev-parse', 'HEAD']);
|
||||
if (tracked.status !== 0 || clean.status !== 0 || head.status !== 0) throw new Error('Use a clean Codewhale source checkout or supply --action-ref with a reviewed commit.');
|
||||
actionRef = head.stdout.trim();
|
||||
}
|
||||
const yaml = workflow({ provider: values.provider, model: values.model, version: values.version,
|
||||
actionRef, triggerPhrase: values['trigger-phrase'], appLogin: values['app-login'] });
|
||||
if (values.print) { process.stdout.write(yaml); return; }
|
||||
const directory = realpathSync(values.directory || process.cwd());
|
||||
if (spawnSync('git', ['-C', directory, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' }).stdout.trim() !== directory) throw new Error('--directory must be a Git repository root.');
|
||||
let current = directory;
|
||||
for (const name of ['.github', 'workflows']) {
|
||||
current = join(current, name);
|
||||
try { mkdirSync(current); } catch (error) { if (error.code !== 'EEXIST') throw error; }
|
||||
if (!lstatSync(current).isDirectory() || lstatSync(current).isSymbolicLink()) throw new Error('Workflow directories must be real directories.');
|
||||
}
|
||||
const output = join(current, 'codewhale-review.yml');
|
||||
writeFileSync(output, yaml, { flag: 'wx' });
|
||||
console.log(`Created ${output}\nSet the ${KEYS[values.provider]} Actions secret, then commit the workflow to your default branch.\n${values['app-login'] ? 'The publisher also needs CODEWHALE_APP_ID and CODEWHALE_APP_PRIVATE_KEY.\n' : ''}Reviews use ${values.provider}/${values.model}. GitHub runner usage and model usage follow your account limits.`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
try { setup(); } catch (error) { console.error(error.message); process.exitCode = 1; }
|
||||
}
|
||||
Reference in new issue
Block a user