From 7be92348fbca9eb3dfdc108ba50cade70a8aa619 Mon Sep 17 00:00:00 2001 From: Gabriel Vinhaes Date: Fri, 13 Mar 2026 11:41:10 -0300 Subject: [PATCH 1/3] Add review and worktree helpers for openclaw --- .github/workflows/codex-pr-review.yml | 270 ++++++++++++++++++++++++++ AGENTS.md | 32 +-- tools/ci/parse_codex_review.py | 38 ++++ tools/ci/run_codex_review.sh | 209 ++++++++++++++++++++ tools/dev/wt-cleanup | 84 ++++++++ tools/dev/wt-new | 67 +++++++ 6 files changed, 685 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/codex-pr-review.yml create mode 100755 tools/ci/parse_codex_review.py create mode 100755 tools/ci/run_codex_review.sh create mode 100755 tools/dev/wt-cleanup create mode 100755 tools/dev/wt-new diff --git a/.github/workflows/codex-pr-review.yml b/.github/workflows/codex-pr-review.yml new file mode 100644 index 0000000..0fa44e0 --- /dev/null +++ b/.github/workflows/codex-pr-review.yml @@ -0,0 +1,270 @@ +name: codex-pr-review + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + review: + if: ${{ github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false }} + runs-on: + - self-hosted + - linux + timeout-minutes: 20 + defaults: + run: + working-directory: repo + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + path: repo + + - name: Ensure runner prerequisites + shell: bash + run: | + set -euo pipefail + command -v codex >/dev/null 2>&1 || { echo "::error::codex CLI was not found on this runner."; exit 1; } + command -v python3 >/dev/null 2>&1 || { echo "::error::python3 was not found on this runner."; exit 1; } + codex --version + if command -v timeout >/dev/null 2>&1; then + echo "timeout=$(command -v timeout)" + elif command -v gtimeout >/dev/null 2>&1; then + echo "timeout=$(command -v gtimeout)" + else + echo "::error::GNU timeout (timeout or gtimeout) was not found on this runner." + exit 1 + fi + if command -v stdbuf >/dev/null 2>&1; then + echo "stdbuf=$(command -v stdbuf)" + elif command -v gstdbuf >/dev/null 2>&1; then + echo "stdbuf=$(command -v gstdbuf)" + else + echo "::warning::stdbuf was not found; codex output will still run, but line buffering may be coarser." + fi + + - name: Fetch base branch + shell: bash + run: | + set -euo pipefail + git fetch --no-tags origin \ + "+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" + + - name: Authenticate Codex (fallback to OPENAI_API_KEY) + shell: bash + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + set -euo pipefail + if [ -n "${OPENAI_API_KEY:-}" ]; then + printenv OPENAI_API_KEY | codex login --with-api-key + else + codex login status + fi + + - name: Run Codex review + id: review + shell: bash + env: + CODEX_REVIEW_BASE: ${{ github.base_ref }} + CODEX_REVIEW_MODEL: gpt-5.4 + CODEX_REVIEW_TIMEOUT_SECONDS: "900" + run: | + set +e + tools/ci/run_codex_review.sh + rc=$? + set -e + echo "codex_exit_code=$rc" >> "$GITHUB_OUTPUT" + + - name: Parse Codex severity counts + id: parse + if: ${{ always() }} + shell: bash + run: | + set -euo pipefail + write_default_parse_env() { + cat > codex-parse.env <<'EOF' + p0=0 + p1=0 + p2=0 + p3=0 + parse_inconclusive=0 + parse_failed=1 + EOF + } + + if [ ! -f codex-review.txt ]; then + echo "::error::codex-review.txt was not generated." + write_default_parse_env + else + set +e + python3 tools/ci/parse_codex_review.py codex-review.txt > codex-parse.env + rc=$? + set -e + if [ "$rc" -ne 0 ]; then + echo "::error::Failed to parse codex-review.txt." + write_default_parse_env + fi + fi + + # shellcheck disable=SC1091 + source codex-parse.env + + echo "p0_count=$p0" >> "$GITHUB_OUTPUT" + echo "p1_count=$p1" >> "$GITHUB_OUTPUT" + echo "p2_count=$p2" >> "$GITHUB_OUTPUT" + echo "p3_count=$p3" >> "$GITHUB_OUTPUT" + echo "parse_inconclusive=$parse_inconclusive" >> "$GITHUB_OUTPUT" + echo "parse_failed=$parse_failed" >> "$GITHUB_OUTPUT" + + - name: Upload Codex review artifact + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: codex-pr-review-${{ github.run_id }}-${{ github.run_attempt }} + path: | + repo/codex-review.txt + repo/codex-parse.env + if-no-files-found: ignore + retention-days: 7 + + - name: Upsert PR comment with Codex output + if: ${{ always() }} + uses: actions/github-script@v7 + env: + CODEX_EXIT_CODE: ${{ steps.review.outputs.codex_exit_code }} + P0_COUNT: ${{ steps.parse.outputs.p0_count }} + P1_COUNT: ${{ steps.parse.outputs.p1_count }} + P2_COUNT: ${{ steps.parse.outputs.p2_count }} + P3_COUNT: ${{ steps.parse.outputs.p3_count }} + PARSE_INCONCLUSIVE: ${{ steps.parse.outputs.parse_inconclusive }} + PARSE_FAILED: ${{ steps.parse.outputs.parse_failed }} + with: + script: | + const fs = require("fs"); + + const marker = ""; + const outputPath = "repo/codex-review.txt"; + let output = "codex-review.txt not found."; + + if (fs.existsSync(outputPath)) { + output = fs.readFileSync(outputPath, "utf8"); + } + + const exitCode = process.env.CODEX_EXIT_CODE || "1"; + const p0 = Number(process.env.P0_COUNT || "0"); + const p1 = Number(process.env.P1_COUNT || "0"); + const p2 = Number(process.env.P2_COUNT || "0"); + const p3 = Number(process.env.P3_COUNT || "0"); + const parseInconclusive = process.env.PARSE_INCONCLUSIVE === "1"; + const parseFailed = process.env.PARSE_FAILED === "1"; + const timedOut = exitCode === "124" || exitCode === "137"; + const totalFindings = p0 + p1 + p2 + p3; + const isBlocking = exitCode !== "0" || p0 > 0 || p1 > 0 || parseInconclusive || parseFailed; + const status = isBlocking ? "❌ blocking" : "✅ non-blocking"; + const shouldComment = exitCode !== "0" || totalFindings > 0 || parseInconclusive || parseFailed; + + const findings = output + .split(/\r?\n/) + .filter((line) => /^\s*-\s*\[P[0-3]\]/.test(line)) + .map((line) => line.trim()) + .slice(0, 5); + + const findingsSection = + findings.length > 0 ? findings.map((line) => `- ${line}`).join("\n") : "- none"; + + const runUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; + + const body = [ + marker, + "### Codex PR review", + "", + `Result: **${status}**`, + `Exit code: \`${exitCode}\``, + `Timed out: \`${timedOut ? "yes" : "no"}\``, + `Blocking findings: P0 \`${p0}\` | P1 \`${p1}\``, + `Other findings: P2 \`${p2}\` | P3 \`${p3}\``, + `Parser status: \`${parseFailed ? "failed" : "ok"}\``, + `Parser fallback used: \`${parseInconclusive ? "yes" : "no"}\``, + "", + "**Top findings**", + findingsSection, + "", + `[Workflow logs](${runUrl})`, + ].join("\n"); + + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, + }); + + const existing = comments.find( + (comment) => comment.user?.type === "Bot" && comment.body?.includes(marker), + ); + + if (!shouldComment) { + if (existing) { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: existing.id, + }); + } + return; + } + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } + + - name: Fail if Codex command failed + if: ${{ steps.review.outputs.codex_exit_code != '0' }} + shell: bash + run: | + echo "::error::codex review command failed (exit code ${{ steps.review.outputs.codex_exit_code }})." + exit 1 + + - name: Fail if Codex parse failed + if: ${{ steps.parse.outputs.parse_failed == '1' }} + shell: bash + run: | + echo "::error::Codex review output parsing failed." + exit 1 + + - name: Fail on P0 or P1 findings + if: ${{ steps.parse.outputs.p0_count != '0' || steps.parse.outputs.p1_count != '0' || steps.parse.outputs.parse_inconclusive == '1' }} + shell: bash + run: | + echo "::error::Codex review reported blocking findings." + exit 1 diff --git a/AGENTS.md b/AGENTS.md index 17f61cf..4b09906 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,21 +6,12 @@ This file is the bootstrap guide for AI agents working in `orbio-openclaw-integr - OpenClaw plugin package and official skill artifacts for Orbio integration. - Sibling repos: `../orbio-api/`, `../frontend/`. -## Required Skills (order) - -1. `orbio-workspace-router` -- Confirm repository scope and cross-project impact. - -2. `implementation-spec-writer` (if long/non-trivial) -- Mandatory before significant behavior changes. - -3. `adr-writer` (if architectural decision exists) - -4. `git-worktree-flow` -- Mandatory git/worktree/PR lifecycle. - -5. `orbio-openclaw-delivery` -- Plugin/runtime policy, security constraints, and release validations. +## Required Skills +- Start with `orbio-workspace-router` when scope is unclear or may cross repositories. +- `git-worktree-flow` is mandatory for every code change. +- Use `orbio-openclaw-delivery` for implementation and validation in this repo. +- Use `implementation-spec-writer` only when the change spans multiple packages/flows or materially changes plugin behavior. +- Use `adr-writer` only for plugin contract, security posture, dependency, or release model decisions. ## Must-Read Docs - `README.md` @@ -31,6 +22,7 @@ This file is the bootstrap guide for AI agents working in `orbio-openclaw-integr ## Hard Rules (explicit approval required) - Architectural changes to plugin contract/command surface/API semantics/security posture. - Behavior-changing fallback paths. +- Workarounds, compatibility shims, and dual-path fixes in place of a correct solution. - TODO/stub/placeholder/dead code. - Skipping/disabling/ignoring lint/typecheck/coverage/build failures. - Introducing shell execution in plugin runtime (`exec`, `curl`, subprocesses). @@ -39,6 +31,12 @@ This file is the bootstrap guide for AI agents working in `orbio-openclaw-integr - Prefer secure/simple behavior over compatibility shims. - Replace incorrect behavior directly unless compatibility path is explicitly approved. +## Worktree and PR Policy +- Keep the primary checkout clean on `main`. +- Create feature branches from a fresh worktree based on `origin/main`. +- Use `tools/dev/wt-new --branch ` to start work and `tools/dev/wt-cleanup --branch ` after merge. +- Merge through PRs targeting `main` only. + ## Mandatory Quality Gates ```bash pnpm verify @@ -50,6 +48,10 @@ pnpm verify - Keep plugin runtime shell-free. - Use scoped workspace tokens for API authentication. +## CI Policy +- Required PR checks are `plugin` and `review`. +- `codex-pr-review` is always part of CI and blocks on execution failure, parse failure, or P0/P1 findings. + ## References - `../AGENTS.md` - Skill: `orbio-openclaw-delivery` diff --git a/tools/ci/parse_codex_review.py b/tools/ci/parse_codex_review.py new file mode 100755 index 0000000..19d6dfa --- /dev/null +++ b/tools/ci/parse_codex_review.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +def main() -> int: + output_path = Path(sys.argv[1] if len(sys.argv) > 1 else "codex-review.txt") + text = output_path.read_text(encoding="utf-8", errors="replace") + counts = {"P0": 0, "P1": 0, "P2": 0, "P3": 0} + + for priority in re.findall(r"^\s*-\s*\[(P[0-3])\]", text, flags=re.MULTILINE): + counts[priority] += 1 + + for priority in re.findall(r'"priority"\s*:\s*"(P[0-3])"', text): + counts[priority] += 1 + + for raw in re.findall(r'"priority"\s*:\s*([0-3])', text): + counts[f"P{raw}"] += 1 + + review_marker = re.search(r"^\s*Review comment[s]?:", text, flags=re.MULTILINE) + total = sum(counts.values()) + parse_inconclusive = 1 if review_marker and total == 0 else 0 + + print(f"p0={counts['P0']}") + print(f"p1={counts['P1']}") + print(f"p2={counts['P2']}") + print(f"p3={counts['P3']}") + print(f"parse_inconclusive={parse_inconclusive}") + print("parse_failed=0") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/ci/run_codex_review.sh b/tools/ci/run_codex_review.sh new file mode 100755 index 0000000..8079923 --- /dev/null +++ b/tools/ci/run_codex_review.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash + +set -euo pipefail + +output_path="${CODEX_REVIEW_OUTPUT_PATH:-codex-review.txt}" +review_base="${CODEX_REVIEW_BASE:-}" +review_model="${CODEX_REVIEW_MODEL:-gpt-5.4}" +review_timeout_seconds="${CODEX_REVIEW_TIMEOUT_SECONDS:-900}" +runner_temp_root="${RUNNER_TEMP:-$PWD/.tmp/codex-review}" +source_home="${HOME:-}" +source_auth_path="${source_home%/}/.codex/auth.json" +codex_bin="" +timeout_bin="" +stdbuf_bin="" + +mkdir -p "$runner_temp_root" + +require_command() { + local name="$1" + if ! command -v "$name" >/dev/null 2>&1; then + printf 'ERROR: required command not found: %s\n' "$name" > "$output_path" + cat "$output_path" + exit 2 + fi +} + +resolve_timeout_bin() { + if command -v timeout >/dev/null 2>&1; then + timeout_bin="$(command -v timeout)" + return + fi + if command -v gtimeout >/dev/null 2>&1; then + timeout_bin="$(command -v gtimeout)" + return + fi + cat > "$output_path" <<'EOF' +ERROR: timeout command not found. +Install GNU coreutils timeout (or gtimeout) on this self-hosted runner before running codex-pr-review. +EOF + cat "$output_path" + exit 2 +} + +resolve_stdbuf_bin() { + if command -v stdbuf >/dev/null 2>&1; then + stdbuf_bin="$(command -v stdbuf)" + return + fi + if command -v gstdbuf >/dev/null 2>&1; then + stdbuf_bin="$(command -v gstdbuf)" + return + fi + stdbuf_bin="" +} + +resolve_realpath() { + python3 - "$1" <<'PY' +import os +import sys + +print(os.path.realpath(sys.argv[1])) +PY +} + +require_command codex +require_command mktemp +require_command tee +require_command sh +require_command python3 +require_command git +resolve_timeout_bin +resolve_stdbuf_bin + +codex_invoked_path="$(command -v codex)" +codex_realpath="$(resolve_realpath "$codex_invoked_path")" +codex_bin="$codex_invoked_path" +if [[ "$codex_invoked_path" == */.volta/bin/codex || "$codex_realpath" == */.volta/bin/volta-shim ]]; then + volta_root="${VOLTA_HOME:-}" + if [[ -z "$volta_root" ]]; then + case "$codex_realpath" in + */.volta/bin/volta-shim) + volta_root="${codex_realpath%/bin/volta-shim}" + ;; + */.volta/bin/codex) + volta_root="${codex_realpath%/bin/codex}" + ;; + esac + fi + if [[ -n "$volta_root" ]]; then + volta_codex_bin="$volta_root/tools/image/packages/@openai/codex/bin/codex" + if [[ -x "$volta_codex_bin" ]]; then + codex_bin="$volta_codex_bin" + fi + fi +fi +if [[ ! -x "$codex_bin" ]]; then + printf 'ERROR: resolved Codex binary is not executable: %s\n' "$codex_bin" > "$output_path" + cat "$output_path" + exit 2 +fi + +codex_home_parent="$(mktemp -d "${runner_temp_root%/}/codex-home.XXXXXX")" +codex_zdotdir="$(mktemp -d "${runner_temp_root%/}/codex-zdotdir.XXXXXX")" +codex_auth_path="$codex_home_parent/.codex/auth.json" + +cleanup() { + rm -rf "$codex_home_parent" "$codex_zdotdir" +} +trap cleanup EXIT + +mkdir -p \ + "$codex_home_parent/.codex" \ + "$codex_home_parent/.config" \ + "$codex_home_parent/.cache" \ + "$codex_home_parent/.local/state" +chmod 700 \ + "$codex_home_parent" \ + "$codex_home_parent/.codex" \ + "$codex_zdotdir" +: > "$codex_home_parent/.codex/config.toml" +: > "$codex_zdotdir/.zshenv" + +export HOME="$codex_home_parent" +export XDG_CONFIG_HOME="$codex_home_parent/.config" +export XDG_CACHE_HOME="$codex_home_parent/.cache" +export XDG_STATE_HOME="$codex_home_parent/.local/state" +export ZDOTDIR="$codex_zdotdir" + +while IFS='=' read -r name _; do + case "$name" in + CODEX_*) + unset "$name" + ;; + esac +done < <(env) + +if [[ -z "$review_base" ]]; then + cat > "$output_path" <<'EOF' +ERROR: CODEX_REVIEW_BASE is required. +EOF + cat "$output_path" + exit 2 +fi + +review_base_ref="refs/remotes/origin/${review_base}" +if ! git rev-parse --verify "$review_base_ref" >/dev/null 2>&1; then + cat > "$output_path" </dev/null 2>&1; then + shallow_state="$(git rev-parse --is-shallow-repository 2>/dev/null || echo unknown)" + cat > "$output_path" < "$output_path" <&1 | tee "$output_path" +rc=${PIPESTATUS[0]} +set -e + +if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then + printf '\nERROR: codex review timed out after %ss.\n' "$review_timeout_seconds" | tee -a "$output_path" +fi + +exit "$rc" diff --git a/tools/dev/wt-cleanup b/tools/dev/wt-cleanup new file mode 100755 index 0000000..1eedd4b --- /dev/null +++ b/tools/dev/wt-cleanup @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TOOLS_DEV_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "${TOOLS_DEV_DIR}/../.." && pwd)" + +die() { + echo "error: $*" >&2 + exit 1 +} + +usage() { + cat <<'EOF' +Usage: tools/dev/wt-cleanup --branch [--worktree-path ] [--keep-branch] + +Removes a worktree and deletes the local branch by default. +EOF +} + +resolve_worktree_path() { + local branch_ref="refs/heads/$1" + git -C "${REPO_ROOT}" worktree list --porcelain | awk -v branch_ref="${branch_ref}" ' + /^worktree / { path = substr($0, 10); next } + /^branch / { + if ($2 == branch_ref) { + print path + exit 0 + } + } + ' +} + +BRANCH="" +WORKTREE_PATH="" +KEEP_BRANCH=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --branch) + [[ $# -ge 2 ]] || die "--branch requires a value" + BRANCH="$2" + shift 2 + ;; + --worktree-path) + [[ $# -ge 2 ]] || die "--worktree-path requires a value" + WORKTREE_PATH="$2" + shift 2 + ;; + --keep-branch) + KEEP_BRANCH=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown argument: $1" + ;; + esac +done + +[[ -n "${BRANCH}" ]] || die "--branch is required" + +if [[ -z "${WORKTREE_PATH}" ]]; then + WORKTREE_PATH="$(resolve_worktree_path "${BRANCH}")" +fi + +if [[ -n "${WORKTREE_PATH}" ]]; then + git -C "${REPO_ROOT}" worktree remove "${WORKTREE_PATH}" --force +fi + +if [[ "${KEEP_BRANCH}" != "1" ]] && git -C "${REPO_ROOT}" show-ref --verify --quiet "refs/heads/${BRANCH}"; then + git -C "${REPO_ROOT}" branch -D "${BRANCH}" +fi + +git -C "${REPO_ROOT}" worktree prune + +cat <&2 + exit 1 +} + +usage() { + cat <<'EOF' +Usage: tools/dev/wt-new --branch [--worktree-path ] + +Creates a fresh worktree from origin/main under ../.worktrees//. +EOF +} + +BRANCH="" +WORKTREE_PATH="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --branch) + [[ $# -ge 2 ]] || die "--branch requires a value" + BRANCH="$2" + shift 2 + ;; + --worktree-path) + [[ $# -ge 2 ]] || die "--worktree-path requires a value" + WORKTREE_PATH="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown argument: $1" + ;; + esac +done + +[[ -n "${BRANCH}" ]] || die "--branch is required" + +REPO_NAME="$(basename "${REPO_ROOT}")" +BRANCH_SLUG="${BRANCH//\//-}" + +if [[ -z "${WORKTREE_PATH}" ]]; then + WORKTREE_PATH="$(cd "${REPO_ROOT}/.." && pwd)/.worktrees/${REPO_NAME}/${BRANCH_SLUG}" +fi + +git -C "${REPO_ROOT}" fetch origin --prune +mkdir -p "$(dirname "${WORKTREE_PATH}")" +git -C "${REPO_ROOT}" worktree add -b "${BRANCH}" "${WORKTREE_PATH}" origin/main + +cat < Date: Fri, 13 Mar 2026 12:27:33 -0300 Subject: [PATCH 2/3] Run OpenClaw review on GitHub-hosted runners --- .github/workflows/codex-pr-review.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codex-pr-review.yml b/.github/workflows/codex-pr-review.yml index 0fa44e0..bcc1548 100644 --- a/.github/workflows/codex-pr-review.yml +++ b/.github/workflows/codex-pr-review.yml @@ -20,9 +20,7 @@ permissions: jobs: review: if: ${{ github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false }} - runs-on: - - self-hosted - - linux + runs-on: ubuntu-latest timeout-minutes: 20 defaults: run: @@ -34,6 +32,15 @@ jobs: fetch-depth: 0 path: repo + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Codex CLI + shell: bash + run: npm install --global @openai/codex@0.114.0 + - name: Ensure runner prerequisites shell: bash run: | From 5ee2c01dd3d8736b127fe5814a965e44265dd818 Mon Sep 17 00:00:00 2001 From: Gabriel Vinhaes Date: Fri, 13 Mar 2026 12:32:26 -0300 Subject: [PATCH 3/3] Restore Codex auth in OpenClaw review --- .github/workflows/codex-pr-review.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codex-pr-review.yml b/.github/workflows/codex-pr-review.yml index bcc1548..c4dc6c7 100644 --- a/.github/workflows/codex-pr-review.yml +++ b/.github/workflows/codex-pr-review.yml @@ -71,16 +71,23 @@ jobs: git fetch --no-tags origin \ "+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" - - name: Authenticate Codex (fallback to OPENAI_API_KEY) + - name: Restore Codex auth shell: bash env: + CODEX_AUTH_JSON_B64: ${{ secrets.CODEX_AUTH_JSON_B64 }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | set -euo pipefail - if [ -n "${OPENAI_API_KEY:-}" ]; then + mkdir -p "${HOME}/.codex" + if [ -n "${CODEX_AUTH_JSON_B64:-}" ]; then + printf '%s' "${CODEX_AUTH_JSON_B64}" | base64 --decode > "${HOME}/.codex/auth.json" + chmod 600 "${HOME}/.codex/auth.json" + codex login status + elif [ -n "${OPENAI_API_KEY:-}" ]; then printenv OPENAI_API_KEY | codex login --with-api-key else - codex login status + echo "::error::Missing CODEX_AUTH_JSON_B64 or OPENAI_API_KEY for Codex auth." + exit 1 fi - name: Run Codex review