github-actions[bot] commented on code in PR #66119:
URL: https://github.com/apache/doris/pull/66119#discussion_r3662762978
##########
.github/workflows/code-review-runner.yml:
##########
@@ -493,44 +578,186 @@ jobs:
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ OSS_AK: ${{ secrets.OSS_AK }}
+ OSS_SK: ${{ secrets.OSS_SK }}
+ OSS_ENDPOINT: oss-cn-hongkong.aliyuncs.com
+ OSS_CODEX_AUTH_STATUS_OBJECT:
oss://doris-community-ci/codex/auth-status.json
+ AUTH_UNAVAILABLE: ${{ steps.configure_auth.outputs.auth_unavailable
}}
+ CONFIGURED_ALL_QUOTA_EXHAUSTED: ${{
steps.configure_auth.outputs.all_quota_exhausted }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ inputs.pr_number }}
HEAD_SHA: ${{ inputs.head_sha }}
run: |
- GOAL_PROMPT="$(cat "$REVIEW_CONTEXT_DIR/codex_goal_prompt.txt")"
- review_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ write_review_output() {
+ local key="$1"
+ local value="$2"
+ {
+ echo "${key}<<EOF"
+ printf '%s\n' "$value"
+ echo "EOF"
+ } >> "$GITHUB_OUTPUT"
+ }
+
+ auth_object_args=()
+ known_auth_object_args=()
+ while IFS= read -r auth_object; do
+ auth_object_args+=(--auth-object "$auth_object")
+ known_auth_object_args+=(--known-auth-object "$auth_object")
+ done < "$CODEX_AUTH_OBJECTS_FILE"
+ if [ "${#auth_object_args[@]}" -eq 0 ]; then
+ write_review_output "all_quota_exhausted" "false"
+ write_review_output "failure_reason" "No valid Codex auth account
is available for review."
+ exit 1
+ fi
- set +e
- # GitHub-hosted runners are ephemeral. Avoid workspace-write here
because
- # Codex uses bubblewrap for that mode and uid maps can be
unavailable.
- codex exec --goal "$GOAL_PROMPT" \
- --cd "$GITHUB_WORKSPACE" \
- --model "gpt-5.6-sol" \
- --config "model_reasoning_effort=xhigh" \
- --sandbox danger-full-access \
- --color never \
- --json \
- --output-last-message
"$REVIEW_CONTEXT_DIR/codex-final-message.txt" \
- > "$REVIEW_CONTEXT_DIR/codex-events.jsonl" \
- 2> >(tee "$REVIEW_CONTEXT_DIR/codex-stderr.log" >&2)
- status=$?
- set -e
+ sync_auth_state() {
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$CODEX_AUTH_STATE_FILE" "$OSS_CODEX_AUTH_STATUS_OBJECT"
+ }
+
+ sync_current_auth() {
+ if [ -s "$CODEX_HOME/auth.json" ] && jq -e '
+ .auth_mode == "chatgpt"
+ and (.tokens.access_token | type == "string" and length > 0)
+ and (.tokens.refresh_token | type == "string" and length > 0)
+ ' "$CODEX_HOME/auth.json" >/dev/null; then
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$CODEX_HOME/auth.json" "$CODEX_AUTH_OSS_OBJECT"
+ else
+ echo "The current Codex auth file is invalid; it will not be
synced to OSS."
+ fi
+ }
+
+ record_auth_result() {
+ local result="$1"
+ local http_status="$2"
+ local record_args=(
+ record
+ --state "$CODEX_AUTH_STATE_FILE"
+ --auth-object "$CODEX_AUTH_OSS_OBJECT"
+ "${known_auth_object_args[@]}"
+ --result "$result"
+ )
+ if [ -n "$http_status" ]; then
+ record_args+=(--http-status "$http_status")
+ fi
+ "$CODEX_AUTH_STATE_HELPER" "${record_args[@]}"
+ }
+
+ select_next_auth() {
+ local selection_json
+ selection_json="$("$CODEX_AUTH_STATE_HELPER" select \
+ --state "$CODEX_AUTH_STATE_FILE" \
+ "${auth_object_args[@]}")"
+ sync_auth_state
+ selected_auth_object="$(jq -r '.auth_object // empty'
<<<"$selection_json")"
+ all_quota_exhausted="$(jq -r '.all_quota_exhausted'
<<<"$selection_json")"
+ next_retry_at="$(jq -r '.next_retry_at // empty'
<<<"$selection_json")"
+ }
+
+ if [ "$AUTH_UNAVAILABLE" = "true" ]; then
+ write_review_output "all_quota_exhausted"
"$CONFIGURED_ALL_QUOTA_EXHAUSTED"
+ write_review_output "failure_reason" "No Codex auth account is
currently eligible for review."
+ exit 1
+ fi
+ GOAL_PROMPT="$(cat "$REVIEW_CONTEXT_DIR/codex_goal_prompt.txt")"
failure_reason=""
- if [ "$status" -ne 0 ]; then
- if [ -s "$REVIEW_CONTEXT_DIR/codex-events.jsonl" ]; then
- failure_reason="$(jq -r 'select(.type == "turn.failed") |
.error.message // empty' "$REVIEW_CONTEXT_DIR/codex-events.jsonl" | tail -n 1)"
+ all_quota_exhausted="${CONFIGURED_ALL_QUOTA_EXHAUSTED:-false}"
+ transient_attempt=0
+ while :; do
+ review_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ events_file="$REVIEW_CONTEXT_DIR/codex-events.jsonl"
+ stderr_file="$REVIEW_CONTEXT_DIR/codex-stderr.log"
+
+ set +e
+ # GitHub-hosted runners are ephemeral. Avoid workspace-write here
because
+ # Codex uses bubblewrap for that mode and uid maps can be
unavailable.
+ codex exec --goal "$GOAL_PROMPT" \
+ --cd "$GITHUB_WORKSPACE" \
+ --model "gpt-5.6-sol" \
+ --config "model_reasoning_effort=xhigh" \
+ --sandbox danger-full-access \
+ --color never \
+ --json \
+ --output-last-message
"$REVIEW_CONTEXT_DIR/codex-final-message.txt" \
+ > "$events_file" \
+ 2> >(tee "$stderr_file" >&2)
+ status=$?
+ set -e
+
+ if [ "$status" -eq 0 ]; then
+ state_result="$(record_auth_result available "")"
+ sync_auth_state
+ all_quota_exhausted="$(jq -r '.all_quota_exhausted'
<<<"$state_result")"
+ break
+ fi
+
+ if [ -s "$events_file" ]; then
+ failure_reason="$(jq -r 'select(.type == "turn.failed") |
.error.message // empty' "$events_file" | tail -n 1)"
if [ -z "$failure_reason" ]; then
- failure_reason="$(jq -r 'select(.type == "error") | .message
// .error.message // empty' "$REVIEW_CONTEXT_DIR/codex-events.jsonl" | tail -n
1)"
+ failure_reason="$(jq -r 'select(.type == "error") | .message
// .error.message // empty' "$events_file" | tail -n 1)"
fi
fi
- if [ -z "$failure_reason" ] && [ -s
"$REVIEW_CONTEXT_DIR/codex-stderr.log" ]; then
- failure_reason="$(awk 'NF { line = $0 } END { print line }'
"$REVIEW_CONTEXT_DIR/codex-stderr.log")"
+ if [ -z "$failure_reason" ] && [ -s "$stderr_file" ]; then
+ failure_reason="$(awk 'NF { line = $0 } END { print line }'
"$stderr_file")"
fi
if [ -z "$failure_reason" ]; then
failure_reason="Codex exited with status $status"
fi
- fi
+
+ classification="$("$CODEX_AUTH_STATE_HELPER" classify --input
"$events_file" --input "$stderr_file")"
Review Comment:
[P1] Do not execute agent-writable control files with OSS authority
`codex exec` runs as the same runner user with `danger-full-access`, so it
can replace `$RUNNER_TEMP/codex_auth_state.py` or forge
`$CODEX_HOME/auth-status.json` before returning. The secret-bearing parent then
executes that mutable helper here (and from `record_auth_result`) and uploads
the mutable state. Consequently, merely unsetting `OSS_AK`/`OSS_SK` for the
child would still let a replacement helper inherit and use the parent's keys,
or persist forged cooldowns for healthy accounts. Please move post-agent
classification/state/auth orchestration behind a separate UID/container/job
boundary with an immutable freshly verified helper and accept only a narrowly
validated result from the agent. This is a CI secret-boundary defect, not a
Doris runtime vulnerability.
##########
.github/workflows/code-review-runner.yml:
##########
@@ -493,44 +578,186 @@ jobs:
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ OSS_AK: ${{ secrets.OSS_AK }}
Review Comment:
[P1] Keep the OSS credentials out of the agent process
Making `OSS_AK`/`OSS_SK` step-scoped means `codex exec` and every shell tool
it launches inherit them; the generated config explicitly uses
`shell_environment_policy.inherit = "all"` and `danger-full-access`. Because
this job reviews PR-controlled source, a prompt-injected or accidental
diagnostic command can now use the installed `ossutil` to list/read/write every
object allowed by these keys—including other auth files, shared state,
memories, and sessions—rather than only the selected `auth.json`. Please
isolate the secret-bearing rotation/sync wrapper from the agent process (or use
a narrowly scoped broker/short-lived credential) and ensure the Codex process
and its parent environment cannot expose the OSS keys. This is a CI
secret-boundary defect, not a Doris runtime vulnerability under the repository
threat model.
##########
.github/workflows/code-review-runner.yml:
##########
@@ -493,44 +578,186 @@ jobs:
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ OSS_AK: ${{ secrets.OSS_AK }}
+ OSS_SK: ${{ secrets.OSS_SK }}
+ OSS_ENDPOINT: oss-cn-hongkong.aliyuncs.com
+ OSS_CODEX_AUTH_STATUS_OBJECT:
oss://doris-community-ci/codex/auth-status.json
+ AUTH_UNAVAILABLE: ${{ steps.configure_auth.outputs.auth_unavailable
}}
+ CONFIGURED_ALL_QUOTA_EXHAUSTED: ${{
steps.configure_auth.outputs.all_quota_exhausted }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ inputs.pr_number }}
HEAD_SHA: ${{ inputs.head_sha }}
run: |
- GOAL_PROMPT="$(cat "$REVIEW_CONTEXT_DIR/codex_goal_prompt.txt")"
- review_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ write_review_output() {
+ local key="$1"
+ local value="$2"
+ {
+ echo "${key}<<EOF"
+ printf '%s\n' "$value"
+ echo "EOF"
+ } >> "$GITHUB_OUTPUT"
+ }
+
+ auth_object_args=()
+ known_auth_object_args=()
+ while IFS= read -r auth_object; do
+ auth_object_args+=(--auth-object "$auth_object")
+ known_auth_object_args+=(--known-auth-object "$auth_object")
+ done < "$CODEX_AUTH_OBJECTS_FILE"
+ if [ "${#auth_object_args[@]}" -eq 0 ]; then
+ write_review_output "all_quota_exhausted" "false"
+ write_review_output "failure_reason" "No valid Codex auth account
is available for review."
+ exit 1
+ fi
- set +e
- # GitHub-hosted runners are ephemeral. Avoid workspace-write here
because
- # Codex uses bubblewrap for that mode and uid maps can be
unavailable.
- codex exec --goal "$GOAL_PROMPT" \
- --cd "$GITHUB_WORKSPACE" \
- --model "gpt-5.6-sol" \
- --config "model_reasoning_effort=xhigh" \
- --sandbox danger-full-access \
- --color never \
- --json \
- --output-last-message
"$REVIEW_CONTEXT_DIR/codex-final-message.txt" \
- > "$REVIEW_CONTEXT_DIR/codex-events.jsonl" \
- 2> >(tee "$REVIEW_CONTEXT_DIR/codex-stderr.log" >&2)
- status=$?
- set -e
+ sync_auth_state() {
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$CODEX_AUTH_STATE_FILE" "$OSS_CODEX_AUTH_STATUS_OBJECT"
+ }
+
+ sync_current_auth() {
+ if [ -s "$CODEX_HOME/auth.json" ] && jq -e '
+ .auth_mode == "chatgpt"
+ and (.tokens.access_token | type == "string" and length > 0)
+ and (.tokens.refresh_token | type == "string" and length > 0)
+ ' "$CODEX_HOME/auth.json" >/dev/null; then
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$CODEX_HOME/auth.json" "$CODEX_AUTH_OSS_OBJECT"
+ else
+ echo "The current Codex auth file is invalid; it will not be
synced to OSS."
+ fi
+ }
+
+ record_auth_result() {
+ local result="$1"
+ local http_status="$2"
+ local record_args=(
+ record
+ --state "$CODEX_AUTH_STATE_FILE"
+ --auth-object "$CODEX_AUTH_OSS_OBJECT"
+ "${known_auth_object_args[@]}"
+ --result "$result"
+ )
+ if [ -n "$http_status" ]; then
+ record_args+=(--http-status "$http_status")
+ fi
+ "$CODEX_AUTH_STATE_HELPER" "${record_args[@]}"
+ }
+
+ select_next_auth() {
+ local selection_json
+ selection_json="$("$CODEX_AUTH_STATE_HELPER" select \
+ --state "$CODEX_AUTH_STATE_FILE" \
+ "${auth_object_args[@]}")"
+ sync_auth_state
+ selected_auth_object="$(jq -r '.auth_object // empty'
<<<"$selection_json")"
+ all_quota_exhausted="$(jq -r '.all_quota_exhausted'
<<<"$selection_json")"
+ next_retry_at="$(jq -r '.next_retry_at // empty'
<<<"$selection_json")"
+ }
+
+ if [ "$AUTH_UNAVAILABLE" = "true" ]; then
+ write_review_output "all_quota_exhausted"
"$CONFIGURED_ALL_QUOTA_EXHAUSTED"
+ write_review_output "failure_reason" "No Codex auth account is
currently eligible for review."
+ exit 1
+ fi
+ GOAL_PROMPT="$(cat "$REVIEW_CONTEXT_DIR/codex_goal_prompt.txt")"
failure_reason=""
- if [ "$status" -ne 0 ]; then
- if [ -s "$REVIEW_CONTEXT_DIR/codex-events.jsonl" ]; then
- failure_reason="$(jq -r 'select(.type == "turn.failed") |
.error.message // empty' "$REVIEW_CONTEXT_DIR/codex-events.jsonl" | tail -n 1)"
+ all_quota_exhausted="${CONFIGURED_ALL_QUOTA_EXHAUSTED:-false}"
+ transient_attempt=0
+ while :; do
+ review_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ events_file="$REVIEW_CONTEXT_DIR/codex-events.jsonl"
+ stderr_file="$REVIEW_CONTEXT_DIR/codex-stderr.log"
+
+ set +e
+ # GitHub-hosted runners are ephemeral. Avoid workspace-write here
because
+ # Codex uses bubblewrap for that mode and uid maps can be
unavailable.
+ codex exec --goal "$GOAL_PROMPT" \
Review Comment:
[P1] Do not replay the full review after side effects may have landed
`codex exec --goal` is not side-effect-free: the required goal submits
GitHub reviews/inline comments before its final verification and completion
turns. If that later turn gets a 503, connection reset, or credential failure,
this loop starts a fresh full goal against the pre-run thread snapshot, so it
can submit the same review again. Resetting `review_started_at` also makes the
final verifier ignore the first attempt's landed review. Please keep retries
before the submission boundary, or persist/reconcile a frozen review payload
and refresh live reviews/comments before any retry.
##########
.github/workflows/code-review-runner.yml:
##########
@@ -101,14 +115,84 @@ jobs:
OSS_CODEX_GOAL_FALLBACK_OBJECT:
oss://doris-community-ci/codex/codex-goal
- name: Configure Codex auth
+ id: configure_auth
run: |
install -m 700 -d "$RUNNER_TEMP/codex-home"
printf 'CODEX_HOME=%s\n' "$RUNNER_TEMP/codex-home" >> "$GITHUB_ENV"
- auth_object="$(printf '%s\n' \
- 'oss://doris-community-ci/codex/auth.json.1' \
- 'oss://doris-community-ci/codex/auth.json.2' \
- | shuf -n 1)"
+ auth_prefix='oss://doris-community-ci/codex/auth.json.'
+ auth_listing="$RUNNER_TEMP/codex-auth-objects.list"
+ if ! ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" ls
"$auth_prefix" -s > "$auth_listing"; then
+ echo 'Failed to list Codex auth objects from OSS.'
+ exit 1
+ fi
+
+ auth_objects_file="$RUNNER_TEMP/codex-home/auth-objects.txt"
+ candidate_auth_file="$(mktemp "$RUNNER_TEMP/codex-auth.XXXXXX")"
+ trap 'rm -f "$candidate_auth_file"' EXIT
+ while IFS= read -r candidate; do
+ if ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f
"$candidate" "$candidate_auth_file" \
+ && test -s "$candidate_auth_file" \
+ && jq -e '
+ .auth_mode == "chatgpt"
+ and (.tokens.access_token | type == "string" and length > 0)
+ and (.tokens.refresh_token | type == "string" and length > 0)
+ ' "$candidate_auth_file" >/dev/null; then
+ printf '%s\n' "$candidate" >> "$auth_objects_file"
+ else
+ echo "Skipping invalid Codex auth object: ${candidate##*/}"
+ fi
+ done < <(
+ awk '$0 ~
/^oss:\/\/doris-community-ci\/codex\/auth\.json\.[0-9]+$/ { print }'
"$auth_listing" | shuf
Review Comment:
[P2] Keep the persisted cursor tied to a stable account order
`next_account` is only an integer index, but this `shuf` gives that index a
different meaning on every job. For example, a run over `[A, B, C]` selects `A`
and stores `1`; the next run over `[C, A, B]` starts at index 1 and selects `A`
again. Thus the helper's round-robin test does not hold in the actual workflow,
and sequential jobs can repeatedly choose the same credential. Please sort the
discovered objects before selection (or persist the next object's identity) and
add a test that reloads state with a reordered list.
##########
.github/scripts/codex_auth_state.py:
##########
@@ -0,0 +1,293 @@
+#!/usr/bin/env python3
+"""Persist and select the rotating credentials used by code review jobs."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import tempfile
+from dataclasses import dataclass
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+from typing import Any
+
+
+STATE_VERSION = 1
+AVAILABLE = "available"
+QUOTA_EXHAUSTED = "quota_exhausted"
+RATE_LIMITED = "rate_limited"
+AUTHENTICATION_FAILED = "authentication_failed"
+TRANSIENT_FAILURE = "transient_failure"
+STATUS_VALUES = {AVAILABLE, QUOTA_EXHAUSTED, RATE_LIMITED,
AUTHENTICATION_FAILED}
+QUOTA_RETRY_DELAY = timedelta(days=1)
+RATE_LIMIT_RETRY_DELAY = timedelta(hours=1)
+AUTHENTICATION_RETRY_DELAY = timedelta(days=1)
+
+
+@dataclass(frozen=True)
+class Selection:
+ auth_object: str | None
+ all_quota_exhausted: bool
+ next_retry_at: str | None
+
+
+def utc_now() -> datetime:
+ return datetime.now(timezone.utc).replace(microsecond=0)
+
+
+def parse_timestamp(value: str) -> datetime:
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
+ if parsed.tzinfo is None:
+ raise ValueError(f"Timestamp must include a timezone: {value!r}")
+ return parsed.astimezone(timezone.utc).replace(microsecond=0)
+
+
+def format_timestamp(value: datetime) -> str:
+ return
value.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00",
"Z")
+
+
+def default_account_state() -> dict[str, Any]:
+ return {
+ "status": AVAILABLE,
+ "last_failure_at": None,
+ "last_http_status": None,
+ "last_success_at": None,
+ "last_recovered_at": None,
+ "retry_after": None,
+ }
+
+
+def default_state() -> dict[str, Any]:
+ return {"version": STATE_VERSION, "next_account": 0, "accounts": {}}
+
+
+def load_state(path: Path) -> dict[str, Any]:
+ if not path.exists():
+ return default_state()
+
+ state = json.loads(path.read_text())
+ if not isinstance(state, dict):
+ raise ValueError("Authentication state must be a JSON object")
+ if state.get("version") != STATE_VERSION:
+ raise ValueError(f"Unsupported authentication state version:
{state.get('version')!r}")
+ if not isinstance(state.get("next_account"), int):
+ raise ValueError("Authentication state next_account must be an
integer")
+ if not isinstance(state.get("accounts"), dict):
+ raise ValueError("Authentication state accounts must be an object")
+ return state
+
+
+def save_state(path: Path, state: dict[str, Any]) -> None:
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent,
delete=False) as temporary:
+ json.dump(state, temporary, indent=2, sort_keys=True)
+ temporary.write("\n")
+ temporary_path = Path(temporary.name)
+ temporary_path.replace(path)
+
+
+def account_state(state: dict[str, Any], auth_object: str) -> dict[str, Any]:
+ accounts = state["accounts"]
+ entry = accounts.setdefault(auth_object, default_account_state())
+ if not isinstance(entry, dict):
+ raise ValueError(f"Authentication state for {auth_object!r} must be an
object")
+ status = entry.get("status", AVAILABLE)
+ if status not in STATUS_VALUES:
+ raise ValueError(f"Unknown authentication status for {auth_object!r}:
{status!r}")
+ entry.setdefault("status", AVAILABLE)
+ entry.setdefault("last_failure_at", None)
+ entry.setdefault("last_http_status", None)
+ entry.setdefault("last_success_at", None)
+ entry.setdefault("last_recovered_at", None)
+ entry.setdefault("retry_after", None)
+ return entry
+
+
+def recover_expired_accounts(state: dict[str, Any], auth_objects: list[str],
now: datetime) -> None:
+ for auth_object in auth_objects:
+ entry = account_state(state, auth_object)
+ retry_after = entry["retry_after"]
+ if retry_after is None:
+ continue
+ if parse_timestamp(retry_after) <= now:
+ entry["status"] = AVAILABLE
+ entry["retry_after"] = None
+ entry["last_recovered_at"] = format_timestamp(now)
+
+
+def next_retry_at(state: dict[str, Any], auth_objects: list[str]) -> str |
None:
+ retry_at = [
+ entry["retry_after"]
+ for auth_object in auth_objects
+ for entry in [account_state(state, auth_object)]
+ if entry["retry_after"] is not None
+ ]
+ return min(retry_at, key=parse_timestamp, default=None)
+
+
+def all_quota_exhausted(state: dict[str, Any], auth_objects: list[str]) ->
bool:
+ return bool(auth_objects) and all(
+ account_state(state, auth_object)["status"] == QUOTA_EXHAUSTED for
auth_object in auth_objects
+ )
+
+
+def select_auth_object(state: dict[str, Any], auth_objects: list[str], now:
datetime) -> Selection:
+ if not auth_objects:
+ raise ValueError("At least one auth object is required")
+
+ recover_expired_accounts(state, auth_objects, now)
+ starting_index = state["next_account"] % len(auth_objects)
+ for offset in range(len(auth_objects)):
+ index = (starting_index + offset) % len(auth_objects)
+ auth_object = auth_objects[index]
+ if account_state(state, auth_object)["status"] == AVAILABLE:
+ state["next_account"] = (index + 1) % len(auth_objects)
+ return Selection(auth_object, False, None)
+
+ return Selection(None, all_quota_exhausted(state, auth_objects),
next_retry_at(state, auth_objects))
+
+
+def record_result(
+ state: dict[str, Any], auth_object: str, result: str, now: datetime,
http_status: int | None = None
+) -> None:
+ entry = account_state(state, auth_object)
+ timestamp = format_timestamp(now)
+ if result == AVAILABLE:
+ entry["status"] = AVAILABLE
+ entry["last_success_at"] = timestamp
+ entry["retry_after"] = None
+ return
+
+ if result == QUOTA_EXHAUSTED:
+ retry_delay = QUOTA_RETRY_DELAY
+ http_status = http_status or 403
+ elif result == RATE_LIMITED:
+ retry_delay = RATE_LIMIT_RETRY_DELAY
+ http_status = http_status or 429
+ elif result == AUTHENTICATION_FAILED:
+ retry_delay = AUTHENTICATION_RETRY_DELAY
+ http_status = http_status or 401
+ elif result == TRANSIENT_FAILURE:
+ entry["status"] = AVAILABLE
+ entry["last_failure_at"] = timestamp
+ entry["last_http_status"] = http_status
+ entry["retry_after"] = None
+ return
+ else:
+ raise ValueError(f"Unsupported result: {result!r}")
+
+ entry["status"] = result
+ entry["last_failure_at"] = timestamp
+ entry["last_http_status"] = http_status
+ entry["retry_after"] = format_timestamp(now + retry_delay)
+
+
+def extract_http_status(text: str) -> int | None:
+ labelled_statuses = re.findall(
+
r"(?:http(?:\s+status)?|status(?:\s+code)?|error\s+code)\s*[:=]?\s*([1-5]\d{2})",
+ text,
+ re.IGNORECASE,
+ )
+ if labelled_statuses:
+ return int(labelled_statuses[-1])
+ matches = re.findall(r"(?<!\d)([1-5]\d{2})(?!\d)", text)
+ return int(matches[0]) if matches else None
+
+
+def classify_failure(text: str) -> str:
Review Comment:
[P1] Handle the actual Codex usage-limit error
The failure that motivated related PR #66111 does not contain an HTTP
status: run 30259990434 emitted `You've hit your usage limit ... try again at
Aug 2nd, 2026 1:27 AM.` Feeding that exact message to this helper returns
`fatal`, so the workflow takes the generic break at lines 726-727 without
recording quota or selecting another credential. This leaves the core
quota-rotation path ineffective. Please classify the structured/terminal
usage-limit error itself, honor its supplied reset time rather than the fixed
one-day cooldown, and add this exact runner message as a regression fixture.
##########
.github/workflows/code-review-runner.yml:
##########
@@ -101,14 +115,84 @@ jobs:
OSS_CODEX_GOAL_FALLBACK_OBJECT:
oss://doris-community-ci/codex/codex-goal
- name: Configure Codex auth
+ id: configure_auth
run: |
install -m 700 -d "$RUNNER_TEMP/codex-home"
printf 'CODEX_HOME=%s\n' "$RUNNER_TEMP/codex-home" >> "$GITHUB_ENV"
- auth_object="$(printf '%s\n' \
- 'oss://doris-community-ci/codex/auth.json.1' \
- 'oss://doris-community-ci/codex/auth.json.2' \
- | shuf -n 1)"
+ auth_prefix='oss://doris-community-ci/codex/auth.json.'
+ auth_listing="$RUNNER_TEMP/codex-auth-objects.list"
+ if ! ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" ls
"$auth_prefix" -s > "$auth_listing"; then
+ echo 'Failed to list Codex auth objects from OSS.'
+ exit 1
+ fi
+
+ auth_objects_file="$RUNNER_TEMP/codex-home/auth-objects.txt"
+ candidate_auth_file="$(mktemp "$RUNNER_TEMP/codex-auth.XXXXXX")"
+ trap 'rm -f "$candidate_auth_file"' EXIT
+ while IFS= read -r candidate; do
+ if ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f
"$candidate" "$candidate_auth_file" \
+ && test -s "$candidate_auth_file" \
+ && jq -e '
+ .auth_mode == "chatgpt"
+ and (.tokens.access_token | type == "string" and length > 0)
+ and (.tokens.refresh_token | type == "string" and length > 0)
+ ' "$candidate_auth_file" >/dev/null; then
+ printf '%s\n' "$candidate" >> "$auth_objects_file"
+ else
+ echo "Skipping invalid Codex auth object: ${candidate##*/}"
+ fi
+ done < <(
+ awk '$0 ~
/^oss:\/\/doris-community-ci\/codex\/auth\.json\.[0-9]+$/ { print }'
"$auth_listing" | shuf
+ )
+
+ if [ ! -s "$auth_objects_file" ]; then
+ echo 'No valid Codex auth object found in OSS.'
+ exit 1
+ fi
+ chmod 600 "$auth_objects_file"
+
+ auth_object_args=()
+ while IFS= read -r auth_object; do
+ auth_object_args+=(--auth-object "$auth_object")
+ done < "$auth_objects_file"
+
+ state_file="$RUNNER_TEMP/codex-home/auth-status.json"
+ set +e
+ state_stat_output="$(ossutil -i "$OSS_AK" -k "$OSS_SK" -e
"$OSS_ENDPOINT" stat "$OSS_CODEX_AUTH_STATUS_OBJECT" 2>&1)"
+ state_stat_status=$?
+ set -e
+ if [ "$state_stat_status" -eq 0 ]; then
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f
"$OSS_CODEX_AUTH_STATUS_OBJECT" "$state_file"
+ elif grep -Eqi 'NoSuchKey|NoSuchObject|404' <<<"$state_stat_output";
then
+ "$CODEX_AUTH_STATE_HELPER" initialize \
+ --state "$state_file" \
+ "${auth_object_args[@]}"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f
"$state_file" "$OSS_CODEX_AUTH_STATUS_OBJECT"
+ echo "Created Codex auth status file in OSS."
+ else
+ echo "Unable to determine whether the Codex auth status file
exists." >&2
+ exit 1
+ fi
+
+ selection_json="$("$CODEX_AUTH_STATE_HELPER" select \
+ --state "$state_file" \
+ "${auth_object_args[@]}")"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f
"$state_file" "$OSS_CODEX_AUTH_STATUS_OBJECT"
Review Comment:
[P1] Coordinate updates to the shared auth state
This is a read/modify/write of one repository-global OSS object, but
code-review runs are allowed to overlap and every sync is an unconditional `cp
-f`. Two jobs can download the same cursor, both select account A, and then a
stale success upload from one job can overwrite the other's quota cooldown; the
refreshed `auth.json` uploads have the same last-writer-wins race. Please
serialize this critical section/run set, or use per-account leases plus
conditional/versioned writes and conflict retries for both state and token
refreshes.
##########
.github/scripts/codex_auth_state.py:
##########
@@ -0,0 +1,293 @@
+#!/usr/bin/env python3
+"""Persist and select the rotating credentials used by code review jobs."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import tempfile
+from dataclasses import dataclass
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+from typing import Any
+
+
+STATE_VERSION = 1
+AVAILABLE = "available"
+QUOTA_EXHAUSTED = "quota_exhausted"
+RATE_LIMITED = "rate_limited"
+AUTHENTICATION_FAILED = "authentication_failed"
+TRANSIENT_FAILURE = "transient_failure"
+STATUS_VALUES = {AVAILABLE, QUOTA_EXHAUSTED, RATE_LIMITED,
AUTHENTICATION_FAILED}
+QUOTA_RETRY_DELAY = timedelta(days=1)
+RATE_LIMIT_RETRY_DELAY = timedelta(hours=1)
+AUTHENTICATION_RETRY_DELAY = timedelta(days=1)
+
+
+@dataclass(frozen=True)
+class Selection:
+ auth_object: str | None
+ all_quota_exhausted: bool
+ next_retry_at: str | None
+
+
+def utc_now() -> datetime:
+ return datetime.now(timezone.utc).replace(microsecond=0)
+
+
+def parse_timestamp(value: str) -> datetime:
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
+ if parsed.tzinfo is None:
+ raise ValueError(f"Timestamp must include a timezone: {value!r}")
+ return parsed.astimezone(timezone.utc).replace(microsecond=0)
+
+
+def format_timestamp(value: datetime) -> str:
+ return
value.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00",
"Z")
+
+
+def default_account_state() -> dict[str, Any]:
+ return {
+ "status": AVAILABLE,
+ "last_failure_at": None,
+ "last_http_status": None,
+ "last_success_at": None,
+ "last_recovered_at": None,
+ "retry_after": None,
+ }
+
+
+def default_state() -> dict[str, Any]:
+ return {"version": STATE_VERSION, "next_account": 0, "accounts": {}}
+
+
+def load_state(path: Path) -> dict[str, Any]:
+ if not path.exists():
+ return default_state()
+
+ state = json.loads(path.read_text())
+ if not isinstance(state, dict):
+ raise ValueError("Authentication state must be a JSON object")
+ if state.get("version") != STATE_VERSION:
+ raise ValueError(f"Unsupported authentication state version:
{state.get('version')!r}")
+ if not isinstance(state.get("next_account"), int):
+ raise ValueError("Authentication state next_account must be an
integer")
+ if not isinstance(state.get("accounts"), dict):
+ raise ValueError("Authentication state accounts must be an object")
+ return state
+
+
+def save_state(path: Path, state: dict[str, Any]) -> None:
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent,
delete=False) as temporary:
+ json.dump(state, temporary, indent=2, sort_keys=True)
+ temporary.write("\n")
+ temporary_path = Path(temporary.name)
+ temporary_path.replace(path)
+
+
+def account_state(state: dict[str, Any], auth_object: str) -> dict[str, Any]:
+ accounts = state["accounts"]
+ entry = accounts.setdefault(auth_object, default_account_state())
+ if not isinstance(entry, dict):
+ raise ValueError(f"Authentication state for {auth_object!r} must be an
object")
+ status = entry.get("status", AVAILABLE)
+ if status not in STATUS_VALUES:
+ raise ValueError(f"Unknown authentication status for {auth_object!r}:
{status!r}")
+ entry.setdefault("status", AVAILABLE)
+ entry.setdefault("last_failure_at", None)
+ entry.setdefault("last_http_status", None)
+ entry.setdefault("last_success_at", None)
+ entry.setdefault("last_recovered_at", None)
+ entry.setdefault("retry_after", None)
+ return entry
+
+
+def recover_expired_accounts(state: dict[str, Any], auth_objects: list[str],
now: datetime) -> None:
+ for auth_object in auth_objects:
+ entry = account_state(state, auth_object)
+ retry_after = entry["retry_after"]
+ if retry_after is None:
+ continue
+ if parse_timestamp(retry_after) <= now:
+ entry["status"] = AVAILABLE
+ entry["retry_after"] = None
+ entry["last_recovered_at"] = format_timestamp(now)
+
+
+def next_retry_at(state: dict[str, Any], auth_objects: list[str]) -> str |
None:
+ retry_at = [
+ entry["retry_after"]
+ for auth_object in auth_objects
+ for entry in [account_state(state, auth_object)]
+ if entry["retry_after"] is not None
+ ]
+ return min(retry_at, key=parse_timestamp, default=None)
+
+
+def all_quota_exhausted(state: dict[str, Any], auth_objects: list[str]) ->
bool:
+ return bool(auth_objects) and all(
+ account_state(state, auth_object)["status"] == QUOTA_EXHAUSTED for
auth_object in auth_objects
+ )
+
+
+def select_auth_object(state: dict[str, Any], auth_objects: list[str], now:
datetime) -> Selection:
+ if not auth_objects:
+ raise ValueError("At least one auth object is required")
+
+ recover_expired_accounts(state, auth_objects, now)
+ starting_index = state["next_account"] % len(auth_objects)
+ for offset in range(len(auth_objects)):
+ index = (starting_index + offset) % len(auth_objects)
+ auth_object = auth_objects[index]
+ if account_state(state, auth_object)["status"] == AVAILABLE:
+ state["next_account"] = (index + 1) % len(auth_objects)
+ return Selection(auth_object, False, None)
+
+ return Selection(None, all_quota_exhausted(state, auth_objects),
next_retry_at(state, auth_objects))
+
+
+def record_result(
+ state: dict[str, Any], auth_object: str, result: str, now: datetime,
http_status: int | None = None
+) -> None:
+ entry = account_state(state, auth_object)
+ timestamp = format_timestamp(now)
+ if result == AVAILABLE:
+ entry["status"] = AVAILABLE
+ entry["last_success_at"] = timestamp
+ entry["retry_after"] = None
+ return
+
+ if result == QUOTA_EXHAUSTED:
+ retry_delay = QUOTA_RETRY_DELAY
+ http_status = http_status or 403
+ elif result == RATE_LIMITED:
+ retry_delay = RATE_LIMIT_RETRY_DELAY
+ http_status = http_status or 429
+ elif result == AUTHENTICATION_FAILED:
+ retry_delay = AUTHENTICATION_RETRY_DELAY
+ http_status = http_status or 401
+ elif result == TRANSIENT_FAILURE:
+ entry["status"] = AVAILABLE
+ entry["last_failure_at"] = timestamp
+ entry["last_http_status"] = http_status
+ entry["retry_after"] = None
+ return
+ else:
+ raise ValueError(f"Unsupported result: {result!r}")
+
+ entry["status"] = result
+ entry["last_failure_at"] = timestamp
+ entry["last_http_status"] = http_status
+ entry["retry_after"] = format_timestamp(now + retry_delay)
+
+
+def extract_http_status(text: str) -> int | None:
+ labelled_statuses = re.findall(
+
r"(?:http(?:\s+status)?|status(?:\s+code)?|error\s+code)\s*[:=]?\s*([1-5]\d{2})",
+ text,
+ re.IGNORECASE,
+ )
+ if labelled_statuses:
+ return int(labelled_statuses[-1])
+ matches = re.findall(r"(?<!\d)([1-5]\d{2})(?!\d)", text)
+ return int(matches[0]) if matches else None
+
+
+def classify_failure(text: str) -> str:
+ http_status = extract_http_status(text)
+ if http_status == 429:
+ return RATE_LIMITED
+ if http_status in {402, 403}:
+ return QUOTA_EXHAUSTED
+ if http_status == 401:
+ return AUTHENTICATION_FAILED
+ if http_status in {408, 409, 425, 500, 502, 503, 504}:
+ return TRANSIENT_FAILURE
+ if re.search(
+ r"connection reset|econnreset|eai_again|network is
unreachable|etimedout|timeout|timed? out",
+ text,
+ re.IGNORECASE,
+ ):
+ return TRANSIENT_FAILURE
+ return "fatal"
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ subcommands = parser.add_subparsers(dest="command", required=True)
+
+ commands_with_state = (
+ subcommands.add_parser("initialize"),
+ subcommands.add_parser("select"),
+ subcommands.add_parser("record"),
+ )
+ for command in commands_with_state:
+ command.add_argument("--state", type=Path, required=True)
+ command.add_argument("--now", type=parse_timestamp)
+
+ initialize = subcommands.choices["initialize"]
+ initialize.add_argument("--auth-object", action="append", required=True)
+
+ select = subcommands.choices["select"]
+ select.add_argument("--auth-object", action="append", required=True)
+
+ record = subcommands.choices["record"]
+ record.add_argument("--auth-object", required=True)
+ record.add_argument(
+ "--result",
+ choices=[AVAILABLE, QUOTA_EXHAUSTED, RATE_LIMITED,
AUTHENTICATION_FAILED, TRANSIENT_FAILURE],
+ required=True,
+ )
+ record.add_argument("--http-status", type=int)
+ record.add_argument("--known-auth-object", action="append", required=True)
+
+ classify = subcommands.add_parser("classify")
+ classify.add_argument("--input", action="append", type=Path, required=True)
+ return parser.parse_args()
+
+
+def main() -> None:
+ args = parse_args()
+ if args.command == "classify":
+ text = "\n".join(path.read_text(errors="replace") for path in
args.input)
Review Comment:
[P1] Classify only the terminal Codex failure payload
These inputs are the complete JSONL/stderr streams, including command
`aggregated_output` and therefore arbitrary reviewed source text. A prior tool
result containing `HTTP 403` followed by an unrelated fatal `turn.failed` is
classified as `quota_exhausted`; the workflow then persists a cooldown for a
healthy account and can repeat this against every credential. Please parse
JSONL structurally and inspect only the latest terminal `turn.failed`/top-level
Codex error (with a tightly scoped stderr fallback), never command output or
arbitrary bare three-digit numbers, and add a contaminated-stream regression
test.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]