Refresh Codex Auth #96
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |