Skip to content

Commit 8bf499b

Browse files
committed
add trigger-driven execution and progress tracking
1 parent 8db38a0 commit 8bf499b

13 files changed

Lines changed: 2128 additions & 55 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# codex-action Changelog
22

3+
## [Unreleased]
4+
5+
### Migration notes
6+
7+
- This release is additive for `v1.x`: existing workflows that only use `prompt`/`prompt-file` and consume `final-message` continue to work without changes.
8+
- New observability inputs are optional: `capture-json-events`, `json-events-file`, and `write-step-summary`.
9+
- New outputs are now available when needed: `structured-output`, `usage-json`, `execution-file`, `session-id`, `conclusion`, `triggered`, and `tracking-comment-id`.
10+
- 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`.
11+
- Progress comments are opt-in (`track-progress`) and require suitable workflow permissions (for example `issues: write` / `pull-requests: write`).
12+
- `structured-output` is only populated when `output-schema` (or `output-schema-file`) is used and the final Codex message is valid JSON.
13+
- No cross-run session resume behavior is introduced in this release (intentional for ephemeral runner compatibility).
14+
315
## [v1.4](https://github.com/openai/codex-action/tree/v1.4) (2005-11-19)
416

517
- [#58](https://github.com/openai/codex-action/pull/58) revert #56 and use the latest stable version of Codex CLI again

README.md

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ For a ChatGPT subscription auth variant, see `examples/code-review-subscription.
124124
| `codex-user` | Username to run Codex as when `safety-strategy` is `unprivileged-user`. | `""` |
125125
| `allow-users` | List of GitHub usernames who can trigger the action in addition to those who have write access to the repo. | `""` |
126126
| `allow-bots` | Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check. | `false` |
127+
| `capture-json-events` | Capture `codex exec --json` output and parse metadata (session ID + usage). | `false` |
128+
| `json-events-file` | Optional path to write raw JSONL events when JSON capture is enabled. | `""` |
129+
| `write-step-summary` | Write run metadata and a final-message preview to GitHub Step Summary. | `true` |
130+
| `trigger-phrase` | Optional phrase that must appear in issue/PR/comment text for the action to proceed. | `""` |
131+
| `label-trigger` | Optional issue/PR label name that triggers execution. | `""` |
132+
| `assignee-trigger` | Optional issue/PR assignee username that triggers execution. | `""` |
133+
| `track-progress` | Create/update a progress comment on issue/PR events while Codex runs. | `false` |
134+
| `use-sticky-comment` | When tracking progress, reuse one marker-based comment instead of creating new comments. | `false` |
135+
| `sanitize-github-context`| Sanitize untrusted GitHub payload text before deriving prompts from trigger-driven events. | `true` |
127136

128137
## Safety Strategy
129138

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

144153
## Outputs
145154

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

150166
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:
151167

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

204+
### Trigger-driven workflows
205+
You can gate execution on GitHub event payload data by setting one or more of:
206+
`trigger-phrase`, `label-trigger`, or `assignee-trigger`.
207+
208+
- If no trigger inputs are configured, behavior is unchanged (the action proceeds).
209+
- If trigger inputs are configured and none match, the action no-ops cleanly with
210+
output `triggered=false`.
211+
- If trigger inputs are configured and a match occurs, the action can derive a
212+
prompt from the event payload when `prompt`/`prompt-file` are not provided.
213+
214+
The `sanitize-github-context` input is `true` by default to strip hidden markup
215+
and zero-width characters before deriving prompt text.
216+
See [`examples/triggered-progress-review.yml`](./examples/triggered-progress-review.yml)
217+
for an end-to-end trigger workflow.
218+
219+
### JSON event capture and summaries
220+
Enable `capture-json-events: true` when you want machine-readable execution
221+
metadata from `codex exec --json`. This powers outputs like `session-id`,
222+
`usage-json`, and `execution-file`.
223+
224+
You can control where the raw JSONL goes with `json-events-file`; otherwise the
225+
action writes to a temporary file and exposes its path via `execution-file`.
226+
227+
`write-step-summary` defaults to `true` and appends run metadata plus a concise
228+
final-message preview to the GitHub Step Summary.
229+
230+
### Progress comments
231+
Set `track-progress: true` on issue/PR events to create/update a progress comment
232+
while Codex runs. Add `use-sticky-comment: true` to reuse one marker-based comment
233+
across runs and reduce comment noise.
234+
188235
- Run this action after `actions/checkout@v5` so Codex has access to your repository contents.
189236
- 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.
190237
- 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.

action.yml

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,67 @@ inputs:
108108
description: "Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check."
109109
required: false
110110
default: "false"
111+
capture-json-events:
112+
description: "Capture codex exec JSON event stream (`codex exec --json`) for metadata extraction."
113+
required: false
114+
default: "false"
115+
json-events-file:
116+
description: "Optional file path where raw codex exec JSONL events should be written."
117+
required: false
118+
default: ""
119+
write-step-summary:
120+
description: "Write execution metadata and a final message preview to the GitHub step summary."
121+
required: false
122+
default: "true"
123+
trigger-phrase:
124+
description: "Optional phrase that must appear in issue/PR/comment text for this action to run."
125+
required: false
126+
default: ""
127+
label-trigger:
128+
description: "Optional issue/PR label name that triggers execution."
129+
required: false
130+
default: ""
131+
assignee-trigger:
132+
description: "Optional issue/PR assignee username that triggers execution."
133+
required: false
134+
default: ""
135+
track-progress:
136+
description: "Create/update a progress comment on issue/PR events while Codex runs."
137+
required: false
138+
default: "false"
139+
use-sticky-comment:
140+
description: "When tracking progress, reuse a single marker-based comment when possible."
141+
required: false
142+
default: "false"
143+
sanitize-github-context:
144+
description: "Sanitize untrusted GitHub payload text before deriving prompts from trigger events."
145+
required: false
146+
default: "true"
111147
outputs:
112148
final-message:
113149
description: "Raw output emitted by `codex exec`."
114150
value: ${{ steps.run_codex.outputs['final-message'] }}
151+
structured-output:
152+
description: "Structured JSON output (stringified) when output-schema is used and the final message is valid JSON."
153+
value: ${{ steps.run_codex.outputs['structured-output'] }}
154+
usage-json:
155+
description: "Token usage metadata extracted from codex exec JSON events."
156+
value: ${{ steps.run_codex.outputs['usage-json'] }}
157+
execution-file:
158+
description: "Path to the raw codex exec JSONL event log when capture-json-events is enabled."
159+
value: ${{ steps.run_codex.outputs['execution-file'] }}
160+
session-id:
161+
description: "Session/thread ID extracted from codex exec JSON events (diagnostic only)."
162+
value: ${{ steps.run_codex.outputs['session-id'] }}
163+
conclusion:
164+
description: "Run conclusion from the codex step (`success` or `failure`)."
165+
value: ${{ steps.run_codex.outputs['conclusion'] }}
166+
triggered:
167+
description: "Whether trigger conditions matched and the action proceeded."
168+
value: ${{ steps.detect_trigger.outputs.triggered }}
169+
tracking-comment-id:
170+
description: "Issue/PR comment ID used for progress tracking when enabled."
171+
value: ${{ steps.progress_start.outputs['comment-id'] }}
115172
runs:
116173
using: "composite"
117174
steps:
@@ -132,7 +189,18 @@ runs:
132189
with:
133190
node-version: "20"
134191

192+
- name: Detect trigger
193+
id: detect_trigger
194+
shell: bash
195+
run: |
196+
node "${{ github.action_path }}/dist/main.js" detect-trigger \
197+
--trigger-phrase "${{ inputs['trigger-phrase'] }}" \
198+
--label-trigger "${{ inputs['label-trigger'] }}" \
199+
--assignee-trigger "${{ inputs['assignee-trigger'] }}" \
200+
--sanitize-github-context "${{ inputs['sanitize-github-context'] }}"
201+
135202
- name: Check repository write access
203+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
136204
env:
137205
GITHUB_TOKEN: ${{ github.token }}
138206
shell: bash
@@ -142,15 +210,18 @@ runs:
142210
--allow-users "${{ inputs['allow-users'] }}"
143211
144212
- name: Install Codex CLI
213+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
145214
shell: bash
146215
run: npm install -g "@openai/codex@${{ inputs['codex-version'] }}"
147216

148217
- name: Install Codex Responses API proxy
218+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
149219
shell: bash
150220
run: npm install -g "@openai/codex-responses-api-proxy@${{ inputs['codex-version'] }}"
151221

152222
- name: Resolve Codex home
153223
id: resolve_home
224+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
154225
shell: bash
155226
run: |
156227
node "${{ github.action_path }}/dist/main.js" resolve-codex-home \
@@ -161,13 +232,15 @@ runs:
161232
162233
- name: Determine server info path
163234
id: derive_server_info
235+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
164236
shell: bash
165237
run: |
166238
server_info_file="${{ steps.resolve_home.outputs.codex-home }}/${{ github.run_id }}.json"
167239
echo "server_info_file=$server_info_file" >> "$GITHUB_OUTPUT"
168240
169241
- name: Check server info file presence
170242
id: check_server_info
243+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' }}
171244
shell: bash
172245
run: |
173246
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
@@ -179,7 +252,7 @@ runs:
179252
180253
- name: Check Responses API proxy status
181254
id: start_proxy
182-
if: ${{ inputs['openai-api-key'] != '' }}
255+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['openai-api-key'] != '' }}
183256
shell: bash
184257
run: |
185258
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
@@ -191,7 +264,7 @@ runs:
191264
fi
192265
193266
- name: Write Codex auth.json (subscription auth)
194-
if: ${{ inputs['codex-auth-json-b64'] != '' }}
267+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['codex-auth-json-b64'] != '' }}
195268
env:
196269
CODEX_AUTH_JSON_B64: ${{ inputs['codex-auth-json-b64'] }}
197270
shell: bash
@@ -206,7 +279,7 @@ runs:
206279
# key do not end up in the memory of the `codex-responses-api-proxy`
207280
# process where environment variables are stored.
208281
- name: Start Responses API proxy
209-
if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
282+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
210283
env:
211284
PROXY_API_KEY: ${{ inputs['openai-api-key'] }}
212285
shell: bash
@@ -228,7 +301,7 @@ runs:
228301
) &
229302
230303
- name: Wait for Responses API proxy
231-
if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
304+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
232305
shell: bash
233306
run: |
234307
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
@@ -251,12 +324,12 @@ runs:
251324
# This step has an output named `port`.
252325
- name: Read server info
253326
id: read_server_info
254-
if: ${{ inputs['openai-api-key'] != '' || steps.check_server_info.outputs.exists == 'true' }}
327+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && (inputs['openai-api-key'] != '' || steps.check_server_info.outputs.exists == 'true') }}
255328
shell: bash
256329
run: node "${{ github.action_path }}/dist/main.js" read-server-info "${{ steps.derive_server_info.outputs.server_info_file }}"
257330

258331
- name: Write Codex proxy config
259-
if: ${{ inputs['openai-api-key'] != '' }}
332+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['openai-api-key'] != '' }}
260333
shell: bash
261334
run: |
262335
node "${{ github.action_path }}/dist/main.js" write-proxy-config \
@@ -265,7 +338,7 @@ runs:
265338
--safety-strategy "${{ inputs['safety-strategy'] }}"
266339
267340
- name: Drop sudo privilege, if appropriate
268-
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs['codex-auth-json-b64'] != '') }}
341+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs['codex-auth-json-b64'] != '') }}
269342
shell: bash
270343
run: |
271344
case "${RUNNER_OS}" in
@@ -282,7 +355,7 @@ runs:
282355
esac
283356
284357
- name: Verify sudo privilege removed
285-
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs['codex-auth-json-b64'] != '') }}
358+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs['codex-auth-json-b64'] != '') }}
286359
shell: bash
287360
run: |
288361
if sudo -n true 2>/dev/null; then
@@ -291,12 +364,26 @@ runs:
291364
fi
292365
echo "Confirmed sudo privilege is disabled."
293366
367+
- name: Start progress comment
368+
id: progress_start
369+
if: ${{ inputs['track-progress'] == 'true' && steps.detect_trigger.outputs.triggered == 'true' && (inputs.prompt != '' || inputs['prompt-file'] != '' || steps.detect_trigger.outputs['derived-prompt-file'] != '') }}
370+
env:
371+
GITHUB_TOKEN: ${{ github.token }}
372+
shell: bash
373+
run: |
374+
node "${{ github.action_path }}/dist/main.js" update-progress-comment \
375+
--mode "start" \
376+
--use-sticky-comment "${{ inputs['use-sticky-comment'] }}" \
377+
--comment-id "" \
378+
--conclusion ""
379+
294380
- name: Run codex exec
295381
id: run_codex
296-
if: ${{ inputs.prompt != '' || inputs['prompt-file'] != '' }}
382+
if: ${{ steps.detect_trigger.outputs.triggered == 'true' && (inputs.prompt != '' || inputs['prompt-file'] != '' || steps.detect_trigger.outputs['derived-prompt-file'] != '') }}
297383
env:
298384
CODEX_PROMPT: ${{ inputs.prompt }}
299385
CODEX_PROMPT_FILE: ${{ inputs['prompt-file'] }}
386+
CODEX_DERIVED_PROMPT_FILE: ${{ steps.detect_trigger.outputs['derived-prompt-file'] }}
300387
CODEX_OUTPUT_FILE: ${{ inputs['output-file'] }}
301388
CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
302389
CODEX_WORKING_DIRECTORY: ${{ inputs['working-directory'] || github.workspace }}
@@ -309,12 +396,20 @@ runs:
309396
CODEX_SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
310397
CODEX_USER: ${{ inputs['codex-user'] }}
311398
CODEX_PASS_THROUGH_ENV: ${{ inputs['pass-through-env'] }}
399+
CODEX_CAPTURE_JSON_EVENTS: ${{ inputs['capture-json-events'] }}
400+
CODEX_JSON_EVENTS_FILE: ${{ inputs['json-events-file'] }}
401+
CODEX_WRITE_STEP_SUMMARY: ${{ inputs['write-step-summary'] }}
312402
FORCE_COLOR: 1
313403
shell: bash
314404
run: |
405+
resolved_prompt_file="$CODEX_PROMPT_FILE"
406+
if [ -z "$CODEX_PROMPT" ] && [ -z "$resolved_prompt_file" ] && [ -n "$CODEX_DERIVED_PROMPT_FILE" ]; then
407+
resolved_prompt_file="$CODEX_DERIVED_PROMPT_FILE"
408+
fi
409+
315410
node "${{ github.action_path }}/dist/main.js" run-codex-exec \
316411
--prompt "${CODEX_PROMPT}" \
317-
--prompt-file "${CODEX_PROMPT_FILE}" \
412+
--prompt-file "${resolved_prompt_file}" \
318413
--output-file "$CODEX_OUTPUT_FILE" \
319414
--codex-home "$CODEX_HOME" \
320415
--cd "$CODEX_WORKING_DIRECTORY" \
@@ -326,4 +421,20 @@ runs:
326421
--effort "$CODEX_EFFORT" \
327422
--safety-strategy "$CODEX_SAFETY_STRATEGY" \
328423
--codex-user "$CODEX_USER" \
329-
--pass-through-env "$CODEX_PASS_THROUGH_ENV"
424+
--pass-through-env "$CODEX_PASS_THROUGH_ENV" \
425+
--capture-json-events "$CODEX_CAPTURE_JSON_EVENTS" \
426+
--json-events-file "$CODEX_JSON_EVENTS_FILE" \
427+
--write-step-summary "$CODEX_WRITE_STEP_SUMMARY"
428+
429+
- name: Finalize progress comment
430+
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'] != '') }}
431+
env:
432+
GITHUB_TOKEN: ${{ github.token }}
433+
CODEX_FINAL_MESSAGE: ${{ steps.run_codex.outputs['final-message'] }}
434+
shell: bash
435+
run: |
436+
node "${{ github.action_path }}/dist/main.js" update-progress-comment \
437+
--mode "finish" \
438+
--use-sticky-comment "${{ inputs['use-sticky-comment'] }}" \
439+
--comment-id "${{ steps.progress_start.outputs['comment-id'] }}" \
440+
--conclusion "${{ steps.run_codex.outcome }}"

0 commit comments

Comments
 (0)