github-actions[bot] commented on code in PR #66119:
URL: https://github.com/apache/doris/pull/66119#discussion_r3664929301
##########
.github/workflows/code-review-runner.yml:
##########
@@ -101,26 +127,138 @@ 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"
+ defer_review() {
+ local retry_at="$1" request_id
+ request_id="${DEFERRED_REQUEST_ID:-$GITHUB_RUN_ID}"
+ local request_file
+ request_file="$(mktemp "$RUNNER_TEMP/codex-review-pending.XXXXXX")"
+ jq -n \
+ --arg pr_number "$PR_NUMBER" \
+ --arg head_sha "$HEAD_SHA" \
+ --arg base_sha "$BASE_SHA" \
+ --arg review_focus "$REVIEW_FOCUS" \
+ --arg not_before "$retry_at" \
+ --arg request_id "$request_id" \
+ '{pr_number: $pr_number, head_sha: $head_sha, base_sha:
$base_sha,
+ review_focus: $review_focus, not_before: $not_before,
request_id: $request_id}' > "$request_file"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$request_file"
"$OSS_CODEX_REVIEW_PENDING_PREFIX/${PR_NUMBER}-${HEAD_SHA}-${request_id}.json"
+ rm -f "$request_file"
+ }
+
+ codex_review_user=codex-review
+ sudo useradd --create-home --shell /usr/sbin/nologin
"$codex_review_user"
+ codex_home="$(mktemp -d /tmp/codex-home.XXXXXX)"
+ sudo chown "$codex_review_user:$codex_review_user" "$codex_home"
+ sudo chgrp "$(id -gn)" "$codex_home"
+ sudo chmod 770 "$codex_home"
+ control_dir="$RUNNER_TEMP/codex-auth-control"
+ install -m 700 -d "$control_dir"
+ printf 'CODEX_HOME=%s\n' "$codex_home" >> "$GITHUB_ENV"
+ printf 'CODEX_REVIEW_USER=%s\n' "$codex_review_user" >> "$GITHUB_ENV"
+
+ 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="$control_dir/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)
Review Comment:
[P1] Keep cooldown identity stable across token refreshes
`auth_state_key` includes a SHA-256 of the entire `auth.json`, but Codex
legitimately rewrites access/refresh tokens during a run. This workflow uploads
those new bytes before recording a quota/rate/auth result under the old hash,
so the next job discovers the same OSS object under a new key and treats it as
immediately available. A refresh followed by quota exhaustion therefore
bypasses the persisted cooldown and can repeatedly select the exhausted
account. Key availability by a stable validated account/object identity (with
an explicit replacement generation if needed), and add a cross-job
refresh-then-quota test.
##########
.github/workflows/code-review-runner.yml:
##########
@@ -101,26 +127,138 @@ 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"
+ defer_review() {
+ local retry_at="$1" request_id
+ request_id="${DEFERRED_REQUEST_ID:-$GITHUB_RUN_ID}"
+ local request_file
+ request_file="$(mktemp "$RUNNER_TEMP/codex-review-pending.XXXXXX")"
+ jq -n \
+ --arg pr_number "$PR_NUMBER" \
+ --arg head_sha "$HEAD_SHA" \
+ --arg base_sha "$BASE_SHA" \
+ --arg review_focus "$REVIEW_FOCUS" \
+ --arg not_before "$retry_at" \
+ --arg request_id "$request_id" \
+ '{pr_number: $pr_number, head_sha: $head_sha, base_sha:
$base_sha,
+ review_focus: $review_focus, not_before: $not_before,
request_id: $request_id}' > "$request_file"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$request_file"
"$OSS_CODEX_REVIEW_PENDING_PREFIX/${PR_NUMBER}-${HEAD_SHA}-${request_id}.json"
+ rm -f "$request_file"
+ }
+
+ codex_review_user=codex-review
+ sudo useradd --create-home --shell /usr/sbin/nologin
"$codex_review_user"
+ codex_home="$(mktemp -d /tmp/codex-home.XXXXXX)"
+ sudo chown "$codex_review_user:$codex_review_user" "$codex_home"
+ sudo chgrp "$(id -gn)" "$codex_home"
+ sudo chmod 770 "$codex_home"
+ control_dir="$RUNNER_TEMP/codex-auth-control"
+ install -m 700 -d "$control_dir"
+ printf 'CODEX_HOME=%s\n' "$codex_home" >> "$GITHUB_ENV"
+ printf 'CODEX_REVIEW_USER=%s\n' "$codex_review_user" >> "$GITHUB_ENV"
+
+ 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="$control_dir/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
Review Comment:
[P1] Validate the ID token before admitting an account
This predicate accepts an object with no `tokens.id_token`, but current
Codex requires that field and parses it as a JWT while deserializing
`auth.json` ([schema and
deserializer](https://github.com/openai/codex/blob/e597169e9a783156e50ae9765d891a3dd74df064/codex-rs/login/src/token_data.rs#L10-L23)).
Such an object is selected as “valid,” then Codex fails to load it; that
schema error is classified as fatal, so the workflow never rotates to the
remaining valid objects. Validate every field required by the installed Codex
schema, including a parseable ID token, both during discovery and later
installs.
##########
.github/scripts/codex_auth_state.py:
##########
@@ -0,0 +1,453 @@
+#!/usr/bin/env python3
+"""Persist and select the rotating credentials used by code review jobs."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+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)
+PERMANENT_AUTH_FAILURE_PATTERN = re.compile(
+ r"refresh[\s_-]*token.*(?:revoked|expired|already\s+used)|"
+ r"(?:revoked|expired).*refresh[\s_-]*token",
+ re.IGNORECASE,
+)
+USAGE_LIMIT_MESSAGE_PATTERN = re.compile(
+ r"you(?:'|\u2019)ve hit your usage limit|usage limit",
+ re.IGNORECASE,
+)
+USAGE_LIMIT_RETRY_PATTERN = re.compile(
+ r"try again
at\s+(?:(?P<date>[A-Za-z]{3,9}\s+\d{1,2}(?:st|nd|rd|th)?,\s+\d{4}\s+"
+ r"\d{1,2}:\d{2}\s+(?:AM|PM))|(?P<time>\d{1,2}:\d{2}\s+(?:AM|PM)))",
+ re.IGNORECASE | re.DOTALL,
+)
+
+
+@dataclass(frozen=True)
+class Selection:
+ auth_object: str | None
+ all_quota_exhausted: bool
+ next_retry_at: str | None
+
+
+@dataclass(frozen=True)
+class FailureClassification:
+ kind: str
+ http_status: int | None
+ retry_after: 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 promote_auth(baseline: dict[str, Any], candidate: dict[str, Any]) ->
dict[str, Any]:
+ if baseline.get("auth_mode") != "chatgpt" or candidate.get("auth_mode") !=
"chatgpt":
+ raise ValueError("Codex credentials must use ChatGPT authentication")
+
+ baseline_tokens = baseline.get("tokens")
+ candidate_tokens = candidate.get("tokens")
+ if not isinstance(baseline_tokens, dict) or not
isinstance(candidate_tokens, dict):
+ raise ValueError("Codex credentials must contain a tokens object")
+ for token_name in ("access_token", "refresh_token"):
+ if not isinstance(baseline_tokens.get(token_name), str) or not
baseline_tokens[token_name]:
+ raise ValueError(f"Baseline {token_name} must be a non-empty
string")
+ if not isinstance(candidate_tokens.get(token_name), str) or not
candidate_tokens[token_name]:
+ raise ValueError(f"Candidate {token_name} must be a non-empty
string")
+
+ immutable_baseline = copy.deepcopy(baseline)
+ immutable_candidate = copy.deepcopy(candidate)
+ for credentials in (immutable_baseline, immutable_candidate):
+ del credentials["tokens"]["access_token"]
+ del credentials["tokens"]["refresh_token"]
+ credentials.pop("last_refresh", None)
+ if immutable_candidate != immutable_baseline:
Review Comment:
[P1] Promote the complete refresh-owned credential bundle
Current Codex can replace `id_token` during refresh and always advances
`last_refresh` ([upstream
implementation](https://github.com/openai/codex/blob/e597169e9a783156e50ae9765d891a3dd74df064/codex-rs/login/src/auth/manager.rs#L1308-L1331)).
Here the new ID token remains in the “immutable” comparison, so a valid
same-account refresh aborts the workflow; when it is unchanged, the returned
timestamp is still discarded, leaving stale refresh state. Validate stable
account/workspace claims instead, then atomically promote `id_token` when
returned together with access token, refresh token, and `last_refresh`.
##########
.github/workflows/code-review-retry.yml:
##########
@@ -0,0 +1,96 @@
+name: Retry Deferred Code Reviews
+
+on:
+ schedule:
+ - cron: '*/15 * * * *'
+ workflow_dispatch:
+
+permissions:
+ actions: write
+ contents: read
+ pull-requests: read
+
+concurrency:
+ group: code-review-deferred-dispatch
+ cancel-in-progress: false
+
+jobs:
+ dispatch-deferred-reviews:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install ossutil
+ run: |
+ tmp_dir="$(mktemp -d)"
+ trap 'rm -rf "$tmp_dir"' EXIT
+ curl -fsSL -o "$tmp_dir/ossutil.zip"
https://gosspublic.alicdn.com/ossutil/1.7.19/ossutil-v1.7.19-linux-amd64.zip
+ unzip -q "$tmp_dir/ossutil.zip" -d "$tmp_dir"
+ sudo install -m 0755 "$tmp_dir/ossutil-v1.7.19-linux-amd64/ossutil"
/usr/local/bin/ossutil
+
+ - name: Dispatch reviews that have reached their retry time
+ 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_REVIEW_PENDING_PREFIX:
oss://doris-community-ci/codex/review-pending
+ REPO: ${{ github.repository }}
+ run: |
+ pending_listing="$RUNNER_TEMP/codex-review-pending.list"
+ if ! ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" ls \
+ "$OSS_CODEX_REVIEW_PENDING_PREFIX/" -s > "$pending_listing"; then
+ echo 'Failed to list deferred Codex review requests.' >&2
+ exit 1
+ fi
+
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ while IFS= read -r pending_object; do
+ pending_file="$(mktemp "$RUNNER_TEMP/codex-review-pending.XXXXXX")"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$pending_object" "$pending_file"
+ if ! jq -e '
+ (.pr_number | type == "string" and test("^[0-9]+$"))
+ and (.head_sha | type == "string" and length > 0)
+ and (.base_sha | type == "string" and length > 0)
+ and (.review_focus | type == "string")
+ and (.not_before | type == "string")
+ and (.request_id | type == "string" and test("^[0-9]+$"))
+ ' "$pending_file" >/dev/null; then
+ echo "Removing malformed deferred review request:
${pending_object##*/}"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" rm -f
"$pending_object"
+ rm -f "$pending_file"
+ continue
+ fi
+
+ not_before="$(jq -r '.not_before' "$pending_file")"
+ if [ -n "$not_before" ] && [[ "$not_before" > "$now" ]]; then
+ rm -f "$pending_file"
+ continue
+ fi
+
+ pr_number="$(jq -r '.pr_number' "$pending_file")"
+ head_sha="$(jq -r '.head_sha' "$pending_file")"
+ base_sha="$(jq -r '.base_sha' "$pending_file")"
+ review_focus="$(jq -r '.review_focus' "$pending_file")"
+ request_id="$(jq -r '.request_id' "$pending_file")"
+ pull="$(gh api "repos/${REPO}/pulls/${pr_number}")"
+ if [ "$(jq -r '.state' <<<"$pull")" != 'open' ] \
+ || [ "$(jq -r '.head.sha' <<<"$pull")" != "$head_sha" ] \
+ || [ "$(jq -r '.base.sha' <<<"$pull")" != "$base_sha" ]; then
+ echo "Removing stale deferred review request:
${pending_object##*/}"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" rm -f
"$pending_object"
+ rm -f "$pending_file"
+ continue
+ fi
+
+ gh workflow run code-review-runner.yml --ref "$GITHUB_REF_NAME" \
Review Comment:
[P1] Resolve the status after a deferred review
The initial `/review` workflow leaves the `code-review` context pending when
no account is eligible, but this scheduled dispatch bypasses that caller.
Neither this scheduler nor `code-review-runner.yml` has `statuses: write` or
posts a terminal status, so even a later successful review leaves the head
permanently pending and can keep a required context blocking merge. Give the
deferred run end-to-end status ownership: pending only while durably
queued/leased, success after verified submission, and an explicit failure/error
when a same-head request is abandoned.
##########
.github/workflows/code-review-comment.yml:
##########
@@ -51,6 +51,7 @@ jobs:
needs:
- resolve-pr
- mark-review-pending
+ - enqueue-review
Review Comment:
[P1] Recover when durable enqueue fails
`mark-review-pending` runs independently of `enqueue-review`. If the status
POST succeeds but the OSS upload fails, this job is skipped because a
dependency failed, and `refresh-required-check` also skips when `code-review`
is skipped. The head is then left permanently pending even though no runner or
deferred object can recover it. Publish pending only after durable enqueue
succeeds, or add an `always()` recovery path that reports the enqueue failure
and resolves the status.
##########
.github/workflows/code-review-runner.yml:
##########
@@ -487,53 +635,309 @@ jobs:
HEAD_SHA: ${{ inputs.head_sha }}
BASE_SHA: ${{ inputs.base_sha }}
+ - name: Grant isolated Codex user access to review context
+ run: |
+ sudo chown -R "$CODEX_REVIEW_USER:$CODEX_REVIEW_USER"
"$REVIEW_CONTEXT_DIR"
+
- name: Run automated code review
id: review
timeout-minutes: 115
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
+ OSS_CODEX_REVIEW_PENDING_PREFIX:
oss://doris-community-ci/codex/review-pending
+ 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 }}
+ BASE_SHA: ${{ inputs.base_sha }}
+ REVIEW_FOCUS: ${{ inputs.review_focus }}
+ DEFERRED_REQUEST_ID: ${{ inputs.deferred_request_id }}
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"
+ }
+
+ defer_review() {
+ local retry_at="$1" request_id
+ request_id="${DEFERRED_REQUEST_ID:-$GITHUB_RUN_ID}"
+ local request_file
+ request_file="$(mktemp "$RUNNER_TEMP/codex-review-pending.XXXXXX")"
+ jq -n \
+ --arg pr_number "$PR_NUMBER" \
+ --arg head_sha "$HEAD_SHA" \
+ --arg base_sha "$BASE_SHA" \
+ --arg review_focus "$REVIEW_FOCUS" \
+ --arg not_before "$retry_at" \
+ --arg request_id "$request_id" \
+ '{pr_number: $pr_number, head_sha: $head_sha, base_sha:
$base_sha,
+ review_focus: $review_focus, not_before: $not_before,
request_id: $request_id}' > "$request_file"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$request_file"
"$OSS_CODEX_REVIEW_PENDING_PREFIX/${PR_NUMBER}-${HEAD_SHA}-${request_id}.json"
+ rm -f "$request_file"
+ }
+
+ 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"
+ }
+
+ record_auth_result() {
+ local result="$1"
+ local http_status="$2"
+ local retry_after="$3"
+ local record_args=(
+ record
+ --state "$CODEX_AUTH_STATE_FILE"
+ --auth-object "$CODEX_AUTH_STATE_KEY"
+ "${known_auth_object_args[@]}"
+ --result "$result"
+ )
+ if [ -n "$http_status" ]; then
+ record_args+=(--http-status "$http_status")
+ fi
+ if [ -n "$retry_after" ]; then
+ record_args+=(--retry-after "$retry_after")
+ fi
+ "$CODEX_AUTH_STATE_HELPER" "${record_args[@]}"
+ }
+
+ install_selected_auth() {
+ local candidate_auth_file
+ candidate_auth_file="$(mktemp "$RUNNER_TEMP/codex-auth.XXXXXX")"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$CODEX_AUTH_OSS_OBJECT" "$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
+ sudo install -o "$CODEX_REVIEW_USER" -g "$CODEX_REVIEW_USER" -m
600 \
+ "$candidate_auth_file" "$CODEX_HOME/auth.json"
+ install -m 600 "$candidate_auth_file" "$CODEX_AUTH_SNAPSHOT"
+ rm -f "$candidate_auth_file"
+ }
+
+ sync_refreshed_auth() {
+ local refreshed_auth_file promoted_auth_file
+ refreshed_auth_file="$(mktemp
"$RUNNER_TEMP/codex-refreshed-auth.XXXXXX")"
+ promoted_auth_file="$(mktemp
"$RUNNER_TEMP/codex-promoted-auth.XXXXXX")"
+ sudo -u "$CODEX_REVIEW_USER" cat "$CODEX_HOME/auth.json" >
"$refreshed_auth_file"
+ "$CODEX_AUTH_STATE_HELPER" promote \
+ --baseline "$CODEX_AUTH_SNAPSHOT" \
+ --candidate "$refreshed_auth_file" \
+ --output "$promoted_auth_file"
+ if ! cmp -s "$refreshed_auth_file" "$CODEX_AUTH_SNAPSHOT"; then
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$promoted_auth_file" "$CODEX_AUTH_OSS_OBJECT"
+ install -m 600 "$promoted_auth_file" "$CODEX_AUTH_SNAPSHOT"
+ fi
+ rm -f "$refreshed_auth_file" "$promoted_auth_file"
+ }
+
+ review_side_effects_present() {
+ local review_sync_dir="$RUNNER_TEMP/codex-review-sync"
+ local reviews_file="$review_sync_dir/reviews.json"
+ local inline_comments_file="$review_sync_dir/inline-comments.json"
+ local issue_comments_file="$review_sync_dir/issue-comments.json"
+ install -m 700 -d "$review_sync_dir"
+
+ for attempt in 1 2 3 4 5 6; do
+ if ! gh api --paginate --slurp
"repos/${REPO}/pulls/${PR_NUMBER}/reviews" > "$reviews_file" \
+ || ! gh api --paginate --slurp
"repos/${REPO}/pulls/${PR_NUMBER}/comments" > "$inline_comments_file" \
+ || ! gh api --paginate --slurp
"repos/${REPO}/issues/${PR_NUMBER}/comments" > "$issue_comments_file"; then
+ return 2
+ fi
+ if jq -s -e --arg started_at "$review_started_at" --arg head_sha
"$HEAD_SHA" '
+ map(add // []) | add // []
+ | any(.[]?; ((.submitted_at // .created_at // "") >=
$started_at)
+ and ((.commit_id // $head_sha) == $head_sha))
+ ' "$reviews_file" "$inline_comments_file" "$issue_comments_file"
>/dev/null; then
+ return 0
+ fi
+ sleep 5
+ done
+ return 1
+ }
+
+ 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_state_key="$(jq -r '.auth_object // empty'
<<<"$selection_json")"
+ selected_auth_object="${selected_auth_state_key%%#*}"
+ 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
Review Comment:
[P2] Preserve root telemetry for every retry attempt
Every iteration redirects the root stream to the same `codex-events.jsonl`
and `codex-stderr.log`, truncating the prior attempt. Litefuse's JSONL input
therefore loses account A's root events, usage, and terminal failure when
account B is attempted; the separate subagent-session scan cannot reconstruct
that root context. Use per-attempt root files and emit or aggregate them with
attempt/account metadata while keeping the final output separate.
##########
.github/workflows/code-review-runner.yml:
##########
@@ -487,53 +635,309 @@ jobs:
HEAD_SHA: ${{ inputs.head_sha }}
BASE_SHA: ${{ inputs.base_sha }}
+ - name: Grant isolated Codex user access to review context
+ run: |
+ sudo chown -R "$CODEX_REVIEW_USER:$CODEX_REVIEW_USER"
"$REVIEW_CONTEXT_DIR"
+
- name: Run automated code review
id: review
timeout-minutes: 115
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
+ OSS_CODEX_REVIEW_PENDING_PREFIX:
oss://doris-community-ci/codex/review-pending
+ 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 }}
+ BASE_SHA: ${{ inputs.base_sha }}
+ REVIEW_FOCUS: ${{ inputs.review_focus }}
+ DEFERRED_REQUEST_ID: ${{ inputs.deferred_request_id }}
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"
+ }
+
+ defer_review() {
+ local retry_at="$1" request_id
+ request_id="${DEFERRED_REQUEST_ID:-$GITHUB_RUN_ID}"
+ local request_file
+ request_file="$(mktemp "$RUNNER_TEMP/codex-review-pending.XXXXXX")"
+ jq -n \
+ --arg pr_number "$PR_NUMBER" \
+ --arg head_sha "$HEAD_SHA" \
+ --arg base_sha "$BASE_SHA" \
+ --arg review_focus "$REVIEW_FOCUS" \
+ --arg not_before "$retry_at" \
+ --arg request_id "$request_id" \
+ '{pr_number: $pr_number, head_sha: $head_sha, base_sha:
$base_sha,
+ review_focus: $review_focus, not_before: $not_before,
request_id: $request_id}' > "$request_file"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$request_file"
"$OSS_CODEX_REVIEW_PENDING_PREFIX/${PR_NUMBER}-${HEAD_SHA}-${request_id}.json"
+ rm -f "$request_file"
+ }
+
+ 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"
+ }
+
+ record_auth_result() {
+ local result="$1"
+ local http_status="$2"
+ local retry_after="$3"
+ local record_args=(
+ record
+ --state "$CODEX_AUTH_STATE_FILE"
+ --auth-object "$CODEX_AUTH_STATE_KEY"
+ "${known_auth_object_args[@]}"
+ --result "$result"
+ )
+ if [ -n "$http_status" ]; then
+ record_args+=(--http-status "$http_status")
+ fi
+ if [ -n "$retry_after" ]; then
+ record_args+=(--retry-after "$retry_after")
+ fi
+ "$CODEX_AUTH_STATE_HELPER" "${record_args[@]}"
+ }
+
+ install_selected_auth() {
+ local candidate_auth_file
+ candidate_auth_file="$(mktemp "$RUNNER_TEMP/codex-auth.XXXXXX")"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$CODEX_AUTH_OSS_OBJECT" "$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
+ sudo install -o "$CODEX_REVIEW_USER" -g "$CODEX_REVIEW_USER" -m
600 \
+ "$candidate_auth_file" "$CODEX_HOME/auth.json"
+ install -m 600 "$candidate_auth_file" "$CODEX_AUTH_SNAPSHOT"
+ rm -f "$candidate_auth_file"
+ }
+
+ sync_refreshed_auth() {
+ local refreshed_auth_file promoted_auth_file
+ refreshed_auth_file="$(mktemp
"$RUNNER_TEMP/codex-refreshed-auth.XXXXXX")"
+ promoted_auth_file="$(mktemp
"$RUNNER_TEMP/codex-promoted-auth.XXXXXX")"
+ sudo -u "$CODEX_REVIEW_USER" cat "$CODEX_HOME/auth.json" >
"$refreshed_auth_file"
+ "$CODEX_AUTH_STATE_HELPER" promote \
+ --baseline "$CODEX_AUTH_SNAPSHOT" \
+ --candidate "$refreshed_auth_file" \
+ --output "$promoted_auth_file"
+ if ! cmp -s "$refreshed_auth_file" "$CODEX_AUTH_SNAPSHOT"; then
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$promoted_auth_file" "$CODEX_AUTH_OSS_OBJECT"
+ install -m 600 "$promoted_auth_file" "$CODEX_AUTH_SNAPSHOT"
+ fi
+ rm -f "$refreshed_auth_file" "$promoted_auth_file"
+ }
+
+ review_side_effects_present() {
+ local review_sync_dir="$RUNNER_TEMP/codex-review-sync"
+ local reviews_file="$review_sync_dir/reviews.json"
+ local inline_comments_file="$review_sync_dir/inline-comments.json"
+ local issue_comments_file="$review_sync_dir/issue-comments.json"
+ install -m 700 -d "$review_sync_dir"
+
+ for attempt in 1 2 3 4 5 6; do
+ if ! gh api --paginate --slurp
"repos/${REPO}/pulls/${PR_NUMBER}/reviews" > "$reviews_file" \
+ || ! gh api --paginate --slurp
"repos/${REPO}/pulls/${PR_NUMBER}/comments" > "$inline_comments_file" \
Review Comment:
[P1] Reconcile only artifacts owned by this attempt
This treats every review or comment after `review_started_at` as a Codex
side effect without checking its actor or an attempt-owned artifact ID. Issue
comments have no `commit_id`, and the fallback assigns them `$head_sha`, so an
unrelated maintainer comment aborts otherwise safe retries. The final verifier
at lines 946-949 repeats the ownership omission for reviews, allowing an
unrelated maintainer review to falsely prove that a zero-exit Codex attempt
submitted one. Snapshot IDs before the attempt and require artifacts
attributable to this workflow/request in both consumers.
##########
.github/workflows/code-review-runner.yml:
##########
@@ -101,26 +127,138 @@ 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"
+ defer_review() {
+ local retry_at="$1" request_id
+ request_id="${DEFERRED_REQUEST_ID:-$GITHUB_RUN_ID}"
+ local request_file
+ request_file="$(mktemp "$RUNNER_TEMP/codex-review-pending.XXXXXX")"
+ jq -n \
+ --arg pr_number "$PR_NUMBER" \
+ --arg head_sha "$HEAD_SHA" \
+ --arg base_sha "$BASE_SHA" \
+ --arg review_focus "$REVIEW_FOCUS" \
+ --arg not_before "$retry_at" \
+ --arg request_id "$request_id" \
+ '{pr_number: $pr_number, head_sha: $head_sha, base_sha:
$base_sha,
+ review_focus: $review_focus, not_before: $not_before,
request_id: $request_id}' > "$request_file"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f \
+ "$request_file"
"$OSS_CODEX_REVIEW_PENDING_PREFIX/${PR_NUMBER}-${HEAD_SHA}-${request_id}.json"
+ rm -f "$request_file"
+ }
+
+ codex_review_user=codex-review
+ sudo useradd --create-home --shell /usr/sbin/nologin
"$codex_review_user"
+ codex_home="$(mktemp -d /tmp/codex-home.XXXXXX)"
+ sudo chown "$codex_review_user:$codex_review_user" "$codex_home"
+ sudo chgrp "$(id -gn)" "$codex_home"
+ sudo chmod 770 "$codex_home"
+ control_dir="$RUNNER_TEMP/codex-auth-control"
+ install -m 700 -d "$control_dir"
+ printf 'CODEX_HOME=%s\n' "$codex_home" >> "$GITHUB_ENV"
+ printf 'CODEX_REVIEW_USER=%s\n' "$codex_review_user" >> "$GITHUB_ENV"
+
+ 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="$control_dir/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
+ candidate_hash="$(sha256sum "$candidate_auth_file" | awk '{print
$1}')"
+ printf '%s#%s\n' "$candidate" "$candidate_hash" >>
"$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" | sort
+ )
+
+ 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="$control_dir/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"
+ chmod 600 "$state_file"
+ printf 'CODEX_AUTH_OBJECTS_FILE=%s\n' "$auth_objects_file" >>
"$GITHUB_ENV"
+ printf 'CODEX_AUTH_STATE_FILE=%s\n' "$state_file" >> "$GITHUB_ENV"
+
+ auth_state_key="$(jq -r '.auth_object // empty'
<<<"$selection_json")"
+ auth_object="${auth_state_key%%#*}"
+ all_quota_exhausted="$(jq -r '.all_quota_exhausted'
<<<"$selection_json")"
+ printf 'all_quota_exhausted=%s\n' "$all_quota_exhausted" >>
"$GITHUB_OUTPUT"
+ if [ -z "$auth_object" ]; then
+ next_retry_at="$(jq -r '.next_retry_at // empty'
<<<"$selection_json")"
+ if [ -n "$next_retry_at" ]; then
+ defer_review "$next_retry_at"
+ fi
+ echo "No Codex auth account is currently eligible; next retry:
${next_retry_at:-unknown}"
+ printf 'auth_unavailable=true\n' >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
Review Comment:
[P1] Claim the deferred request before dispatch
The scheduler dispatches this request while leaving its immediately-due OSS
object pending, and this best-effort deletion occurs only after
checkout/install/setup. A scheduler tick before this line can queue the same
request again; a delete failure leaves the same replay path afterward. The
concurrency group merely serializes those runs, and the queued run does not
require the object still to exist, so a second complete review can execute.
Introduce a unique `pending -> claimed` transition with a lease/generation that
the runner validates, then acknowledge/delete only on terminal completion.
##########
.github/scripts/codex_auth_state.py:
##########
@@ -0,0 +1,453 @@
+#!/usr/bin/env python3
+"""Persist and select the rotating credentials used by code review jobs."""
+
+from __future__ import annotations
+
+import argparse
+import copy
+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)
+PERMANENT_AUTH_FAILURE_PATTERN = re.compile(
+ r"refresh[\s_-]*token.*(?:revoked|expired|already\s+used)|"
+ r"(?:revoked|expired).*refresh[\s_-]*token",
+ re.IGNORECASE,
+)
+USAGE_LIMIT_MESSAGE_PATTERN = re.compile(
+ r"you(?:'|\u2019)ve hit your usage limit|usage limit",
+ re.IGNORECASE,
+)
+USAGE_LIMIT_RETRY_PATTERN = re.compile(
+ r"try again
at\s+(?:(?P<date>[A-Za-z]{3,9}\s+\d{1,2}(?:st|nd|rd|th)?,\s+\d{4}\s+"
+ r"\d{1,2}:\d{2}\s+(?:AM|PM))|(?P<time>\d{1,2}:\d{2}\s+(?:AM|PM)))",
+ re.IGNORECASE | re.DOTALL,
+)
+
+
+@dataclass(frozen=True)
+class Selection:
+ auth_object: str | None
+ all_quota_exhausted: bool
+ next_retry_at: str | None
+
+
+@dataclass(frozen=True)
+class FailureClassification:
+ kind: str
+ http_status: int | None
+ retry_after: 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 promote_auth(baseline: dict[str, Any], candidate: dict[str, Any]) ->
dict[str, Any]:
+ if baseline.get("auth_mode") != "chatgpt" or candidate.get("auth_mode") !=
"chatgpt":
+ raise ValueError("Codex credentials must use ChatGPT authentication")
+
+ baseline_tokens = baseline.get("tokens")
+ candidate_tokens = candidate.get("tokens")
+ if not isinstance(baseline_tokens, dict) or not
isinstance(candidate_tokens, dict):
+ raise ValueError("Codex credentials must contain a tokens object")
+ for token_name in ("access_token", "refresh_token"):
+ if not isinstance(baseline_tokens.get(token_name), str) or not
baseline_tokens[token_name]:
+ raise ValueError(f"Baseline {token_name} must be a non-empty
string")
+ if not isinstance(candidate_tokens.get(token_name), str) or not
candidate_tokens[token_name]:
+ raise ValueError(f"Candidate {token_name} must be a non-empty
string")
+
+ immutable_baseline = copy.deepcopy(baseline)
+ immutable_candidate = copy.deepcopy(candidate)
+ for credentials in (immutable_baseline, immutable_candidate):
+ del credentials["tokens"]["access_token"]
+ del credentials["tokens"]["refresh_token"]
+ credentials.pop("last_refresh", None)
+ if immutable_candidate != immutable_baseline:
+ raise ValueError("Refreshed credentials changed an immutable identity
field")
+
+ promoted = copy.deepcopy(baseline)
+ promoted["tokens"]["access_token"] = candidate_tokens["access_token"]
+ promoted["tokens"]["refresh_token"] = candidate_tokens["refresh_token"]
+ return promoted
+
+
+def promote_auth_file(baseline_path: Path, candidate_path: Path, output_path:
Path) -> None:
+ baseline = json.loads(baseline_path.read_text())
+ candidate = json.loads(candidate_path.read_text())
+ if not isinstance(baseline, dict) or not isinstance(candidate, dict):
+ raise ValueError("Codex credentials must be JSON objects")
+ save_state(output_path, promote_auth(baseline, candidate))
+
+
+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")
+
+ auth_objects = sorted(set(auth_objects))
+ 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,
+ retry_after: datetime | 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:
+ http_status = http_status or 403
+ retry_at = retry_after or now + QUOTA_RETRY_DELAY
+ elif result == RATE_LIMITED:
+ http_status = http_status or 429
+ elif result == AUTHENTICATION_FAILED:
+ http_status = http_status or 401
+ retry_at = None
+ 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}")
+
+ if result == RATE_LIMITED:
+ retry_at = now + RATE_LIMIT_RETRY_DELAY
+
+ entry["status"] = result
+ entry["last_failure_at"] = timestamp
+ entry["last_http_status"] = http_status
+ entry["retry_after"] = format_timestamp(retry_at) if retry_at is not None
else None
+
+
+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})",
Review Comment:
[P1] Recognize Codex's actual refresh-error format
Current Codex reports transient refresh responses as `Failed to refresh
token: 503 Service Unavailable: ...`
([source](https://github.com/openai/codex/blob/e597169e9a783156e50ae9765d891a3dd74df064/codex-rs/login/src/auth/manager.rs#L1356-L1373)),
but this regex only extracts numbers labelled `HTTP`, `status`, or `error
code`. Passing that exact message to this helper returns `fatal` with no
status, so the workflow skips its transient retry path. The generic permanent
refresh message is likewise missed and skips account rotation. Prefer a
structured failure kind/status; otherwise cover Codex's canonical refresh
messages and add upstream-shaped fixtures.
--
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]