This is an automated email from the ASF dual-hosted git repository.
shuke987 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new ac7244ff197 [fix](ci) Skip Codex auth with reused refresh tokens
(#67635)
ac7244ff197 is described below
commit ac7244ff1978b368a985a2874a48a3637b0df815
Author: shuke <[email protected]>
AuthorDate: Tue Sep 8 14:46:06 2026 +0800
[fix](ci) Skip Codex auth with reused refresh tokens (#67635)
Problem Summary: A review that fails with `refresh_token_reused` leaves
its credentials eligible because the runner only records usage limits.
Later reviews can repeatedly select the same unusable credentials.
Record explicit reuse failures in a separate OSS object keyed by the
selected refresh token's SHA-256, then check that marker before
selecting a downloaded auth snapshot. A replacement refresh token
restores eligibility; metadata-only changes do not. Separate objects
keep late failures and usage-context writes from overwriting another
token's quarantine state. Marker read/write failures are surfaced
explicitly.
The marker applies to the selected token version. Runs that already
selected credentials before the marker was written can still fail. This
does not add credential leasing or change auth writeback.
---
.github/scripts/test_review_auth_quarantine.py | 394 +++++++++++++++++++++++++
.github/workflows/code-review-runner.yml | 99 ++++++-
2 files changed, 481 insertions(+), 12 deletions(-)
diff --git a/.github/scripts/test_review_auth_quarantine.py
b/.github/scripts/test_review_auth_quarantine.py
new file mode 100644
index 00000000000..035b78b75cc
--- /dev/null
+++ b/.github/scripts/test_review_auth_quarantine.py
@@ -0,0 +1,394 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Run the actual workflow shell steps against fake OSS, Codex and GitHub.
+
+Requires bash, jq and GNU coreutils (gdate is accepted on macOS).
+Run: python3 -m unittest discover -s .github/scripts -p
test_review_auth_quarantine.py
+No credentials or network access are used.
+"""
+
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import textwrap
+import time
+import unittest
+
+
+WORKFLOW = Path(__file__).resolve().parents[1] /
"workflows/code-review-runner.yml"
+PREFIX = "oss://doris-community-ci/codex/"
+REUSED_MESSAGE = (
+ "Your access token could not be refreshed because your refresh token was
already used. "
+ "Please log out and sign in again."
+)
+
+
+def workflow_step(name):
+ match = re.search(
+ rf"^ - name: {re.escape(name)}\n(.*?)(?=^ - name:|\Z)",
+ WORKFLOW.read_text(), re.MULTILINE | re.DOTALL,
+ )
+ if match is None:
+ raise AssertionError(f"Missing workflow step: {name}")
+ return match.group(1)
+
+
+def step_script(name):
+ match = re.search(r"^ run: \|\n((?: .*\n|\n)+)",
+ workflow_step(name), re.MULTILINE)
+ if match is None:
+ raise AssertionError(f"Missing shell script: {name}")
+ return textwrap.dedent(match.group(1))
+
+
+FAKE_OSS = r'''
+import json
+import os
+from pathlib import Path
+import shutil
+import sys
+
+root = Path(os.environ["FAKE_OSS_ROOT"])
+args = sys.argv[1:]
+op = next(arg for arg in args if arg in ("ls", "cp"))
+args = [arg for arg in args[args.index(op) + 1:] if not arg.startswith("-")]
+with (root / "calls.jsonl").open("a") as log:
+ log.write(json.dumps([op, *args]) + "\n")
+prefix = "oss://doris-community-ci/codex/"
+if os.environ.get("FAKE_OSS_FAIL") == f"{op}:{args[-1] if op == 'cp' else
args[0]}":
+ sys.exit(1)
+if op == "ls":
+ for path in sorted((root / "objects").iterdir()):
+ name = prefix + path.name
+ if name.startswith(args[0]):
+ print(name)
+ sys.exit(0)
+source, destination = args
+if source.startswith(prefix):
+ source = root / "objects" / source.removeprefix(prefix)
+if destination.startswith(prefix):
+ destination = root / "objects" / destination.removeprefix(prefix)
+if not Path(source).exists():
+ sys.exit(1)
+shutil.copyfile(source, destination)
+# Simulate a concurrent replacement immediately after a candidate is
downloaded.
+if os.environ.get("FAKE_OSS_REPLACE_AFTER_DOWNLOAD") == Path(source).name:
+ source.write_text(os.environ["FAKE_OSS_REPLACEMENT"])
+'''
+
+FAKE_CODEX = r'''
+import os
+from pathlib import Path
+import sys
+
+print(os.environ.get("FAKE_CODEX_EVENTS", ""))
+print(os.environ.get("FAKE_CODEX_STDERR", ""), file=sys.stderr, flush=True)
+# Rotation during a run must not change the identity used for the failure
marker.
+if "FAKE_CODEX_ROTATED_AUTH" in os.environ:
+ Path(os.environ["CODEX_HOME"],
"auth.json").write_text(os.environ["FAKE_CODEX_ROTATED_AUTH"])
+sys.exit(int(os.environ.get("FAKE_CODEX_STATUS", "1")))
+'''
+
+FAKE_GH = r'''
+import json
+import os
+from pathlib import Path
+import sys
+
+if sys.argv[1] == "api":
+ print(json.dumps([[{"submitted_at": "2099-01-01T00:00:00Z", "commit_id":
os.environ["HEAD_SHA"]}]]))
+else:
+ Path(os.environ["FAKE_COMMENT_FILE"]).write_text(sys.argv[-1])
+'''
+
+
+class ReviewAuthQuarantineTest(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.tmp.cleanup)
+ self.root = Path(self.tmp.name)
+ self.objects = self.root / "objects"
+ self.objects.mkdir()
+ self.bin = self.root / "bin"
+ self.bin.mkdir()
+ for name, source in (("ossutil", FAKE_OSS), ("codex", FAKE_CODEX),
("gh", FAKE_GH)):
+ path = self.bin / name
+ path.write_text(f"#!{sys.executable}\n" + source)
+ path.chmod(0o700)
+ # Deterministic candidate order, so the tests must encounter invalid
.2 first.
+ shuf = self.bin / "shuf"
+ shuf.write_text("#!/bin/sh\nexec cat \"$@\"\n")
+ shuf.chmod(0o700)
+ if shutil.which("gdate"):
+ (self.bin / "date").symlink_to(shutil.which("gdate"))
+ self.env = dict(os.environ)
+ self.env.update({
+ "PATH": f"{self.bin}{os.pathsep}{os.environ['PATH']}",
+ "FAKE_OSS_ROOT": str(self.root),
+ "FAKE_COMMENT_FILE": str(self.root / "comment.txt"),
+ "OSS_AK": "fake", "OSS_SK": "fake", "OSS_ENDPOINT": "unused",
+ "REPO": "example/repo", "PR_NUMBER": "1", "HEAD_SHA": "a" * 40,
+ "GITHUB_WORKSPACE": str(self.root),
+ })
+ self.new_runner()
+
+ def new_runner(self):
+ runner = Path(tempfile.mkdtemp(dir=self.root))
+ context = runner / "review-context"
+ context.mkdir()
+ (context / "codex_goal_prompt.txt").write_text("Fake review")
+ self.env.update({
+ "RUNNER_TEMP": str(runner), "CODEX_HOME": str(runner /
"codex-home"),
+ "GITHUB_ENV": str(runner / "env"), "GITHUB_OUTPUT": str(runner /
"output"),
+ "REVIEW_CONTEXT_DIR": str(context),
+ })
+ self.env.pop("CODEX_AUTH_OSS_OBJECT", None)
+
+ def run_step(self, name, expected=0, **env):
+ output = Path(self.env["GITHUB_OUTPUT"])
+ output.write_text("")
+ result = subprocess.run(
+ ["bash", "--noprofile", "--norc", "-eo", "pipefail", "-c",
step_script(name)],
+ env={**self.env, **env}, cwd=self.root, capture_output=True,
text=True, timeout=15,
+ )
+ self.assertEqual(result.returncode, expected, result.stdout +
result.stderr)
+ env_file = Path(self.env["GITHUB_ENV"])
+ if env_file.exists():
+ for line in env_file.read_text().splitlines():
+ key, value = line.split("=", 1)
+ self.env[key] = value
+ return result.stdout + result.stderr, output.read_text()
+
+ def put_auth(self, slot=2, refresh="refresh-old", **fields):
+ auth = {"auth_mode": "chatgpt", "tokens": {"access_token":
"access-fake", "refresh_token": refresh}}
+ auth.update(fields)
+ self.object(f"auth.json.{slot}").write_text(json.dumps(auth))
+ return auth
+
+ def object(self, name):
+ return self.objects / name
+
+ def marker(self, slot=2, refresh="refresh-old"):
+ digest = hashlib.sha256(refresh.encode()).hexdigest()
+ return self.object(f"auth.json.{slot}.invalid.{digest}")
+
+ def configure(self, expected=0, **env):
+ return self.run_step("Configure Codex auth", expected=expected, **env)
+
+ def fail_review(self, events=None, stderr="", expected_invalid=True,
**env):
+ if events is None:
+ events = [{"type": "turn.failed", "error": {"message":
REUSED_MESSAGE}}]
+ logs, outputs = self.run_step(
+ "Run automated code review", expected=1,
+ FAKE_CODEX_EVENTS="\n".join(json.dumps(event) for event in events),
+ FAKE_CODEX_STDERR=stderr, **env,
+ )
+ self.assertEqual("auth_invalid_reason=refresh_token_reused\n" in
outputs, expected_invalid, outputs)
+ return logs, outputs
+
+ def record(self, expected=0, **env):
+ return self.run_step("Record invalid Codex auth", expected=expected,
**env)
+
+ def test_reused_token_is_persisted_and_skipped_on_next_run(self):
+ self.put_auth()
+ self.put_auth(3, "refresh-other")
+ self.configure()
+ self.fail_review()
+ self.record()
+ marker = json.loads(self.marker().read_text())
+ self.assertEqual(marker["reason"], "refresh_token_reused")
+ self.assertNotIn("refresh-old", self.marker().read_text())
+ self.new_runner()
+ logs, _ = self.configure()
+ self.assertIn("Skipping Codex auth auth.json.2", logs)
+ self.assertEqual(self.env["CODEX_AUTH_OSS_OBJECT"], PREFIX +
"auth.json.3")
+ local_auth = Path(self.env["CODEX_HOME"], "auth.json")
+ self.assertEqual(local_auth.read_bytes(),
self.object("auth.json.3").read_bytes())
+ original = Path(self.env["RUNNER_TEMP"],
"codex-auth-original.sha256").read_text().strip()
+ self.assertEqual(original,
hashlib.sha256(local_auth.read_bytes()).hexdigest())
+ self.assertEqual(local_auth.stat().st_mode & 0o777, 0o600)
+ logs, _ = self.run_step("Sync refreshed Codex auth back to OSS")
+ self.assertIn("not refreshed; skipping", logs)
+
+ def test_replacement_refresh_token_restores_eligibility(self):
+ self.put_auth()
+ self.configure()
+ self.record()
+ self.put_auth(refresh="refresh-new")
+ self.new_runner()
+ self.configure()
+ self.assertEqual(self.env["CODEX_AUTH_OSS_OBJECT"], PREFIX +
"auth.json.2")
+ self.assertTrue(self.marker().exists())
+ self.assertFalse(self.marker(refresh="refresh-new").exists())
+
+ def test_metadata_and_formatting_do_not_restore_reused_token(self):
+ self.put_auth()
+ self.configure()
+ self.record()
+ auth = self.put_auth(last_refresh="later")
+ auth["tokens"]["access_token"] = "access-new"
+ self.object("auth.json.2").write_text(json.dumps(auth, indent=4))
+ self.new_runner()
+ logs, outputs = self.configure(expected=1)
+ self.assertIn("No eligible", outputs)
+ self.assertNotIn("invalid date", logs)
+ self.assertNotIn("Earliest usage-limit retry", outputs)
+
+ def test_late_failure_does_not_quarantine_replacement(self):
+ self.put_auth()
+ self.configure()
+ replacement = self.put_auth(refresh="refresh-new")
+ self.fail_review(FAKE_CODEX_ROTATED_AUTH=json.dumps(replacement))
+ self.record()
+ self.assertTrue(self.marker().exists())
+ self.assertFalse(self.marker(refresh="refresh-new").exists())
+ self.new_runner()
+ self.configure()
+
+ def test_delayed_old_marker_does_not_overwrite_new_token_marker(self):
+ self.put_auth()
+ self.configure()
+ old_env = dict(self.env)
+ self.put_auth(refresh="refresh-new")
+ self.new_runner()
+ self.configure()
+ self.record()
+ self.env = old_env
+ self.record()
+ self.record() # The marker write is idempotent.
+ self.assertTrue(self.marker().exists())
+ self.assertTrue(self.marker(refresh="refresh-new").exists())
+ self.new_runner()
+ self.configure(expected=1)
+
+ def test_usage_context_cannot_overwrite_quarantine(self):
+ self.put_auth()
+ self.configure()
+ self.record()
+ self.run_step("Record Codex usage limit", RETRY_AFTER_EPOCH="1")
+ self.new_runner()
+ logs, _ = self.configure(expected=1)
+ self.assertIn("Skipping Codex auth auth.json.2", logs)
+
+ def test_mixed_cooldown_and_invalid_pool_reports_both(self):
+ self.put_auth()
+ self.marker().write_text("{}")
+ self.put_auth(3, "refresh-other")
+ self.object("auth.json.3.context").write_text(json.dumps({
+ "version": 1, "state": "usage_limited", "retry_after_epoch":
int(time.time()) + 3600,
+ }))
+ _, outputs = self.configure(expected=1)
+ self.assertIn("Earliest usage-limit retry", outputs)
+ self.assertIn("reused refresh tokens must be replaced", outputs)
+
+ def test_marker_lookup_failure_does_not_select_token(self):
+ self.put_auth()
+ _, outputs = self.configure(expected=1, FAKE_OSS_FAIL="ls:" + PREFIX +
self.marker().name)
+ self.assertIn("Failed to check invalid-token marker", outputs)
+ self.assertNotIn("CODEX_AUTH_OSS_OBJECT", self.env)
+
+ def test_failed_marker_upload_is_visible(self):
+ self.put_auth()
+ self.configure()
+ logs, _ = self.record(expected=1, FAKE_OSS_FAIL="cp:" + PREFIX +
self.marker().name)
+ self.assertIn("may still be selected", logs)
+ self.assertFalse(self.marker().exists())
+
+ def test_exact_marker_lookup_does_not_confuse_prefix_matches(self):
+ self.put_auth()
+ self.object(self.marker().name + ".unrelated").write_text("{}")
+ self.configure()
+
+ def test_selected_snapshot_is_not_downloaded_again(self):
+ original = self.put_auth()
+ replacement = json.loads(json.dumps(original))
+ replacement["tokens"]["refresh_token"] = "refresh-new"
+ self.configure(FAKE_OSS_REPLACE_AFTER_DOWNLOAD="auth.json.2",
+ FAKE_OSS_REPLACEMENT=json.dumps(replacement))
+ local = json.loads(Path(self.env["CODEX_HOME"],
"auth.json").read_text())
+ self.assertEqual(local, original)
+ self.record()
+ self.new_runner()
+ self.configure()
+
+ def test_structured_reused_codes_and_terminal_message(self):
+ for event in (
+ {"type": "turn.failed", "error": {"code": "refresh_token_reused",
"message": "Refresh failed"}},
+ {"type": "error", "code": "refresh_token_reused", "message":
"Refresh failed"},
+ {"type": "error", "message": "refresh_token_reused"},
+ {"type": "error", "message": REUSED_MESSAGE},
+ ):
+ with self.subTest(event=event):
+ self.fail_review(events=[event])
+
+ def test_codex_stderr_reused_code(self):
+ self.fail_review(events=[], stderr='Token refresh failed: 401
Unauthorized\n{\n "error": {\n "code": "refresh_token_reused"\n }\n}')
+
+ def test_unrelated_failures_are_not_quarantined(self):
+ for message in ("401 Unauthorized", "We're experiencing high demand",
"Request timed out", "You've hit your usage limit"):
+ with self.subTest(message=message):
+ _, outputs = self.fail_review(
+ events=[{"type": "turn.failed", "error": {"message":
message}}], expected_invalid=False,
+ )
+ if "usage limit" in message:
+ self.assertIn("usage_limit_retry_after_epoch=", outputs)
+
+ def test_tool_output_quoting_reused_error_is_not_quarantined(self):
+ self.fail_review(events=[
+ {"type": "item.completed", "item": {"type": "command_execution",
"aggregated_output": REUSED_MESSAGE}},
+ {"type": "turn.failed", "error": {"message": "Request timed out"}},
+ ], expected_invalid=False)
+
+ def test_success_is_not_quarantined_even_with_earlier_stderr_error(self):
+ _, outputs = self.run_step(
+ "Run automated code review", FAKE_CODEX_STATUS="0",
+ FAKE_CODEX_STDERR='{"code":"refresh_token_reused"}',
+ )
+ self.assertNotIn("auth_invalid_reason", outputs)
+
+ def test_failure_comment_reports_marker_write_outcome(self):
+ for outcome in ("success", "failure"):
+ with self.subTest(outcome=outcome):
+ self.run_step(
+ "Comment PR on review failure", AUTH_FAILURE_REASON="",
+ AUTH_INVALID_REASON="refresh_token_reused",
AUTH_INVALID_RECORD_OUTCOME=outcome,
+ REVIEW_FAILURE_REASON=REUSED_MESSAGE,
RUN_URL="https://example.test/run",
+ )
+ body = Path(self.env["FAKE_COMMENT_FILE"]).read_text()
+ if outcome == "success":
+ self.assertIn("excluded from future reviews", body)
+ else:
+ self.assertIn("may still be selected", body)
+ self.assertNotIn("excluded from future reviews", body)
+
+ def
test_marker_step_runs_after_failed_review_with_explicit_detection(self):
+ step = workflow_step("Record invalid Codex auth")
+ self.assertIn("always() && steps.auth.outcome == 'success'", step)
+ self.assertIn("steps.review.outputs.auth_invalid_reason ==
'refresh_token_reused'", step)
+ self.assertNotIn("continue-on-error: true", step)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/workflows/code-review-runner.yml
b/.github/workflows/code-review-runner.yml
index 196025cd0e0..5d200533979 100644
--- a/.github/workflows/code-review-runner.yml
+++ b/.github/workflows/code-review-runner.yml
@@ -60,9 +60,9 @@ jobs:
head_sha: ${{ steps.review_inputs.outputs.head_sha }}
manage_status: ${{ steps.review_inputs.outputs.manage_status }}
pr_number: ${{ steps.review_inputs.outputs.pr_number }}
- # Pre-finalization steps can use 183 minutes and auth sync can use 8 more,
+ # Pre-finalization steps can use 188 minutes and auth sync can use 8 more,
# leaving 12 minutes for runner setup and post-job cleanup.
- timeout-minutes: 203
+ timeout-minutes: 208
if: >-
inputs.pr_number != '' ||
(
@@ -284,13 +284,46 @@ jobs:
fi
fi
+ candidate_auth="$(mktemp
"$RUNNER_TEMP/codex-home/candidate-auth.XXXXXX")"
+ ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f
"$candidate" "$candidate_auth"
+ 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" >/dev/null
+ refresh_token_hash="$(jq -rj '.tokens.refresh_token'
"$candidate_auth" | sha256sum | awk '{print $1}')"
+
+ # Quarantine the refresh token, not the slot or the entire auth
file:
+ # replacing the token restores eligibility; changing metadata does
not.
+ invalid_object="${candidate}.invalid.${refresh_token_hash}"
+ invalid_listing="$(mktemp
"$RUNNER_TEMP/codex-auth-invalid.XXXXXX")"
+ if ! ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" \
+ ls "$invalid_object" -s > "$invalid_listing"; then
+ auth_failure_reason="Failed to check invalid-token marker for
${candidate##*/}."
+ echo "failure_reason=$auth_failure_reason" >> "$GITHUB_OUTPUT"
+ echo "::error::$auth_failure_reason"
+ exit 1
+ fi
+ if grep -Fxq "$invalid_object" "$invalid_listing"; then
+ echo "Skipping Codex auth ${candidate##*/}: refresh token was
already used."
+ rm -f "$candidate_auth"
+ continue
+ fi
+
auth_object="$candidate"
+ # Use the checked snapshot; downloading it again could select a
different token.
+ mv "$candidate_auth" "$RUNNER_TEMP/codex-home/auth.json"
+ printf '%s\n' "$refresh_token_hash" >
"$RUNNER_TEMP/codex-auth-refresh-token.sha256"
break
done < <(shuf "$auth_candidates")
if [ -z "$auth_object" ]; then
- retry_at="$(date -u -d "@$earliest_retry_after_epoch"
+%Y-%m-%dT%H:%M:%SZ)"
- auth_failure_reason="All Codex review accounts are usage-limited;
earliest retry is $retry_at."
+ auth_failure_reason="No eligible Codex review credentials: all are
usage-limited or have a reused refresh token."
+ if [ -n "$earliest_retry_after_epoch" ]; then
+ retry_at="$(date -u -d "@$earliest_retry_after_epoch"
+%Y-%m-%dT%H:%M:%SZ)"
+ auth_failure_reason="$auth_failure_reason Earliest usage-limit
retry is $retry_at."
+ fi
+ auth_failure_reason="$auth_failure_reason Credentials with reused
refresh tokens must be replaced."
echo "failure_reason=$auth_failure_reason" >> "$GITHUB_OUTPUT"
echo "::error::$auth_failure_reason"
exit 1
@@ -298,14 +331,7 @@ jobs:
printf 'CODEX_AUTH_OSS_OBJECT=%s\n' "$auth_object" >> "$GITHUB_ENV"
echo "Selected Codex auth object: ${auth_object##*/}"
- ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" cp -f
"$auth_object" "$RUNNER_TEMP/codex-home/auth.json"
chmod 600 "$RUNNER_TEMP/codex-home/auth.json"
- test -s "$RUNNER_TEMP/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)
- ' "$RUNNER_TEMP/codex-home/auth.json" >/dev/null
sha256sum "$RUNNER_TEMP/codex-home/auth.json" \
| awk '{print $1}' \
> "$RUNNER_TEMP/codex-auth-original.sha256"
@@ -710,6 +736,8 @@ jobs:
> "$REVIEW_CONTEXT_DIR/codex-events.jsonl" \
2> >(tee "$REVIEW_CONTEXT_DIR/codex-stderr.log" >&2)
status=$?
+ # Drain the stderr tee before inspecting it for refresh-token
failures.
+ wait
set -e
failure_reason=""
@@ -727,6 +755,17 @@ jobs:
failure_reason="Codex exited with status $status"
fi
+ # Only terminal auth errors and Codex's own stderr identify an
invalid
+ # token. A tool result quoting this error must not quarantine
credentials.
+ if grep -Fqi "Your access token could not be refreshed because
your refresh token was already used." <<<"$failure_reason" || \
+ grep -Fxq "refresh_token_reused" <<<"$failure_reason" || \
+ { jq -r 'select(.type == "turn.failed" or .type == "error") |
.error.code // .code // empty' \
+ "$REVIEW_CONTEXT_DIR/codex-events.jsonl" | grep -Fx
"refresh_token_reused" >/dev/null; } || \
+ grep -Eq
'"code"[[:space:]]*:[[:space:]]*"refresh_token_reused"'
"$REVIEW_CONTEXT_DIR/codex-stderr.log"; then
+ echo "auth_invalid_reason=refresh_token_reused" >>
"$GITHUB_OUTPUT"
+ echo "::warning::Codex refresh token was already used; recording
this credential version as invalid."
+ fi
+
usage_limit_retry_after_epoch=""
if grep -Fqi "You've hit your usage limit" <<<"$failure_reason";
then
now_epoch="$(date +%s)"
@@ -784,6 +823,34 @@ jobs:
exit 1
fi
+ - name: Record invalid Codex auth
+ id: auth_invalid_record
+ if: ${{ always() && steps.auth.outcome == 'success' &&
steps.review.outputs.auth_invalid_reason == 'refresh_token_reused' }}
+ timeout-minutes: 5
+ env:
+ OSS_AK: ${{ secrets.OSS_AK }}
+ OSS_SK: ${{ secrets.OSS_SK }}
+ OSS_ENDPOINT: oss-cn-hongkong.aliyuncs.com
+ run: |
+ # Bind the marker to the selected snapshot, so a late failing job
cannot
+ # invalidate a replacement uploaded by another job or an operator.
+
refresh_token_hash="$(<"$RUNNER_TEMP/codex-auth-refresh-token.sha256")"
+ [[ "$refresh_token_hash" =~ ^[0-9a-f]{64}$ ]]
+ invalid_file="$(mktemp "$RUNNER_TEMP/codex-auth-invalid.XXXXXX")"
+ jq -n --arg refresh_token_sha256 "$refresh_token_hash" \
+ '{version: 1, state: "auth_invalid", reason:
"refresh_token_reused", refresh_token_sha256: $refresh_token_sha256}' \
+ > "$invalid_file"
+
+ # A separate object per token is idempotent and cannot be
overwritten by
+ # concurrent usage-limit context writes or failures of older tokens.
+ if ! ossutil -i "$OSS_AK" -k "$OSS_SK" -e "$OSS_ENDPOINT" \
+ --retry-times=3 --connect-timeout=10 --read-timeout=30 \
+ cp -f "$invalid_file"
"${CODEX_AUTH_OSS_OBJECT}.invalid.${refresh_token_hash}"; then
+ echo "::error::Could not record the invalid Codex refresh token;
it may still be selected."
+ exit 1
+ fi
+ echo "Recorded invalid refresh token for
${CODEX_AUTH_OSS_OBJECT##*/}; this credential version will be skipped."
+
- name: Record Codex usage limit
id: usage_limit_record
if: ${{ always() && steps.review.outputs.usage_limit_retry_after_epoch
!= '' }}
@@ -811,6 +878,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AUTH_FAILURE_REASON: ${{ steps.auth.outputs.failure_reason }}
+ AUTH_INVALID_REASON: ${{ steps.review.outputs.auth_invalid_reason }}
+ AUTH_INVALID_RECORD_OUTCOME: ${{ steps.auth_invalid_record.outcome }}
USAGE_LIMIT_RECORD_OUTCOME: ${{ steps.usage_limit_record.outcome }}
USAGE_LIMIT_RETRY_AFTER_EPOCH: ${{
steps.review.outputs.usage_limit_retry_after_epoch }}
REVIEW_CONTEXT_OUTCOME: ${{ steps.review_context.outcome }}
@@ -822,7 +891,13 @@ jobs:
recovery_msg="Please inspect the workflow logs and rerun the review
after the underlying issue is resolved."
if [ -n "$AUTH_FAILURE_REASON" ]; then
error_msg="$AUTH_FAILURE_REASON"
- recovery_msg="Please trigger /review again after that time."
+ elif [ "$AUTH_INVALID_REASON" = "refresh_token_reused" ]; then
+ error_msg="${REVIEW_FAILURE_REASON:-The selected Codex refresh
token was already used.}"
+ if [ "$AUTH_INVALID_RECORD_OUTCOME" = "success" ]; then
+ recovery_msg="This credential version is excluded from future
reviews. Replace its refresh token to restore eligibility. Please trigger
/review again; another configured account may be available."
+ else
+ recovery_msg="The workflow failed to record the invalid
credential, so it may still be selected. Please inspect the recording step
before retrying."
+ fi
elif [ -n "$USAGE_LIMIT_RETRY_AFTER_EPOCH" ] && \
[ "$USAGE_LIMIT_RECORD_OUTCOME" = "success" ]; then
retry_at="$(date -u -d "@$USAGE_LIMIT_RETRY_AFTER_EPOCH"
+%Y-%m-%dT%H:%M:%SZ)"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]