mirror of
https://github.com/Sea-Haven-Industries/shoc-pr-review-runner.git
synced 2026-09-30 07:13:14 +00:00
323 lines
13 KiB
YAML
323 lines
13 KiB
YAML
name: Review PR
|
|
|
|
# SHOC PR Review Runner — Phase 1 (spec §8, §26).
|
|
# Manually dispatched. Checks out exact PR heads read-only, runs clean
|
|
# build/test gates, generates a truthful evidence report, invokes the Fireworks
|
|
# review agent, validates its output, and publishes artifacts.
|
|
#
|
|
# This workflow NEVER writes to the product repositories or their PRs: the
|
|
# GITHUB_TOKEN carries contents:read only (listing any permission zeroes every
|
|
# unlisted scope), and product-repo access uses a GitHub App installation token
|
|
# downscoped at mint time to contents/pull-requests/metadata READ.
|
|
#
|
|
# SECURITY ARCHITECTURE — why this is two jobs:
|
|
# Running the product repos' build gates executes code authored in the PR under
|
|
# review (npm lifecycle scripts, eslint/vite/vitest configs, MSBuild targets).
|
|
# That code must never share a job with a secret, because step-level `env:` is
|
|
# not an isolation boundary: PR code can poison $GITHUB_ENV for later steps,
|
|
# read a later step's /proc environ, or overwrite the runner's own scripts.
|
|
# Therefore:
|
|
# job `gates` — executes untrusted PR code. Holds NO Fireworks key, and the
|
|
# App token is revoked before the first build command runs.
|
|
# job `review` — holds the Fireworks key. Executes NO product-repo code; it
|
|
# re-checks out this repo fresh (so tampered scripts from the
|
|
# gates job cannot follow) and consumes only text artifacts.
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
review_type:
|
|
description: Review type
|
|
required: true
|
|
type: choice
|
|
options: [frontend, backend, paired]
|
|
frontend_pr:
|
|
description: Frontend PR number (required for frontend/paired)
|
|
required: false
|
|
type: string
|
|
backend_pr:
|
|
description: Backend PR number (required for backend/paired)
|
|
required: false
|
|
type: string
|
|
ticket:
|
|
description: SH ticket identifier or URL
|
|
required: false
|
|
type: string
|
|
review_notes:
|
|
description: "Reviewer context. Sent to the Fireworks model and kept in run artifacts for 30 days — do not paste credentials."
|
|
required: false
|
|
type: string
|
|
run_mocked_e2e:
|
|
description: Run the mocked Playwright suite (frontend/paired)
|
|
required: true
|
|
type: boolean
|
|
default: true
|
|
model:
|
|
description: Fireworks review model
|
|
required: true
|
|
type: choice
|
|
default: deepseek-v4-pro
|
|
options: [deepseek-v4-pro, kimi-k2p6]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: review-pr-${{ github.event.inputs.review_type }}-${{ github.event.inputs.frontend_pr }}-${{ github.event.inputs.backend_pr }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
FRONTEND_REPO: Sea-Haven-Industries/shoc-frontend-new
|
|
BACKEND_REPO: Sea-Haven-Industries/shoc-backend
|
|
COMPANION_BRANCH: dev
|
|
REVIEW_TYPE: ${{ github.event.inputs.review_type }}
|
|
TICKET: ${{ github.event.inputs.ticket }}
|
|
REVIEW_NOTES: ${{ github.event.inputs.review_notes }}
|
|
|
|
jobs:
|
|
gates:
|
|
name: Gates (${{ github.event.inputs.review_type }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
steps:
|
|
- name: Checkout runner
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Configure workspace paths
|
|
# The product checkouts live OUTSIDE github.workspace so PR code is
|
|
# never a sibling of this repo's scripts. RUNNER_TEMP is only available
|
|
# as a shell variable, not in a workflow-level env block.
|
|
run: |
|
|
{
|
|
echo "WORKSPACE_DIR=$RUNNER_TEMP/workspace"
|
|
echo "ARTIFACTS_DIR=$RUNNER_TEMP/workspace/artifacts"
|
|
echo "STATE_DIR=$RUNNER_TEMP/runner-state-$(openssl rand -hex 8)"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Resolve and validate inputs
|
|
id: inputs
|
|
env:
|
|
FRONTEND_PR: ${{ github.event.inputs.frontend_pr }}
|
|
BACKEND_PR: ${{ github.event.inputs.backend_pr }}
|
|
run: ./scripts/resolve-inputs.sh
|
|
|
|
- name: Mint read-only App token
|
|
id: app-token
|
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
with:
|
|
app-id: ${{ secrets.SHOC_REVIEW_APP_ID }}
|
|
private-key: ${{ secrets.SHOC_REVIEW_APP_PRIVATE_KEY }}
|
|
owner: Sea-Haven-Industries
|
|
repositories: shoc-frontend-new,shoc-backend
|
|
# Downscope at mint time so the token stays read-only even if the App
|
|
# installation is later granted broader permissions.
|
|
permission-contents: read
|
|
permission-pull-requests: read
|
|
permission-checks: read
|
|
permission-metadata: read
|
|
|
|
- name: Resolve frontend PR head
|
|
if: steps.inputs.outputs.frontend_pr != ''
|
|
id: frontend-head
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
PR_NUMBER: ${{ steps.inputs.outputs.frontend_pr }}
|
|
run: ./scripts/resolve-pr-head.sh frontend "$FRONTEND_REPO" "$PR_NUMBER"
|
|
|
|
- name: Resolve backend PR head
|
|
if: steps.inputs.outputs.backend_pr != ''
|
|
id: backend-head
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
PR_NUMBER: ${{ steps.inputs.outputs.backend_pr }}
|
|
run: ./scripts/resolve-pr-head.sh backend "$BACKEND_REPO" "$PR_NUMBER"
|
|
|
|
- name: Checkout product repositories at exact heads
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
FRONTEND_SHA: ${{ steps.frontend-head.outputs.frontend_sha }}
|
|
BACKEND_SHA: ${{ steps.backend-head.outputs.backend_sha }}
|
|
run: ./scripts/checkout-repositories.sh
|
|
|
|
- name: Collect review context
|
|
# Runs before any PR-authored code executes, so the collected diff and
|
|
# file contents cannot be tampered with by the build.
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
run: ./scripts/collect-context.sh
|
|
|
|
- name: Revoke App token before running untrusted code
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
run: |
|
|
gh api -X DELETE /installation/token --silent || echo "token revoke returned non-zero (it also expires on its own)"
|
|
|
|
# Everything below this line may execute code authored in the PR.
|
|
# No secret is present in this job from here on.
|
|
|
|
- name: Set up .NET
|
|
if: inputs.review_type == 'backend' || inputs.review_type == 'paired'
|
|
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
|
with:
|
|
dotnet-version: 8.0.x
|
|
|
|
- name: Backend gates
|
|
if: inputs.review_type == 'backend' || inputs.review_type == 'paired'
|
|
continue-on-error: true
|
|
run: ./scripts/run-backend-gates.sh
|
|
|
|
- name: Set up Node
|
|
if: inputs.review_type == 'frontend' || inputs.review_type == 'paired'
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Frontend gates
|
|
if: inputs.review_type == 'frontend' || inputs.review_type == 'paired'
|
|
continue-on-error: true
|
|
env:
|
|
RUN_MOCKED_E2E: ${{ inputs.run_mocked_e2e }}
|
|
run: ./scripts/run-frontend-gates.sh
|
|
|
|
- name: Stop stray background processes
|
|
if: always()
|
|
run: |
|
|
# PR-authored scripts can background processes that would otherwise
|
|
# keep running and mutate files after the gates finish.
|
|
pkill -u "$(id -u)" -f 'node|dotnet|vite|playwright' 2>/dev/null || true
|
|
sleep 2
|
|
|
|
- name: Stage gate results for the review job
|
|
if: always()
|
|
run: |
|
|
cp "$STATE_DIR/gate-status.tsv" "$ARTIFACTS_DIR/gate-status.tsv" 2>/dev/null || true
|
|
ls -la "$ARTIFACTS_DIR"
|
|
|
|
- name: Upload gates context
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
if: always()
|
|
with:
|
|
name: gates-context-${{ github.run_id }}
|
|
path: ${{ runner.temp }}/workspace/artifacts/
|
|
retention-days: 1
|
|
if-no-files-found: warn
|
|
|
|
review:
|
|
name: Review
|
|
needs: gates
|
|
if: always() && needs.gates.result != 'cancelled'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout runner
|
|
# Fresh checkout: scripts tampered with in the gates job cannot follow.
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Configure workspace paths
|
|
# The product checkouts live OUTSIDE github.workspace so PR code is
|
|
# never a sibling of this repo's scripts. RUNNER_TEMP is only available
|
|
# as a shell variable, not in a workflow-level env block.
|
|
run: |
|
|
{
|
|
echo "WORKSPACE_DIR=$RUNNER_TEMP/workspace"
|
|
echo "ARTIFACTS_DIR=$RUNNER_TEMP/workspace/artifacts"
|
|
echo "STATE_DIR=$RUNNER_TEMP/runner-state-$(openssl rand -hex 8)"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Download gates context
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: gates-context-${{ github.run_id }}
|
|
path: ${{ runner.temp }}/workspace/artifacts
|
|
|
|
- name: Point the gate table at the downloaded results
|
|
run: |
|
|
mkdir -p "$STATE_DIR"
|
|
cp "$ARTIFACTS_DIR/gate-status.tsv" "$STATE_DIR/gate-status.tsv" 2>/dev/null || true
|
|
|
|
- name: Generate evidence report
|
|
run: ./scripts/generate-evidence.sh
|
|
|
|
- name: Run review agent
|
|
id: agent
|
|
continue-on-error: true
|
|
env:
|
|
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
|
MODEL: ${{ inputs.model }}
|
|
run: ./scripts/run-review-agent.sh
|
|
|
|
- name: Artifact redaction check
|
|
id: redact
|
|
# Runs even when earlier steps failed so nothing is uploaded unscanned.
|
|
if: always()
|
|
env:
|
|
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
|
run: ./scripts/redact-check.sh
|
|
|
|
- name: Job summary
|
|
if: always() && steps.redact.outcome == 'success'
|
|
run: |
|
|
{
|
|
echo "## SHOC PR Review — ${REVIEW_TYPE}"
|
|
echo ""
|
|
echo "### Gate status (machine-recorded, authoritative)"
|
|
echo ""
|
|
echo "| Gate | Status |"
|
|
echo "| --- | --- |"
|
|
if [ -f "$STATE_DIR/gate-status.tsv" ]; then
|
|
awk -F'\t' '{printf "| %s | %s |\n", $1, $2}' "$STATE_DIR/gate-status.tsv"
|
|
fi
|
|
echo ""
|
|
if [ -f "$ARTIFACTS_DIR/review.md" ] && [ "${{ steps.agent.outcome }}" = "success" ]; then
|
|
echo "### Review (model-generated, copy into GitHub manually)"
|
|
echo ""
|
|
echo "The block below is model output influenced by PR content. The"
|
|
echo "gate table above is the authoritative record of what ran."
|
|
echo ""
|
|
echo '```markdown'
|
|
cat "$ARTIFACTS_DIR/review.md"
|
|
echo '```'
|
|
else
|
|
echo "### No valid review produced"
|
|
echo ""
|
|
echo "Agent step outcome: ${{ steps.agent.outcome }} — see validation-errors.txt in the artifacts."
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Upload review
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
if: always() && steps.redact.outcome == 'success'
|
|
with:
|
|
name: review-${{ github.run_id }}
|
|
path: |
|
|
${{ runner.temp }}/workspace/artifacts/review.md
|
|
${{ runner.temp }}/workspace/artifacts/review-evidence.md
|
|
${{ runner.temp }}/workspace/artifacts/gate-status.tsv
|
|
${{ runner.temp }}/workspace/artifacts/validation-errors.txt
|
|
${{ runner.temp }}/workspace/artifacts/logs/
|
|
retention-days: 30
|
|
if-no-files-found: warn
|
|
|
|
- name: Upload prompt and diff material
|
|
# Contains full private-repo source; kept briefly for debugging only.
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
if: always() && steps.redact.outcome == 'success'
|
|
with:
|
|
name: review-context-${{ github.run_id }}
|
|
path: |
|
|
${{ runner.temp }}/workspace/artifacts/agent-prompt*.txt
|
|
${{ runner.temp }}/workspace/artifacts/agent-raw-response*
|
|
${{ runner.temp }}/workspace/artifacts/prompt-*.txt
|
|
${{ runner.temp }}/workspace/artifacts/*.diff
|
|
retention-days: 2
|
|
if-no-files-found: warn
|
|
|
|
- name: Fail run if agent or validation failed
|
|
if: steps.agent.outcome != 'success'
|
|
run: |
|
|
echo "Review agent or output validation failed — see artifacts." >&2
|
|
exit 1
|