Skip to content

Commit ba6ab3b

Browse files
committed
Emit verdict rows to S3 instead of ClickHouse
- verdict: emit a gzipped JSONEachRow row to fixed /tmp paths for the record workflow to upload, dropping the direct ClickHouse INSERT - clickhouse_client: strip VerdictRow/INSERT/insert_verdict_row; keep only connect() as a read-path helper returning the driver Client - workflow: record job drops ClickHouse write secrets, gains OIDC id-token + arc role, and aws s3 cp's the row to gha-artifacts - tests: replace the fake CH insert client with a fake emit; assert gzip payload contents, key scheme, and default file-writing emit - docs: README/CHEATSHEET describe the S3 -> replicator ingest path The record job no longer holds ClickHouse write credentials or runs any INSERT. Verdict rows land in s3://gha-artifacts/greenlight_pr_state/ and the clickhouse-replicator-s3 Lambda ingests them into misc.greenlight_pr_state; this keeps App-key and write access off the untrusted-content boundary. greenlight retains ClickHouse READ access for the service's own SELECTs. The object is a single PutObject with no content-encoding so the ClickHouse s3() reader parses it as stored. Signed-off-by: Jean Schmidt <contato@jschmidt.me>
1 parent d182f99 commit ba6ab3b

8 files changed

Lines changed: 289 additions & 293 deletions

File tree

.github/workflows/greenlight-pr-review.yml

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ name: Green Light PR Review
44
# review — UNPRIVILEGED. Runs the AI model over untrusted PR content. Has Bedrock
55
# (OIDC) access only; it can read code and emit a verdict file, nothing more.
66
# record — PRIVILEGED. No model. Validates the verdict, mints the Green Light App
7-
# token, and writes the row to ClickHouse. Kept separate so untrusted PR
8-
# content never runs in a job that holds the App key or write creds.
7+
# token, posts the review, and uploads the verdict row to
8+
# s3://gha-artifacts/greenlight_pr_state/ (via OIDC role/arc) for the
9+
# clickhouse-replicator-s3 Lambda to ingest. Kept separate so untrusted
10+
# PR content never runs in a job that holds the App key.
911

1012
on:
1113
workflow_dispatch:
@@ -195,17 +197,15 @@ jobs:
195197
if: ${{ always() }}
196198
runs-on: ubuntu-latest
197199
timeout-minutes: 15
198-
# Protected environment holding ALL record-job secrets (provision before first run):
199-
# GREENLIGHT_APP_ID and GREENLIGHT_APP_PRIVATE_KEY (the Green Light App key), and
200-
# CLICKHOUSE_HUD_USER_WRITE_URL/_USERNAME/_PASSWORD (a dedicated ClickHouse write
201-
# user). These live on this environment and are NOT inherited from the repo/org.
200+
# Protected environment holding the Green Light App key (provision before first run):
201+
# GREENLIGHT_APP_ID and GREENLIGHT_APP_PRIVATE_KEY. These live on this environment
202+
# and are NOT inherited from the repo/org. Verdict rows are emitted to S3 via the
203+
# OIDC role below, so no ClickHouse credentials are needed here.
202204
environment: greenlight-record
203205
permissions:
204206
contents: read
207+
id-token: write
205208
env:
206-
CLICKHOUSE_ENDPOINT: ${{ secrets.CLICKHOUSE_HUD_USER_WRITE_URL }}
207-
CLICKHOUSE_USERNAME: ${{ secrets.CLICKHOUSE_HUD_USER_WRITE_USERNAME }}
208-
CLICKHOUSE_PASSWORD: ${{ secrets.CLICKHOUSE_HUD_USER_WRITE_PASSWORD }}
209209
PR_NUMBER: ${{ github.event.inputs.pr_number }}
210210
HEAD_SHA: ${{ github.event.inputs.head_sha }}
211211
EVAL_HASH: ${{ github.event.inputs.eval_hash }}
@@ -226,6 +226,13 @@ jobs:
226226
- name: Install uv
227227
uses: ./.github/actions/setup-uv
228228

229+
- name: Configure AWS credentials via OIDC
230+
# Ungated: success, cancelled, and failed all upload a verdict row to S3.
231+
uses: aws-actions/configure-aws-credentials@v4
232+
with:
233+
role-to-assume: arn:aws:iam::308535385114:role/arc
234+
aws-region: us-east-1
235+
229236
- name: Download verdict artifact
230237
if: ${{ needs.review.result == 'success' }}
231238
uses: actions/download-artifact@v4
@@ -307,3 +314,13 @@ jobs:
307314
--head-sha "$HEAD_SHA" \
308315
--eval-hash "$EVAL_HASH" \
309316
--eval-job-url "$RUN_URL"
317+
318+
- name: Upload verdict row to S3
319+
# Exactly one record step above produced the row + key at fixed /tmp paths.
320+
# No `if:` — the default success() runs this after whichever branch ran and
321+
# skips it if that branch failed (skipped siblings don't count as failures).
322+
# Single PutObject, no --content-encoding: the ClickHouse s3() reader expects
323+
# the object stored as-is and would read 0 rows if it were marked gzip-encoded.
324+
run: |
325+
set -euo pipefail
326+
aws s3 cp /tmp/greenlight-verdict-row.json.gz "s3://gha-artifacts/$(cat /tmp/greenlight-verdict-key.txt)"

greenlight/CHEATSHEET.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,13 @@ automated DDL), so @clee2000 or @huydhn apply them manually:
131131

132132
- `001_create_misc_greenlight_pr_state.sql` — create the table
133133
- `002_alter_greenlight_pr_state_version_default.sql` — set the `version` DEFAULT
134-
135-
No per-table grant file is needed: the PR-review workflow writes as a shared ClickHouse
136-
write user (`CLICKHOUSE_HUD_USER_WRITE_*` secrets on the `greenlight-record` environment)
137-
that already holds INSERT through its broad grants. The `verdict` subcommand reads the
138-
standard `CLICKHOUSE_*` connection variables (`CLICKHOUSE_HOST` or its
139-
`CLICKHOUSE_ENDPOINT` alias, `CLICKHOUSE_USERNAME`, `CLICKHOUSE_PASSWORD`, and
140-
`CLICKHOUSE_PORT` default `8443`) and only ever INSERTs rows. The review-side fingerprint
141-
computation and the land-time verifier that reads this table back are not built yet.
134+
- `003_alter_greenlight_pr_state_add_meta.sql` — add the `_meta` column the replicator needs
135+
136+
The `verdict` subcommand does NOT write ClickHouse directly: it emits a gzipped JSON row
137+
that the record workflow uploads to `s3://gha-artifacts/greenlight_pr_state/`, and the
138+
clickhouse-replicator-s3 path ingests it into the table. greenlight keeps ClickHouse READ
139+
access for the service's SELECTs via `clickhouse_client.connect()`, which reads the standard
140+
`CLICKHOUSE_*` connection variables (`CLICKHOUSE_HOST` or its `CLICKHOUSE_ENDPOINT` alias,
141+
`CLICKHOUSE_USERNAME`, `CLICKHOUSE_PASSWORD`, and `CLICKHOUSE_PORT` default `8443`). The
142+
review-side fingerprint computation and the land-time verifier that reads this table back
143+
are not built yet.

greenlight/README.md

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,29 @@ The `review` examples require `PYTORCH_GREENLIGHT_GITHUB_TOKEN` to be set; witho
3939
### Recording a verdict
4040

4141
A privileged CI job records a review verdict with `verdict`. It runs once (never a
42-
daemon): it writes a row to `misc.greenlight_pr_state` — storing the passed-in
43-
`eval_hash` verbatim — then, for `LAND`/`NO_LAND`, acts on the PR (`LAND` approves;
44-
`NO_LAND` dismisses greenlight's own prior approval and comments). `CANCELLED` and
45-
`FAILED` markers only record a row. The `reason` must be a canonical `ALLOWED_REASONS`
46-
code, and the model's message is defanged before it is posted to GitHub (the full
47-
message is stored verbatim in ClickHouse).
42+
daemon): it emits a gzipped single-line JSON row (whose `reason` must be a canonical
43+
`ALLOWED_REASONS` code) that the record workflow uploads to
44+
`s3://gha-artifacts/greenlight_pr_state/`, where the clickhouse-replicator-s3 path ingests
45+
it into `misc.greenlight_pr_state` — the command never writes ClickHouse directly. Then,
46+
for `LAND`/`NO_LAND`, it acts on the PR (`LAND` approves; `NO_LAND` dismisses greenlight's
47+
own prior approval and comments). `CANCELLED` and `FAILED` markers only emit the row. The
48+
model's message is defanged before it is posted to GitHub, while the full message is stored
49+
verbatim in the emitted row.
4850

4951
```bash
5052
just run verdict --pr 123 --head-sha "$SHA" --verdict-file verdict.json \
5153
--eval-hash "$EVAL_HASH" --bot-login 'greenlight-app[bot]' # LAND/NO_LAND
52-
just run verdict --pr 123 --head-sha "$SHA" --status CANCELLED # marker: row only
54+
just run verdict --pr 123 --head-sha "$SHA" --status CANCELLED # marker: emit row only
5355
just run verdict --pr 123 --head-sha "$SHA" --verdict-file verdict.json \
5456
--eval-hash "$EVAL_HASH" --dry-run # offline; logs only
5557
```
5658

57-
`verdict` needs `PYTORCH_GREENLIGHT_GITHUB_TOKEN` (for `LAND`/`NO_LAND`) and the
58-
`CLICKHOUSE_*` credentials to write; `--dry-run` needs neither. `--bot-login` (the
59-
greenlight GitHub App's `<slug>[bot]` account) is required for `NO_LAND`.
59+
The command writes the gzipped row to `/tmp/greenlight-verdict-row.json.gz` and its
60+
bucket-relative key to `/tmp/greenlight-verdict-key.txt`; the workflow `aws s3 cp`s the
61+
former to the latter. There is no direct ClickHouse write, so no `CLICKHOUSE_*` credentials
62+
are needed here; `verdict` needs `PYTORCH_GREENLIGHT_GITHUB_TOKEN` to post `LAND`/`NO_LAND`,
63+
and `--dry-run` needs nothing. `--bot-login` (the greenlight GitHub App's `<slug>[bot]`
64+
account) is required for `NO_LAND`.
6065

6166
Configuration is read from the environment via `PYTORCH_GREENLIGHT_*` variables:
6267

@@ -83,10 +88,13 @@ on failure, and clean signal shutdown — all built and tested. `review` fetches
8388
PRs from a fixed set of trusted authors in `pytorch/pytorch` (read-only GitHub) and
8489
logs them; it requires `PYTORCH_GREENLIGHT_GITHUB_TOKEN`.
8590

86-
Also works: the `verdict` subcommand records a PR-review verdict to
87-
`misc.greenlight_pr_state` (storing the passed-in `eval_hash` verbatim) and, for
88-
LAND/NO_LAND, acts on the PR — approve, or dismiss greenlight's prior approval and
89-
comment. It is a one-shot call meant for a privileged CI job.
91+
Also works: the `verdict` subcommand emits a PR-review verdict row (with the passed-in
92+
`eval_hash` verbatim) for the record workflow to upload to
93+
`s3://gha-artifacts/greenlight_pr_state/`, where the clickhouse-replicator-s3 path ingests
94+
it into `misc.greenlight_pr_state`; for LAND/NO_LAND it also acts on the PR — approve, or
95+
dismiss greenlight's prior approval and comment. It is a one-shot call for a privileged CI
96+
job and never writes ClickHouse directly. The service keeps ClickHouse READ access
97+
(`clickhouse_client.connect()`) for its own SELECTs.
9098

9199
Not built yet: risk-scoring and the review decision in `review`; the AI code-review
92100
workflow (a separate component) that produces the verdict; and the review-side
@@ -120,9 +128,9 @@ src/greenlight/
120128
cli.py # CLI parsing (review + verdict subcommands), dispatch, exit codes
121129
runner.py # run_forever(): resilient daemon loop; execute_once(): one-shot phase run
122130
review.py # fetch open PRs from trusted authors in pytorch/pytorch and log them; raises on failure
123-
verdict.py # one-shot: record a PR-review verdict, then approve/dismiss/comment on the PR
131+
verdict.py # one-shot: emit a verdict row for S3->replicator, then approve/dismiss/comment on the PR
124132
github_client.py # GitHub PR access: read PR list/fingerprint + post verdict actions
125-
clickhouse_client.py # ClickHouse connection + verdict-row insert into misc.greenlight_pr_state
133+
clickhouse_client.py # ClickHouse connection helper for the service's read (SELECT) queries
126134
pr_hash.py # eval_hash land-guard: deterministic PR fingerprint hash
127135
config.py # PYTORCH_GREENLIGHT_* environment configuration
128136
guards.py # single-instance lock + per-iteration SIGALRM timeout + hard watchdog

greenlight/src/greenlight/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ def build_parser() -> argparse.ArgumentParser:
4747
"verdict",
4848
help="record a PR review verdict once and act on GitHub",
4949
description=(
50-
"Record a single PR-review verdict in ClickHouse and, for LAND/NO_LAND, act on the PR. "
51-
"Runs once outside the daemon loop and lock. Requires PYTORCH_GREENLIGHT_GITHUB_TOKEN for "
52-
"LAND/NO_LAND and CLICKHOUSE_* credentials to write."
50+
"Record a single PR-review verdict: emit the row for S3 -> ClickHouse ingestion and, "
51+
"for LAND/NO_LAND, post the GitHub review. Runs once outside the daemon loop and lock. "
52+
"Requires PYTORCH_GREENLIGHT_GITHUB_TOKEN for LAND/NO_LAND."
5353
),
5454
)
5555
verdict_parser.add_argument("--repo", default=review.TARGET_REPO, help="owner/name of the repository")

greenlight/src/greenlight/clickhouse_client.py

Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
1-
"""ClickHouse access for the greenlight service: connection and verdict-row insert.
1+
"""ClickHouse connection helper for the greenlight service.
22
3-
Connection settings come from the standard ``CLICKHOUSE_*`` environment variables.
4-
Verdict rows are written to ``misc.greenlight_pr_state``; its ``_inserted_at`` column is
5-
MATERIALIZED server-side and is therefore never part of an insert.
3+
Connection settings come from the standard ``CLICKHOUSE_*`` environment variables. The
4+
returned client is used for the service's read (SELECT) queries; verdict writes go
5+
through the S3 -> replicator path (see ``verdict``), not a direct INSERT from here.
66
"""
77

88
from __future__ import annotations
99

1010
import logging
1111
import os
12-
from dataclasses import astuple, dataclass, fields
13-
from typing import TYPE_CHECKING, Protocol
12+
from typing import TYPE_CHECKING
1413

1514
if TYPE_CHECKING:
16-
from collections.abc import Sequence
17-
from datetime import datetime
15+
from clickhouse_connect.driver.client import Client
1816

1917
logger = logging.getLogger(__name__)
2018

21-
TABLE = "misc.greenlight_pr_state"
22-
2319
_DEFAULT_PORT = 8443
2420
_CLICKHOUSE_CLOUD_DOMAIN = ".clickhouse.cloud"
2521
_ENV_HELP = (
@@ -28,37 +24,6 @@
2824
)
2925

3026

31-
class ClickHouseClient(Protocol):
32-
def insert(
33-
self,
34-
table: str,
35-
data: Sequence[Sequence[object]],
36-
*,
37-
column_names: Sequence[str],
38-
) -> object: ...
39-
40-
41-
@dataclass(frozen=True, slots=True)
42-
class VerdictRow:
43-
repo: str
44-
pr_number: int
45-
head_sha: str
46-
status: str
47-
reason: str
48-
eval_hash: str
49-
message: str
50-
eval_job: str
51-
agent_job: str
52-
version: datetime
53-
54-
55-
INSERT_COLUMNS: tuple[str, ...] = tuple(f.name for f in fields(VerdictRow))
56-
57-
58-
def insert_verdict_row(client: ClickHouseClient, row: VerdictRow) -> None:
59-
client.insert(TABLE, [list(astuple(row))], column_names=list(INSERT_COLUMNS))
60-
61-
6227
def _require_env(name: str) -> str:
6328
value = os.environ.get(name)
6429
if not value:
@@ -92,15 +57,15 @@ def _ensure_clickhouse_cloud_no_proxy() -> None:
9257
os.environ[var] = ",".join([*entries, _CLICKHOUSE_CLOUD_DOMAIN])
9358

9459

95-
def connect() -> ClickHouseClient:
60+
def connect() -> Client:
9661
host = _host_from_env()
9762
username = _require_env("CLICKHOUSE_USERNAME")
9863
password = _require_env("CLICKHOUSE_PASSWORD")
9964
port = _port_from_env()
10065
_ensure_clickhouse_cloud_no_proxy()
10166
import clickhouse_connect # lazy: keeps this module importable without the dep
10267

103-
client: ClickHouseClient = clickhouse_connect.get_client(
68+
client: Client = clickhouse_connect.get_client(
10469
host=host,
10570
username=username,
10671
password=password,

0 commit comments

Comments
 (0)