Skip to content

Refresh Codex Auth

Refresh Codex Auth #96

name: Refresh Codex Auth
# Refreshes the CODEX_AUTH_JSON secret (repository copy and ok-to-test
# environment copy) that e2e uses: Codex rotates file-backed auth.json and the
# result is written back with a kelos-secret-manager App installation token.
on:
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
# Refreshing rotates the shared OAuth refresh_token, so concurrent runs would
# invalidate each other. Never run more than one at a time.
concurrency:
group: refresh-codex-auth
cancel-in-progress: false
jobs:
refresh:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Mint secret-manager token
id: app-token
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547
with:
app-id: ${{ secrets.KELOS_SECRET_MANAGER_APP_ID }}
private-key: ${{ secrets.KELOS_SECRET_MANAGER_PRIVATE_KEY }}
- name: Verify secret write access
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
probe_secret="CODEX_AUTH_REFRESH_WRITE_PROBE"
probe_value="write-probe-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
# Prove both secret targets are writable before Codex rotates the
# refresh_token; read/list access is not enough for this workflow.
gh secret set "${probe_secret}" \
--repo "${GITHUB_REPOSITORY}" \
--body "${probe_value}"
gh secret delete "${probe_secret}" \
--repo "${GITHUB_REPOSITORY}"
gh secret set "${probe_secret}" \
--repo "${GITHUB_REPOSITORY}" \
--env ok-to-test \
--body "${probe_value}"
gh secret delete "${probe_secret}" \
--repo "${GITHUB_REPOSITORY}" \
--env ok-to-test
- name: Refresh Codex auth.json
env:
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
run: |
if [ -z "${CODEX_AUTH_JSON}" ]; then
echo "::error::CODEX_AUTH_JSON secret is required"
exit 1
fi
refresh_token=$(jq -r '.tokens.refresh_token // ""' <<<"${CODEX_AUTH_JSON}")
if [ -z "${refresh_token}" ] || [ "${refresh_token}" = "null" ]; then
echo "::error::CODEX_AUTH_JSON is missing tokens.refresh_token"
exit 1
fi
client_id=$(jq -r '.client_id // .tokens.client_id // ""' <<<"${CODEX_AUTH_JSON}")
if [ -z "${client_id}" ] || [ "${client_id}" = "null" ]; then
# OpenAI's public Codex OAuth client id. This is a public identifier
# used by Codex CLI for refresh_token exchange when no client_id is
# embedded in the auth JSON bundle.
client_id="app_EMoamEEZ73f0CkXaXp7hrann"
fi
mkdir -p ~/.codex
chmod 700 ~/.codex
response_file="$(mktemp)"
if ! http_status=$(curl -sS \
--request POST \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=${refresh_token}" \
--data-urlencode "client_id=${client_id}" \
--output "${response_file}" \
--write-out "%{http_code}" \
"https://auth.openai.com/oauth/token"); then
rm -f "${response_file}"
echo "::error::OAuth token endpoint request failed"
exit 1
fi
response="$(cat "${response_file}")"
rm -f "${response_file}"
if [ "${http_status}" != "200" ]; then
echo "::error::OAuth token endpoint returned HTTP ${http_status}"
echo "${response}"
exit 1
fi
if [ -z "${response}" ]; then
echo "::error::OAuth token endpoint returned an empty response"
exit 1
fi
if ! jq -e 'type == "object"' >/dev/null <<<"${response}" 2>&1; then
echo "::error::OAuth token response is invalid JSON"
echo "${response}"
exit 1
fi
access_token=$(jq -r 'if (.access_token | type) == "string" then .access_token else "" end' <<<"${response}")
if [ -z "${access_token}" ] || [ "${access_token}" = "null" ]; then
echo "::error::OAuth token response is missing access_token"
exit 1
fi
jq --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--argjson refreshed "${response}" \
'.last_refresh = $now
| .tokens = (.tokens // {})
| if ($refreshed.expires_at == null and $refreshed.expires_in != null)
then del(.tokens.expires_at)
else .
end
| .tokens |= . + (
$refreshed
| to_entries
| map(select(
.value != null and
((.value | type) != "string" or .value != "") and (
.key == "access_token" or
.key == "id_token" or
.key == "refresh_token" or
.key == "token_type" or
.key == "scope" or
.key == "expires_at" or
.key == "expires_in"
)
))
| from_entries
)' \
<<<"${CODEX_AUTH_JSON}" > ~/.codex/auth.json
chmod 600 ~/.codex/auth.json
- name: Update CODEX_AUTH_JSON secrets
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
# Both secrets back the same Codex account; write the one refreshed
# bundle to both so neither copy goes stale.
gh secret set CODEX_AUTH_JSON \
--repo "${GITHUB_REPOSITORY}" \
<~/.codex/auth.json
gh secret set CODEX_AUTH_JSON \
--repo "${GITHUB_REPOSITORY}" \
--env ok-to-test \
<~/.codex/auth.json