Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
- align bot bypass defaults so `allow-bots` defaults to `false` across CLI/action paths
- expand CI to run tests plus workflow linting (`actionlint` + `shellcheck`)

### Migration notes

- This release is additive for `v1.x`: existing workflows that only use `prompt`/`prompt-file` and consume `final-message` continue to work without changes.
- New observability inputs are optional: `capture-json-events`, `json-events-file`, and `write-step-summary`.
- New outputs are now available when needed: `structured-output`, `usage-json`, `execution-file`, `session-id`, `conclusion`, `triggered`, and `tracking-comment-id`.
- Trigger-based execution is opt-in. If you configure any trigger input (`trigger-phrase`, `label-trigger`, or `assignee-trigger`) and no trigger matches, the action exits cleanly with `triggered=false`.
- Progress comments are opt-in (`track-progress`) and require suitable workflow permissions (for example `issues: write` / `pull-requests: write`).
- `structured-output` is only populated when `output-schema` (or `output-schema-file`) is used and the final Codex message is valid JSON.
- No cross-run session resume behavior is introduced in this release (intentional for ephemeral runner compatibility).

## [v1.4](https://github.com/openai/codex-action/tree/v1.4) (2005-11-19)

- [#58](https://github.com/openai/codex-action/pull/58) revert #56 and use the latest stable version of Codex CLI again
Expand Down
53 changes: 50 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ For a ChatGPT subscription auth variant, see `examples/code-review-subscription.
| `codex-user` | Username to run Codex as when `safety-strategy` is `unprivileged-user`. | `""` |
| `allow-users` | List of GitHub usernames who can trigger the action in addition to those who have write access to the repo. | `""` |
| `allow-bots` | Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check. | `false` |
| `capture-json-events` | Capture `codex exec --json` output and parse metadata (session ID + usage). | `false` |
| `json-events-file` | Optional path to write raw JSONL events when JSON capture is enabled. | `""` |
| `write-step-summary` | Write run metadata and a final-message preview to GitHub Step Summary. | `true` |
| `trigger-phrase` | Optional phrase that must appear in issue/PR/comment text for the action to proceed. | `""` |
| `label-trigger` | Optional issue/PR label name that triggers execution. | `""` |
| `assignee-trigger` | Optional issue/PR assignee username that triggers execution. | `""` |
| `track-progress` | Create/update a progress comment on issue/PR events while Codex runs. | `false` |
| `use-sticky-comment` | When tracking progress, reuse one marker-based comment instead of creating new comments. | `false` |
| `sanitize-github-context`| Sanitize untrusted GitHub payload text before deriving prompts from trigger-driven events. | `true` |

## Safety Strategy

Expand All @@ -143,9 +152,16 @@ See [Protecting your `OPENAI_API_KEY`](./docs/security.md#protecting-your-openai

## Outputs

| Name | Description |
| --------------- | --------------------------------------- |
| `final-message` | Final message returned by `codex exec`. |
| Name | Description |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| `final-message` | Final message returned by `codex exec`. |
| `structured-output` | Stringified JSON when `output-schema` is used and Codex returns valid JSON in the final message. |
| `usage-json` | Stringified token usage extracted from JSON events (`input_tokens`, `cached_input_tokens`, `output_tokens`). |
| `execution-file` | Path to the raw JSONL event log when `capture-json-events` is enabled. |
| `session-id` | Session/thread ID extracted from JSON events (diagnostic only). |
| `conclusion` | Codex run result (`success` or `failure`). |
| `triggered` | Whether trigger conditions matched and the action proceeded. |
| `tracking-comment-id`| Comment ID used for progress tracking when `track-progress` is enabled. |

As we saw in the example above, we took the `final-message` output of the `run_codex` step and made it an output of the `codex` job in the workflow:

Expand Down Expand Up @@ -185,6 +201,37 @@ jobs:
full workflow, and [`docs/pass-through-env.md`](./docs/pass-through-env.md) for a
deeper walkthrough that covers rotation and troubleshooting.

### Trigger-driven workflows
You can gate execution on GitHub event payload data by setting one or more of:
`trigger-phrase`, `label-trigger`, or `assignee-trigger`.

- If no trigger inputs are configured, behavior is unchanged (the action proceeds).
- If trigger inputs are configured and none match, the action no-ops cleanly with
output `triggered=false`.
- If trigger inputs are configured and a match occurs, the action can derive a
prompt from the event payload when `prompt`/`prompt-file` are not provided.

The `sanitize-github-context` input is `true` by default to strip hidden markup
and zero-width characters before deriving prompt text.
See [`examples/triggered-progress-review.yml`](./examples/triggered-progress-review.yml)
for an end-to-end trigger workflow.

### JSON event capture and summaries
Enable `capture-json-events: true` when you want machine-readable execution
metadata from `codex exec --json`. This powers outputs like `session-id`,
`usage-json`, and `execution-file`.

You can control where the raw JSONL goes with `json-events-file`; otherwise the
action writes to a temporary file and exposes its path via `execution-file`.

`write-step-summary` defaults to `true` and appends run metadata plus a concise
final-message preview to the GitHub Step Summary.

### Progress comments
Set `track-progress: true` on issue/PR events to create/update a progress comment
while Codex runs. Add `use-sticky-comment: true` to reuse one marker-based comment
across runs and reduce comment noise.

- Run this action after `actions/checkout@v5` so Codex has access to your repository contents.
- To use a non-default Responses endpoint (for example Azure OpenAI), set `responses-api-endpoint` to the provider's URL while keeping `openai-api-key` populated; the proxy will still send `Authorization: Bearer <key>` upstream.
- If you want Codex to have access to a narrow set of privileged functionality, consider running a local MCP server that can perform these actions and configure Codex to use it.
Expand Down
133 changes: 122 additions & 11 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,67 @@ inputs:
description: "Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check."
required: false
default: "false"
capture-json-events:
description: "Capture codex exec JSON event stream (`codex exec --json`) for metadata extraction."
required: false
default: "false"
json-events-file:
description: "Optional file path where raw codex exec JSONL events should be written."
required: false
default: ""
write-step-summary:
description: "Write execution metadata and a final message preview to the GitHub step summary."
required: false
default: "true"
trigger-phrase:
description: "Optional phrase that must appear in issue/PR/comment text for this action to run."
required: false
default: ""
label-trigger:
description: "Optional issue/PR label name that triggers execution."
required: false
default: ""
assignee-trigger:
description: "Optional issue/PR assignee username that triggers execution."
required: false
default: ""
track-progress:
description: "Create/update a progress comment on issue/PR events while Codex runs."
required: false
default: "false"
use-sticky-comment:
description: "When tracking progress, reuse a single marker-based comment when possible."
required: false
default: "false"
sanitize-github-context:
description: "Sanitize untrusted GitHub payload text before deriving prompts from trigger events."
required: false
default: "true"
outputs:
final-message:
description: "Raw output emitted by `codex exec`."
value: ${{ steps.run_codex.outputs['final-message'] }}
structured-output:
description: "Structured JSON output (stringified) when output-schema is used and the final message is valid JSON."
value: ${{ steps.run_codex.outputs['structured-output'] }}
usage-json:
description: "Token usage metadata extracted from codex exec JSON events."
value: ${{ steps.run_codex.outputs['usage-json'] }}
execution-file:
description: "Path to the raw codex exec JSONL event log when capture-json-events is enabled."
value: ${{ steps.run_codex.outputs['execution-file'] }}
session-id:
description: "Session/thread ID extracted from codex exec JSON events (diagnostic only)."
value: ${{ steps.run_codex.outputs['session-id'] }}
conclusion:
description: "Run conclusion from the codex step (`success` or `failure`)."
value: ${{ steps.run_codex.outputs['conclusion'] }}
triggered:
description: "Whether trigger conditions matched and the action proceeded."
value: ${{ steps.detect_trigger.outputs.triggered }}
tracking-comment-id:
description: "Issue/PR comment ID used for progress tracking when enabled."
value: ${{ steps.progress_start.outputs['comment-id'] }}
runs:
using: "composite"
steps:
Expand All @@ -132,7 +189,18 @@ runs:
with:
node-version: "20"

- name: Detect trigger
id: detect_trigger
shell: bash
run: |
node "${{ github.action_path }}/dist/main.js" detect-trigger \
--trigger-phrase "${{ inputs['trigger-phrase'] }}" \
--label-trigger "${{ inputs['label-trigger'] }}" \
--assignee-trigger "${{ inputs['assignee-trigger'] }}" \
--sanitize-github-context "${{ inputs['sanitize-github-context'] }}"

- name: Check repository write access
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
env:
GITHUB_TOKEN: ${{ github.token }}
shell: bash
Expand All @@ -142,6 +210,7 @@ runs:
--allow-users "${{ inputs['allow-users'] }}"

- name: Install Codex CLI
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
shell: bash
run: |
version="${{ inputs['codex-version'] }}"
Expand All @@ -152,6 +221,7 @@ runs:
fi

- name: Install Codex Responses API proxy
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
shell: bash
run: |
version="${{ inputs['codex-version'] }}"
Expand All @@ -163,6 +233,7 @@ runs:

- name: Resolve Codex home
id: resolve_home
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
shell: bash
run: |
node "${{ github.action_path }}/dist/main.js" resolve-codex-home \
Expand All @@ -173,13 +244,15 @@ runs:

- name: Determine server info path
id: derive_server_info
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
shell: bash
run: |
server_info_file="${{ steps.resolve_home.outputs.codex-home }}/${{ github.run_id }}.json"
echo "server_info_file=$server_info_file" >> "$GITHUB_OUTPUT"

- name: Check server info file presence
id: check_server_info
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
shell: bash
run: |
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
Expand All @@ -191,14 +264,14 @@ runs:

- name: Probe existing Responses API proxy
id: probe_proxy
if: ${{ inputs['openai-api-key'] != '' }}
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['openai-api-key'] != '' }}
shell: bash
run: |
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
node "${{ github.action_path }}/dist/main.js" probe-proxy "$server_info_file"

- name: Write Codex auth.json (subscription auth)
if: ${{ inputs['codex-auth-json-b64'] != '' }}
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['codex-auth-json-b64'] != '' }}
env:
CODEX_AUTH_JSON_B64: ${{ inputs['codex-auth-json-b64'] }}
shell: bash
Expand All @@ -213,7 +286,7 @@ runs:
# key do not end up in the memory of the `codex-responses-api-proxy`
# process where environment variables are stored.
- name: Start Responses API proxy
if: ${{ inputs['openai-api-key'] != '' && steps.probe_proxy.outputs.healthy != 'true' }}
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['openai-api-key'] != '' && steps.probe_proxy.outputs.healthy != 'true' }}
env:
PROXY_API_KEY: ${{ inputs['openai-api-key'] }}
shell: bash
Expand All @@ -238,7 +311,7 @@ runs:
) &

- name: Wait for Responses API proxy
if: ${{ inputs['openai-api-key'] != '' && steps.probe_proxy.outputs.healthy != 'true' }}
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['openai-api-key'] != '' && steps.probe_proxy.outputs.healthy != 'true' }}
shell: bash
run: |
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
Expand All @@ -263,12 +336,12 @@ runs:
# This step has an output named `port`.
- name: Read server info
id: read_server_info
if: ${{ inputs['openai-api-key'] != '' || steps.check_server_info.outputs.exists == 'true' }}
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && (inputs['openai-api-key'] != '' || steps.check_server_info.outputs.exists == 'true') }}
shell: bash
run: node "${{ github.action_path }}/dist/main.js" read-server-info "${{ steps.derive_server_info.outputs.server_info_file }}"

- name: Write Codex proxy config
if: ${{ inputs['openai-api-key'] != '' }}
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['openai-api-key'] != '' }}
shell: bash
run: |
node "${{ github.action_path }}/dist/main.js" write-proxy-config \
Expand All @@ -277,7 +350,7 @@ runs:
--safety-strategy "${{ inputs['safety-strategy'] }}"

- name: Drop sudo privilege, if appropriate
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs['codex-auth-json-b64'] != '') }}
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs['codex-auth-json-b64'] != '') }}
shell: bash
run: |
case "${RUNNER_OS}" in
Expand All @@ -294,7 +367,7 @@ runs:
esac

- name: Verify sudo privilege removed
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs['codex-auth-json-b64'] != '') }}
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs['codex-auth-json-b64'] != '') }}
shell: bash
run: |
if sudo -n true 2>/dev/null; then
Expand All @@ -303,12 +376,26 @@ runs:
fi
echo "Confirmed sudo privilege is disabled."

- name: Start progress comment
id: progress_start
if: ${{ inputs['track-progress'] == 'true' && steps.detect_trigger.outputs.triggered == 'true' && (inputs.prompt != '' || inputs['prompt-file'] != '' || steps.detect_trigger.outputs['derived-prompt-file'] != '') }}
env:
GITHUB_TOKEN: ${{ github.token }}
shell: bash
run: |
node "${{ github.action_path }}/dist/main.js" update-progress-comment \
--mode "start" \
--use-sticky-comment "${{ inputs['use-sticky-comment'] }}" \
--comment-id "" \
--conclusion ""

- name: Run codex exec
id: run_codex
if: ${{ inputs.prompt != '' || inputs['prompt-file'] != '' }}
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && (inputs.prompt != '' || inputs['prompt-file'] != '' || steps.detect_trigger.outputs['derived-prompt-file'] != '') }}
env:
CODEX_PROMPT: ${{ inputs.prompt }}
CODEX_PROMPT_FILE: ${{ inputs['prompt-file'] }}
CODEX_DERIVED_PROMPT_FILE: ${{ steps.detect_trigger.outputs['derived-prompt-file'] }}
CODEX_OUTPUT_FILE: ${{ inputs['output-file'] }}
CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
CODEX_WORKING_DIRECTORY: ${{ inputs['working-directory'] || github.workspace }}
Expand All @@ -321,12 +408,20 @@ runs:
CODEX_SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
CODEX_USER: ${{ inputs['codex-user'] }}
CODEX_PASS_THROUGH_ENV: ${{ inputs['pass-through-env'] }}
CODEX_CAPTURE_JSON_EVENTS: ${{ inputs['capture-json-events'] }}
CODEX_JSON_EVENTS_FILE: ${{ inputs['json-events-file'] }}
CODEX_WRITE_STEP_SUMMARY: ${{ inputs['write-step-summary'] }}
FORCE_COLOR: 1
shell: bash
run: |
resolved_prompt_file="$CODEX_PROMPT_FILE"
if [ -z "$CODEX_PROMPT" ] && [ -z "$resolved_prompt_file" ] && [ -n "$CODEX_DERIVED_PROMPT_FILE" ]; then
resolved_prompt_file="$CODEX_DERIVED_PROMPT_FILE"
fi

node "${{ github.action_path }}/dist/main.js" run-codex-exec \
--prompt "${CODEX_PROMPT}" \
--prompt-file "${CODEX_PROMPT_FILE}" \
--prompt-file "${resolved_prompt_file}" \
--output-file "$CODEX_OUTPUT_FILE" \
--codex-home "$CODEX_HOME" \
--cd "$CODEX_WORKING_DIRECTORY" \
Expand All @@ -338,4 +433,20 @@ runs:
--effort "$CODEX_EFFORT" \
--safety-strategy "$CODEX_SAFETY_STRATEGY" \
--codex-user "$CODEX_USER" \
--pass-through-env "$CODEX_PASS_THROUGH_ENV"
--pass-through-env "$CODEX_PASS_THROUGH_ENV" \
--capture-json-events "$CODEX_CAPTURE_JSON_EVENTS" \
--json-events-file "$CODEX_JSON_EVENTS_FILE" \
--write-step-summary "$CODEX_WRITE_STEP_SUMMARY"

- name: Finalize progress comment
if: ${{ always() && inputs['track-progress'] == 'true' && steps.detect_trigger.outputs.triggered == 'true' && steps.progress_start.outputs['comment-id'] != '' && (inputs.prompt != '' || inputs['prompt-file'] != '' || steps.detect_trigger.outputs['derived-prompt-file'] != '') }}
env:
GITHUB_TOKEN: ${{ github.token }}
CODEX_FINAL_MESSAGE: ${{ steps.run_codex.outputs['final-message'] }}
shell: bash
run: |
node "${{ github.action_path }}/dist/main.js" update-progress-comment \
--mode "finish" \
--use-sticky-comment "${{ inputs['use-sticky-comment'] }}" \
--comment-id "${{ steps.progress_start.outputs['comment-id'] }}" \
--conclusion "${{ steps.run_codex.outcome }}"
Loading
Loading