Skip to content

GitHub Actions

Apex Ray ships a composite action at .github/actions/apex-ray-review. It checks out the exact pull-request head into an isolated, per-run $GITHUB_WORKSPACE/.apex-ray-review-<run-id>-<attempt>/repository analysis directory, reviews the immutable base-SHA diff, writes Markdown/JSON/SARIF, adds a compact job summary, uploads the reports as an artifact, and attempts a non-blocking code-scanning upload.

The action runs Apex Ray from the immutable action source under GITHUB_ACTION_PATH, never from the repository-under-review checkout. It requires the canonical action source root to be disjoint from GITHUB_WORKSPACE and installs and builds the locked runtime before checking out the pull-request head. It installs Python dependencies from the pinned action commit's uv.lock with uv sync --locked --no-install-project, imports only that pinned Python source, installs TypeScript analyzer dependencies from that action commit's package-lock.json with npm ci, and builds only the pinned analyzer. It does not run package-manager hooks, build scripts, tests, analyzer scripts, or Python imports from the pull-request head. The reviewed checkout is parser input, not executable action code. This separation avoids both pull-request runtime replacement and an unrelated PyPI artifact or unlocked Python build-isolation environment. The Python, Node.js, and uv tool versions are also exact.

Replace <full-release-commit-sha> with the 40-character commit for the Apex Ray release you have reviewed. Pinning the action itself and its transitive actions prevents a mutable tag from changing the code that receives API credentials. Before checkout, the action verifies that github.action_ref is a full 40-character commit SHA and that the canonical action source is disjoint from GITHUB_WORKSPACE; it rejects mutable tags and local action paths.

Do not replace the pinned remote uses: line with uses: ./.github/actions/apex-ray-review in a pull-request workflow that can receive API credentials. A local action is loaded from the caller's checkout; for pull_request, that can be the pull request merge commit, so the action implementation itself is not an immutable trust boundary.

Before copying this workflow, create a protected GitHub Environment named apex-ray-review, enable required reviewers for it, and store OPENAI_API_KEY there as an environment secret, not a repository secret. Select independent gatekeepers who inspect workflow changes, enable Prevent self-review, and deselect Allow administrators to bypass configured protection rules. A same-repository pull request can change its caller workflow, so approving the environment is the explicit trust decision for the exact workflow revision that will receive the credential. Review that revision before approving the deployment. If these protection rules are unavailable, keep LLM access disabled in pull_request workflows or use a separate trusted workflow; do not fall back to a repository-level API secret.

name: Apex Ray

on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]

permissions:
  contents: read
  security-events: write

concurrency:
  group: apex-ray-${{ github.workflow }}-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  review:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    environment: apex-ray-review
    timeout-minutes: 45
    strategy:
      fail-fast: false
      matrix:
        reviewer: [correctness, security, ux]
    steps:
      - name: Review as ${{ matrix.reviewer }}
        uses: dobrotacreator/apex-ray/.github/actions/apex-ray-review@<full-release-commit-sha>
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        with:
          reviewers: ${{ matrix.reviewer }}
          llm: auto
          artifact-name: apex-ray-${{ matrix.reviewer }}
          sarif-category: apex-ray-${{ matrix.reviewer }}

contents: read is sufficient for checkout and deterministic review. security-events: write is needed only for the optional SARIF upload. The composite action cannot grant job permissions or configure concurrency, so both belong in the caller workflow. If code scanning is unavailable for the repository, SARIF upload is non-blocking and the artifact remains available.

The action intentionally does not post pull-request comments. That keeps the default token read-only apart from the narrowly scoped code-scanning permission. Teams can consume the JSON or SARIF in a separate, explicitly privileged workflow after review.

The repository's github-actions-api.yml is a bounded TypeScript/API starting point. See Tuning before expanding matrix reviewers or token budgets, because each reviewer has an independent cap.

Dart And Flutter Projects

The Apex Ray action does not install Flutter or run package resolution. For a semantic Flutter review, select a pinned project-compatible SDK, restore the SDK and Pub caches, and create .dart_tool/package_config.json before the review. The following shape checks out the exact PR head itself, then tells the remotely pinned Apex Ray action to use that prepared checkout:

jobs:
  review:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    environment: apex-ray-review
    timeout-minutes: 45
    permissions:
      contents: read
      security-events: write
    steps:
      - name: Checkout exact review head
        uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0
          persist-credentials: false

      - name: Set up the project's Flutter SDK
        uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
        with:
          flutter-version: "<exact-project-flutter-version>"
          channel: stable
          cache: true
          pub-cache: true

      - name: Resolve locked Flutter dependencies
        run: flutter pub get --enforce-lockfile

      - name: Review Flutter diff
        uses: dobrotacreator/apex-ray/.github/actions/apex-ray-review@<full-release-commit-sha>
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        with:
          checkout: "false"
          reviewers: correctness
          llm: auto
          artifact-name: apex-ray-flutter
          sarif-category: apex-ray-flutter

Replace both placeholders with immutable values reviewed by the team. The Flutter action commit shown above is the pinned v2.23.0 implementation; keep the SDK version exact instead of following latest. If the project checks in an FVM version file, the setup action also supports flutter-version-file: .fvmrc; pinning the resolved version explicitly makes upgrades visible in the workflow diff.

For a pure Dart project, use a full reviewed commit SHA of dart-lang/setup-dart with an exact sdk version, then run dart pub get --enforce-lockfile before Apex Ray. The restricted action recognizes both the official setup-dart tool-cache layout and Flutter's bundled Dart SDK.

Scope API secrets only to the Apex Ray step as shown. Dependency resolution has network access and consumes pull-request-controlled manifests, so it must run before any credential is mapped into the step environment. Use a committed application lockfile with --enforce-lockfile, avoid executing repository scripts or builds in the review job, and provide private package-registry credentials only through a separately reviewed policy when they are required.

For a Pub workspace, run flutter pub get --enforce-lockfile at the workspace root. For a repository with independent packages, invoke pub get in a fixed, trusted list of relevant package directories; do not execute a helper script from the pull-request head in a credentialed review job. The Dart language server uses the resulting package configurations; Apex Ray does not run application entry points or generators.

The shared config normally leaves review.analyzer.dart.command: []; the Dart executable added to PATH by the setup action is then selected automatically. In restricted pull-request mode, the action ignores any repository-provided Dart command, resolves the PATH entry to a native Dart SDK binary inside RUNNER_TOOL_CACHE (unwrapping Flutter's shell launcher without executing it), and starts the language server with analyzer plugins disabled. Arbitrary PATH shims and SDKs outside the runner tool cache are rejected. If the selected SDK cannot enforce that mode, the action disables Dart semantics and keeps diff-only Dart coverage instead of executing a less restricted server, and emits an Apex Ray analyzer fallback workflow warning so reduced coverage is visible on the job. Local trusted runs may use project-local FVM and analyzer plugins. Generated Dart remains available to semantic resolution, while generated review targets and raw snippets are suppressed. See Dart analyzer configuration for the local configuration and fallback policy.

The Flutter project maintains the stable SDK archive, and Dart documents why pub get creates package configuration and why --enforce-lockfile is appropriate in CI. The third-party setup action's cache inputs are documented in its pinned release.

Configure focused reviewers

Reviewer ids in the workflow must exist in the trusted repository config. A matrix gives every focus an independent timeout, artifact, SARIF category, and status check. A single action invocation may instead accept a comma- or newline-separated list; the action converts it into repeated --reviewer flags.

review:
  llm:
    enabled: true
    provider: openai_api
    model: "<approved-openai-model-id>"
  reviewers:
    - id: security
      name: Security
      focus: Authentication, authorization, injection, secrets, and trust boundaries.
      risk_tags: [security]
    - id: finance
      name: Financial risk
      focus: Money movement, rounding, reconciliation, limits, and idempotency.
      risk_tags: [financial]
    - id: ux
      name: UX and UI
      focus: User-visible regressions, accessibility, state transitions, and error recovery.
      paths: ["src/ui/**", "apps/web/**"]
  risk:
    rules:
      - id: money-movement
        title: Money movement boundary
        severity: critical
        paths: ["src/payments/**", "src/ledger/**"]
        categories: [financial, correctness]
        reviewer_tags: [finance]
        guidance: Verify units, rounding, idempotency, authorization, and reconciliation.

Keep focus text specific and testable. Prefer separate reviewers when a domain needs a different model profile, token budget, timeout, artifact, or ownership boundary; use one invocation with several reviewer ids when a combined report is more useful than independent checks.

The action fails when review execution or report rendering fails. By default it also fails when llm_coverage.quality_gate_status is fail; this includes a configured required reviewer that did not complete successfully. A finding, even a critical one, does not by itself fail the action. Use branch protection or repository rules to make the resulting matrix job checks required for merge.

Line-addressable findings are published as escaped workflow annotations (error for critical/high, warning for medium, and notice for low), capped at 50 annotations per invocation; the full set remains in JSON and SARIF. The action also exposes stable machine outputs: findings-count plus per-severity counts, partial-coverage, partial-coverage-severity, coverage-status (disabled, complete, partial, or incomplete), reviewer-statuses (a compact JSON object), quality-gate-status, and the enforced gate-outcome. Report path outputs are described below. Findings and annotations cover only the packs/reviewer assignments that actually ran; inspect coverage-status before treating a zero-finding report as whole-diff evidence.

For an advisory rollout, set fail-on-quality-gate: "false". Reports, the job summary, and the quality-gate-status action output still expose the failed coverage gate, but it does not change the step's exit status. Keep the default for reviewers whose coverage is a merge requirement.

Require complete coverage

The default coverage-policy: configured runs the budgets in the repository configuration and enforces the configured quality gate. It can legitimately finish with partial coverage. Use complete for a job whose selected reviewer scope must be fully reviewed:

- name: Complete correctness review
  uses: dobrotacreator/apex-ray/.github/actions/apex-ray-review@<full-release-commit-sha>
  env:
    OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
  with:
    reviewers: correctness
    llm: auto
    coverage-policy: complete
    followup-max-pack-reviews: "16"
    max-followup-passes: "8"
    artifact-name: apex-ray-correctness
    sarif-category: apex-ray-correctness

The action adds --until-complete, reviews at most followup-max-pack-reviews reviewer-pack assignments per follow-up batch, and runs at most max-followup-passes batches. The job fails unless the final coverage-status is complete; fail-on-quality-gate: "false" makes only the configured quality gate advisory and does not disable this completion policy. Reports and SARIF are still finalized when the bounded loop reaches its limit.

Completion is evaluated against packs matching the selected reviewer, not unrelated global debt outside that reviewer's filters. Consequently a focused reviewer job can return coverage-status: complete even though another scope would still be partial. Use a broad, unfiltered correctness reviewer for a whole-diff merge requirement, and keep focused specialist jobs as separate contracts.

Make the reviewer scope explicit, preferably as one reviewer per matrix job. When reviewers is omitted and several reviewers are enabled, the config must identify exactly one reviewer marked as required; a single enabled reviewer is also unambiguous. Apex Ray rejects any other implicit multi-reviewer completion scope before provider calls. On a fresh run, the uniquely required reviewer is the only reviewer executed; the action does not spend budget on the other configured reviewers before draining the baseline.

Complete coverage requires trusted LLM access. In the restricted PR workflow, that means an API provider, protected credentials, and a same-repository trusted run. Fork and Dependabot runs are forced to --no-llm, so they cannot satisfy coverage-policy: complete; use the default configured policy for those deterministic runs or condition the complete-coverage job to trusted same-repository pull requests. With the default llm: auto, the Action honors the trusted config and does not enable LLM review merely because coverage-policy is complete; the config must set review.llm.enabled: true. Use llm: "true" when this Action job should explicitly override that setting and enable the configured provider. Keep batch limits conservative and increase them only from measured residual coverage and cost.

API credentials

Do not put API keys, secret values, or secret names in action inputs. Configure the provider in .apex-ray/config.yml, name its environment variables there, and pass values only to the pinned Apex Ray step through its env mapping from GitHub Environment Secrets. Preset providers use these default key names:

Provider Config value Default secret environment variable
OpenAI openai_api OPENAI_API_KEY
Anthropic anthropic_api ANTHROPIC_API_KEY
DeepSeek deepseek_api DEEPSEEK_API_KEY
Qwen qwen_api DASHSCOPE_API_KEY
Kimi kimi_api MOONSHOT_API_KEY
Z.AI zai_api ZAI_API_KEY

For a custom OpenAI-compatible gateway, keep the URL and host allowlist under CI control as repository or environment variables:

review:
  llm:
    enabled: true
    provider: openai_compatible
    model: internal-review-model
    api:
      protocol: openai_chat
      structured_output: json_object
      base_url_env: APEX_RAY_LLM_BASE_URL
      api_key_env: APEX_RAY_LLM_API_KEY
      allowed_hosts_env: APEX_RAY_API_ALLOWED_HOSTS
jobs:
  review:
    runs-on: ubuntu-latest
    environment: apex-ray-review
    env:
      APEX_RAY_LLM_BASE_URL: ${{ vars.APEX_RAY_LLM_BASE_URL }}
      APEX_RAY_API_ALLOWED_HOSTS: ${{ vars.APEX_RAY_API_ALLOWED_HOSTS }}
      APEX_RAY_API_ALLOWED_BASE_URL_ENV_VARS: APEX_RAY_LLM_BASE_URL
      APEX_RAY_API_ALLOWED_API_KEY_ENV_VARS: APEX_RAY_LLM_API_KEY
    steps:
      - name: Review
        uses: dobrotacreator/apex-ray/.github/actions/apex-ray-review@<full-release-commit-sha>
        env:
          APEX_RAY_LLM_API_KEY: ${{ secrets.APEX_RAY_LLM_API_KEY }}

The host allowlist is a comma- or whitespace-separated list of hostnames, without schemes or paths. In CI, allowed_hosts_env is fixed to APEX_RAY_API_ALLOWED_HOSTS; repository configuration cannot select another variable. A custom endpoint must come from base_url_env, its normalized host must appear in APEX_RAY_API_ALLOWED_HOSTS, and every environment selector chosen by repository configuration must appear in its role-specific trusted allowlist: base URL, API key, or custom header. Variables designated by the API-key policy, the selected credential, and built-in preset credential variables can never be reused as custom headers in CI. Define policy variables in the workflow or a protected environment. Use an environment with the protections described above for high-value credentials, set provider spending limits, and avoid forwarding unrelated repository secrets to the job.

Fork and configuration safety

The workflow uses the ordinary, unprivileged pull-request event. GitHub does not pass Actions secrets to workflows triggered from forks (apart from a read-only repository token), so the action explicitly forces --no-llm for fork and Dependabot pull requests. Those runs still perform deterministic classification and analyzer-backed review, produce artifacts, and skip the code-scanning upload that requires a write token.

Do not switch this workflow to pull_request_target. That event runs with the base repository's privileged token and secrets, and combining it with a pull-request head checkout is unsafe if any later step starts executing code from that checkout. Apex Ray keeps its own runtime separate and treats the head as analysis input, but the ordinary pull_request event is still the intended and least-privileged integration. Use a separate privileged workflow that consumes validated report artifacts if a later operation needs write access.

The endpoint and environment-selector allowlists defend against untrusted Apex Ray configuration; they cannot defend credentials if an attacker is also allowed to rewrite the caller workflow. Treat workflow authors as trusted, never expose repository-level API secrets to pull_request jobs, keep API credentials in a protected GitHub environment with required reviewers, and require ownership review for .github/workflows/ changes before granting same-repository pull-request jobs access to those credentials.

For every pull request, the default trust-pr-config: false loads .apex-ray/config.yml from the base commit and writes a restricted temporary copy. The restricted copy disables custom analyzer scripts, external rule/memory files, caches, telemetry, report archives, and triage writes. For Dart, it also replaces repository-provided commands with the validated native binary of a Dart SDK in RUNNER_TOOL_CACHE and disables analyzer plugins; without a compatible SDK there, the Dart backend becomes diff-only and the action emits a workflow warning. Inline risk rules and reviewer definitions from the base branch remain available. The optional base input changes only the diff-analysis base; it cannot select the configuration trust root. With this default (and always for forks), configuration is read exclusively from the immutable pull_request.base.sha, and the action fails closed if that event commit is missing or unavailable in the checkout.

When LLM review can run from that restricted copy, every effective provider route must use an API provider. The action rejects codex_cli and claude_code_cli both at review.llm.provider and in any profile, including a profile selected by global routing or a focused reviewer. Fork and Dependabot runs are forced to --no-llm, so CLI routes present in the trusted base config are never executed there. Configure openai_api, anthropic_api, another preset API, or openai_compatible for CI.

Set trust-pr-config: true only for same-repository pull requests whose authors are allowed to choose the declarative API endpoint, reviewer, and risk configuration. The option is ignored for forks. Head configuration still goes through the restricted-copy sanitizer: custom analyzer scripts, external rule/memory files, CLI LLM providers, caches, telemetry, report archives, and triage writes cannot execute from the pull-request checkout; Dart commands and analyzer plugins remain restricted as described above. Review changes to shared config, reviewer prompts, API environment-variable names, and endpoint allowlists as security changes before merging them to the base branch.

If another step performs checkout, use checkout: "false" only after checking out the exact head commit at GITHUB_WORKSPACE with full history and without persisted credentials. The checkout input accepts only the exact strings "true" and "false" so a typo cannot silently bypass the isolated checkout. Keep the action itself remotely pinned and do not run dependency hooks, builds, tests, or repository-provided scripts before review when the job has secrets. The action validates all report paths relative to the repository under review and exposes both repository-relative outputs (markdown-output, json-output, sarif-output) and absolute outputs (repository-path, markdown-path, json-path, sarif-path). Local absolute paths remain omitted from SARIF locations and messages.

GitHub references: