Run OpenAI Codex CLI non-interactively in GitHub Actions workflows via codex-docker.
- name: Run Codex
id: codex
uses: icoretech/codex-action@v0
with:
prompt: "Summarize these changes for operators"
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
- name: Use result
run: echo "${{ steps.codex.outputs.result }}"You must provide exactly one of openai_api_key or codex_config. Providing both or neither will cause the action to fail immediately.
-
Get an API key from platform.openai.com/api-keys.
-
Add it as a repository secret named
OPENAI_API_KEY: Settings → Secrets and variables → Actions → New repository secret -
Reference it in your workflow:
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
Authenticate via device auth and store the resulting auth.json as a secret. This is useful for ChatGPT Pro/Plus subscribers who use Codex through their OpenAI account rather than an API key.
How it works: The device-auth flow produces an auth.json file containing OAuth tokens (access token, refresh token, account ID). Codex uses the access token to authenticate with OpenAI's API. When the access token expires, Codex automatically refreshes it using the refresh token — no keychain or browser required.
-
Pull the codex-docker image:
docker pull ghcr.io/icoretech/codex-docker:0.114.0
-
Run the device auth flow (the
codex-bootstraphelper forces file-based credential storage, which is required for CI):mkdir -p .codex docker run --rm -it \ -v "$PWD/.codex:/home/codex/.codex" \ ghcr.io/icoretech/codex-docker:0.114.0 \ codex-bootstrap device-auth -
Follow the browser prompt to complete authentication.
-
Verify the credentials were written:
# You should see auth.json with OAuth tokens cat .codex/auth.json | python3 -c "import json,sys; d=json.load(sys.stdin); print('auth_mode:', d['auth_mode'])"
-
Encode the credentials file:
# Linux base64 -w0 .codex/auth.json # macOS base64 -i .codex/auth.json
-
Store the output as a repository secret named
CODEX_CONFIG_B64: Settings → Secrets and variables → Actions → New repository secret -
Reference it in your workflow:
codex_config: ${{ secrets.CODEX_CONFIG_B64 }}
Token lifetime: The access token expires frequently but is refreshed automatically using the refresh token. The refresh token itself eventually expires (typically weeks to months). When authentication starts failing, repeat steps 2–6 to obtain fresh tokens.
| API Key | OAuth (Device Auth) | |
|---|---|---|
| Setup | Simple (paste key) | Requires device-auth flow via Docker |
| Token refresh | Never expires (until revoked) | Auto-refreshes; refresh token expires after weeks/months |
| Best for | CI/CD with platform API access | ChatGPT Pro/Plus subscribers without a separate API key |
| Credential file | N/A (action runs codex-bootstrap) |
auth.json with OAuth tokens |
You can pass a base64-encoded config.toml to customize Codex behavior (model defaults, personality, sandbox mode, etc.). This works with either authentication method.
-
Create a
config.tomlwith your preferences:model = "o4-mini" sandbox_mode = "off"
-
Encode it:
# Linux base64 -w0 config.toml # macOS base64 -i config.toml
-
Store the output as a repository secret (e.g.,
CODEX_CONFIG_TOML_B64) and reference it:codex_config_toml: ${{ secrets.CODEX_CONFIG_TOML_B64 }}
Note: The
modelandreasoning_effortaction inputs take precedence over values inconfig.tomlwhen both are provided.
| Input | Required | Default | Description |
|---|---|---|---|
prompt |
Yes | — | Instructions for Codex (e.g., "Summarize these changes for operators"). |
input_text |
No | "" |
Data to process (e.g., changelog content). Appended after the prompt with a --- separator when provided. |
openai_api_key |
No | "" |
OpenAI API key. Mutually exclusive with codex_config. |
codex_config |
No | "" |
Base64-encoded auth.json from a prior device-auth session. Mutually exclusive with openai_api_key. |
codex_config_toml |
No | "" |
Base64-encoded config.toml with Codex preferences (model, personality, etc.). Works with either auth method. |
image_version |
No | 0.114.0 |
codex-docker image version tag used for the container. |
model |
No | "" |
Model override passed to codex exec --model. When omitted, the model configured in your Codex config is used. |
reasoning_effort |
No | "" |
Reasoning effort level (minimal, low, medium, high, xhigh). Passed as model_reasoning_effort config override. |
network_access |
No | false |
Allow Codex to make network requests (curl, wget, etc.) during execution. When false, a prompt-level policy instructs the model not to use networking tools. |
quiet |
No | true |
Suppress verbose Codex output (tool calls, grep results, file reads) from workflow logs. Prevents source code leakage in CI logs. Set to false for debugging. |
timeout |
No | 300 |
Maximum seconds allowed for Codex execution before the step is killed. |
| Output | Description |
|---|---|
result |
Text output produced by Codex. |
Generate a changelog with git-cliff, pass it to Codex for operator-friendly summarization, then use the result as a pull request body.
name: Release Summary
on:
push:
tags:
- 'v*'
jobs:
summarize:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Generate changelog with git-cliff
id: cliff
run: |
pip install git-cliff
CHANGELOG=$(git cliff --latest --strip all)
echo "changelog<<EOF" >> "$GITHUB_OUTPUT"
echo "$CHANGELOG" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Summarize changelog with Codex
id: codex
uses: icoretech/codex-action@v0
with:
prompt: |
You are a technical writer. Summarize the following changelog
into a concise, human-readable release summary suitable for
an operator audience. Focus on user impact, not implementation
details. Use bullet points.
input_text: ${{ steps.cliff.outputs.changelog }}
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
- name: Open release PR with summary
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUMMARY: ${{ steps.codex.outputs.result }}
run: |
gh pr create \
--title "Release ${{ github.ref_name }}" \
--body "$SUMMARY" \
--base main \
--head "${{ github.ref_name }}"Automatically generate a pull request description by diffing the branch against the base.
name: Generate PR Description
on:
pull_request:
types: [opened]
jobs:
describe:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get diff
id: diff
run: |
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD -- . ':(exclude)*.lock')
echo "diff<<EOF" >> "$GITHUB_OUTPUT"
echo "$DIFF" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Generate description with Codex
id: codex
uses: icoretech/codex-action@v0
with:
prompt: |
You are a senior engineer reviewing a pull request. Given the
following git diff, write a clear PR description with these
sections: Summary, Changes, and Testing Notes.
input_text: ${{ steps.diff.outputs.diff }}
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
- name: Update PR body
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DESCRIPTION: ${{ steps.codex.outputs.result }}
run: |
gh pr edit ${{ github.event.pull_request.number }} \
--body "$DESCRIPTION"Run an automated code review on every pull request and post the result as a comment.
name: Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get diff
id: diff
run: |
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD)
echo "diff<<EOF" >> "$GITHUB_OUTPUT"
echo "$DIFF" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Review with Codex
id: codex
uses: icoretech/codex-action@v0
with:
prompt: |
You are an experienced software engineer performing a code review.
Analyze the following diff and provide:
- A brief summary of what changed
- Any potential bugs or logic errors
- Security concerns if applicable
- Suggestions for improvement
Be concise and constructive.
input_text: ${{ steps.diff.outputs.diff }}
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
- name: Post review comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REVIEW: ${{ steps.codex.outputs.result }}
run: |
gh pr comment ${{ github.event.pull_request.number }} \
--body "## Automated Code Review
$REVIEW
---
*Generated by [codex-action](https://github.com/icoretech/codex-action)*"Automatically analyze new issues by cloning relevant repositories and posting an implementation plan as a comment. Codex explores the actual source code, references specific files and line numbers, and produces a grounded technical plan.
This recipe demonstrates:
- Fetching rich issue metadata (labels, comments, timeline, project board fields)
- Resolving issue signals (labels, title brackets, body mentions) to repository names
- Cloning matched repos so Codex can read the source code
- One-shot analysis with structured output and a bail-out mechanism
- Auto-labeling based on Codex's analysis
- Comment upsert (update existing comment on re-run instead of appending)
name: Issue Triage
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to analyze'
required: true
type: number
concurrency:
group: issue-triage-${{ github.event.issue.number || inputs.issue_number }}
cancel-in-progress: true
jobs:
triage:
runs-on: ubuntu-latest
permissions:
issues: write
env:
ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }}
steps:
# Wait for the author to finish editing (skip on manual dispatch)
- name: Wait for issue to settle
if: github.event_name == 'issues'
run: sleep 300
- name: Fetch issue details
id: issue
env:
GH_TOKEN: ${{ github.token }}
run: |
repo="${{ github.repository }}"
state=$(gh api "repos/${repo}/issues/${ISSUE_NUMBER}" --jq '.state')
if [ "$state" != "open" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
gh api "repos/${repo}/issues/${ISSUE_NUMBER}" \
--jq '{number, title, body, state, labels: [.labels[].name],
assignees: [.assignees[].login], user: .user.login,
created_at, comment_count: .comments}' > /tmp/issue.json
# Clone repos mentioned in the issue (needs a PAT for private repos)
- name: Clone relevant repos
if: steps.issue.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.ORG_PAT }}
run: |
mkdir -p "$GITHUB_WORKSPACE/repos"
# Extract repo names from labels, title brackets, body mentions
# (your resolution logic here)
for repo_name in $REPOS; do
gh repo clone "your-org/$repo_name" \
"$GITHUB_WORKSPACE/repos/$repo_name" \
-- --depth=1 --no-single-branch 2>/dev/null || true
done
# Assemble context file for Codex
- name: Prepare context
if: steps.issue.outputs.skip != 'true'
run: |
mkdir -p "$GITHUB_WORKSPACE/repos"
{
echo "=== ISSUE ==="
cat /tmp/issue.json
echo ""
echo "=== CLONED REPOS ==="
for d in "$GITHUB_WORKSPACE/repos"/*/; do
repo=$(basename "$d")
echo "--- $repo ---"
find "$d" -maxdepth 3 -not -path '*/.git/*' \
-not -path '*/node_modules/*' | head -200
done
} > "$GITHUB_WORKSPACE/context.txt"
- name: Analyze with Codex
if: steps.issue.outputs.skip != 'true'
id: analysis
uses: icoretech/codex-action@v0
with:
prompt: |
You are a senior engineering triage assistant. This is a ONE-SHOT
analysis — do NOT ask questions or defer decisions.
## Execution environment
You are running inside a read-only GitHub Actions workflow. Do NOT
attempt git push, commit, or any state-modifying operations. Your
purpose is to examine code and produce a written technical plan.
## Available tools
Only: bash, git, grep, ripgrep (rg), sed, awk, find, cat, jq, curl.
NO npm, node, python, or other runtimes are installed.
## Context
Read /workspace/context.txt for issue details and repo listings.
Source code is under /workspace/repos/ — explore it thoroughly.
When linking to files, use GitHub URLs:
https://github.com/your-org/{repo}/blob/{branch}/{path}#L{line}
## Output format
1. **Repos involved** — which repo(s) and why
2. **Analysis** — what the issue asks for, grounded in actual code
3. **Implementation plan** — numbered steps with effort estimates
4. **Risks and dependencies**
5. **Assumptions**
## Auto-labeling
At the very end, on a separate line:
<!-- CODEX_LABELS: repo1,repo2,repo3 -->
## Bail-out
If the issue is too vague, already resolved, or not code-related,
respond with ONLY: SKIP
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
network_access: 'false'
timeout: '1800'
- name: Post analysis comment
if: >-
steps.issue.outputs.skip != 'true'
&& steps.analysis.outputs.result != ''
&& steps.analysis.outputs.result != 'SKIP'
env:
GH_TOKEN: ${{ github.token }}
CODEX_RESULT: ${{ steps.analysis.outputs.result }}
run: |
clean_result=$(printf '%s\n' "$CODEX_RESULT" \
| sed '/<!-- CODEX_LABELS:.*-->/d')
{
echo "### Codex Triage Analysis"
echo ""
echo "> [!WARNING]"
echo "> Automated preliminary analysis — may contain inaccuracies."
echo ""
printf '%s\n' "$clean_result"
} > /tmp/comment.md
# Upsert: update existing comment or create new
existing=$(gh api \
"repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments?per_page=100" \
--jq '[.[] | select(.body | contains("Codex Triage"))] | last | .id // empty')
if [ -n "$existing" ]; then
gh api "repos/${{ github.repository }}/issues/comments/${existing}" \
-X PATCH -F "body=@/tmp/comment.md"
else
gh issue comment "$ISSUE_NUMBER" \
--repo "${{ github.repository }}" --body-file /tmp/comment.md
fi
- name: Apply repo labels
if: >-
steps.issue.outputs.skip != 'true'
&& steps.analysis.outputs.result != ''
&& steps.analysis.outputs.result != 'SKIP'
env:
GH_TOKEN: ${{ github.token }}
CODEX_RESULT: ${{ steps.analysis.outputs.result }}
run: |
labels=$(printf '%s\n' "$CODEX_RESULT" \
| grep -o 'CODEX_LABELS: [^ ]*' | cut -d' ' -f2 || true)
[ -z "$labels" ] && exit 0
IFS=',' read -ra REPO_LABELS <<< "$labels"
for label in "${REPO_LABELS[@]}"; do
gh api "repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/labels" \
-X POST -f "labels[]=$label" 2>/dev/null || true
doneKey implementation notes:
safe.directory: codex-action automatically configuresGIT_CONFIG_GLOBALinside the Docker container, so Codex can run git commands on repos cloned by the runner without ownership errors.--no-single-branch: Cloning with this flag lets Codex check out non-default branches (e.g.,develop, feature branches) when the issue refers to a specific environment.- Prompt engineering: The prompt explicitly lists available tools (preventing wasted tokens on
npm: not found), enforces read-only behavior, and includes aSKIPbail-out for non-code issues. - Comment upsert: On re-runs, the workflow updates the existing triage comment instead of appending a new one.
- Auto-labeling: Codex outputs a hidden HTML comment with repo names; the workflow parses it and applies them as issue labels.
Use the model input to target a specific model for a particular task.
name: Deep Analysis
on:
workflow_dispatch:
inputs:
target_file:
description: File to analyze
required: true
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Read target file
id: content
run: |
CONTENT=$(cat "${{ github.event.inputs.target_file }}")
echo "content<<EOF" >> "$GITHUB_OUTPUT"
echo "$CONTENT" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Analyze with Codex
id: codex
uses: icoretech/codex-action@v0
with:
prompt: |
Perform a thorough security and correctness analysis of the
following source file. Identify any vulnerabilities, edge cases,
and areas that need hardening.
input_text: ${{ steps.content.outputs.content }}
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
model: o4-mini
timeout: "600"
- name: Print analysis
run: echo "${{ steps.codex.outputs.result }}"Symptoms: The action fails early with an error referencing codex-bootstrap api-key-login or an authentication/authorization error from the Codex CLI.
Fixes:
- API key auth: Verify the secret
OPENAI_API_KEYis set correctly in your repository and that the key is active at platform.openai.com/api-keys. - Config auth: The OAuth token embedded in
auth.jsonmay have expired. Re-run the device-auth flow, re-encode the file, and update theCODEX_CONFIG_B64secret.
Symptoms: The action fails with a message like Unable to find image 'ghcr.io/icoretech/codex-docker:...' or an HTTP 429 / rate-limit error.
Fixes:
- Confirm the
image_versioninput matches an available tag on ghcr.io/icoretech/codex-docker. - If you are hitting anonymous pull rate limits, authenticate your runner to GHCR by adding a
docker loginstep before the action.
Symptoms: The step is killed after 300 seconds (the default) with a non-zero exit code.
Fix: Increase the timeout input:
with:
timeout: "600"Symptoms: steps.codex.outputs.result is an empty string even though the step succeeded.
Fixes:
- Review your
prompt— vague instructions can lead to empty or minimal responses. - Check your OpenAI API status at platform.openai.com/usage.
- If using
input_text, verify the input is not empty before the action runs.
Symptoms: The action fails immediately with:
Exactly one of openai_api_key or codex_config must be provided, got both
or
Exactly one of openai_api_key or codex_config must be provided, got neither
Fix: Provide exactly one authentication method. Remove the unused input or ensure the referenced secret is not empty. Both inputs default to "", so an unset secret resolves to an empty string and is treated as "not provided".
- bats-core for running the test suite
- shellcheck for static analysis of the shell script
bats tests/entrypoint.batsThe test suite uses a mock docker binary (loaded from tests/test_helper/mocks.bash) so no real Docker daemon or network access is required.
shellcheck entrypoint.shThis repository uses release-please with the simple release type. Merging a conventional-commit PR into main triggers release-please to open a release PR. When that release PR is merged:
- A new semver tag (e.g.,
v0.2.0) is created automatically. - The
update-major-tagjob force-updates the corresponding major tag (e.g.,v0) to point at the new release.
Users pinning to a major tag (e.g., uses: icoretech/codex-action@v0) always receive the latest patch and minor releases within that major automatically.