Skip to content

Codex Plan Mode CI/CD Implementation | Building Auto Plan Review with GitHub Actions

This is a follow-up to the morning article

Morning article: Codex CLI Plan Mode Practical Guide

Goals

  • Auto-generate Codex plan mode execution plans driven by PR comments
  • Build safe automation flow that only executes approved plans
  • Implement automatic rollback and detailed logging on errors

Architecture Overview

PR Comment → Workflow Trigger → Plan Generation → Plan Review (Human) → Approval → Execute → Commit
    ↓                                  ↓                                               ↓
   /codex [prompt]              codex --plan              /approve label          codex (normal)

Flow Details: 1. Post /codex [prompt] in PR comment 2. GitHub Actions generates execution plan in plan mode 3. Auto-post generated plan to PR comment 4. Reviewer checks plan content and adds approved-plan label 5. Label triggers normal mode execution 6. Auto-commit & push changes

Implementation Steps

Step 1: Plan Generation Workflow

.github/workflows/codex-plan-generator.yml:

name: Codex Plan Generator
on:
  issue_comment:
    types: [created]

jobs:
  generate-plan:
    if: startsWith(github.event.comment.body, '/codex ')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Extract prompt
        id: prompt
        env:
          COMMENT_BODY: "${{ github.event.comment.body }}"
        run: |
          PROMPT="${COMMENT_BODY#/codex }"
          echo "text=$PROMPT" >> $GITHUB_OUTPUT

      - name: Generate execution plan
        id: plan
        env:
          PLAN_PROMPT: "${{ steps.prompt.outputs.text }}"
        run: |
          codex --plan "$PLAN_PROMPT" > plan.txt 2>&1
          EXIT_CODE=$?
          if [ $EXIT_CODE -ne 0 ]; then
            echo "error=true" >> $GITHUB_OUTPUT
          fi
        continue-on-error: true

      - name: Post plan as comment
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('plan.txt', 'utf8');
            const isError = '${{ steps.plan.outputs.error }}' === 'true';

            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: isError
                ? `❌ Plan generation failed\n\`\`\`\n${plan}\n\`\`\``
                : `## 📋 Execution Plan\n\`\`\`\n${plan}\n\`\`\`\n\n✅ Review and add \`approved-plan\` label to execute.`
            });

Step 2: Post-Approval Execution Workflow

.github/workflows/codex-executor.yml:

name: Codex Executor
on:
  pull_request:
    types: [labeled]

jobs:
  execute-plan:
    if: github.event.label.name == 'approved-plan'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Get approved prompt
        id: prompt
        uses: actions/github-script@v7
        with:
          script: |
            const comments = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number
            });

            const codexComment = comments.data
              .reverse()
              .find(c => c.body.startsWith('/codex '));

            if (!codexComment) {
              core.setFailed('No /codex command found');
              return;
            }

            const prompt = codexComment.body.replace(/^\/codex /, '');
            core.setOutput('text', prompt);

      - name: Execute with Codex
        env:
          PLAN_PROMPT: "${{ steps.prompt.outputs.text }}"
        run: |
          codex "$PLAN_PROMPT"

      - name: Commit changes
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add .
          git commit -m "chore: apply codex changes [codex]" || exit 0
          git push

Step 3: Error Handling and Rollback

Failure Detection:

- name: Execute with validation
  id: execute
  env:
    PLAN_PROMPT: "${{ steps.prompt.outputs.text }}"
  run: |
    codex "$PLAN_PROMPT" 2>&1 | tee execution.log
    EXIT_CODE=${PIPESTATUS[0]}

    if [ $EXIT_CODE -ne 0 ]; then
      echo "failed=true" >> $GITHUB_OUTPUT
    fi
  continue-on-error: true

- name: Rollback on failure
  if: steps.execute.outputs.failed == 'true'
  run: |
    git reset --hard HEAD
    git clean -fd

Detailed Log Posting:

- name: Post execution result
  uses: actions/github-script@v7
  with:
    script: |
      const fs = require('fs');
      const log = fs.readFileSync('execution.log', 'utf8');
      const failed = '${{ steps.execute.outputs.failed }}' === 'true';

      await github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: failed
          ? `❌ Execution failed\n\`\`\`\n${log}\n\`\`\``
          : `✅ Changes applied successfully\n<details><summary>Execution log</summary>\n\n\`\`\`\n${log}\n\`\`\`\n</details>`
      });

Benchmark / Comparison

ConditionManualPlan Mode ManualCI/CD IntegrationNotes
Plan review time0min (none)1-2minAuto (in PR)Async review possible with CI
Approval processNoneVerbalLabel-basedAudit log auto-recorded
Error handlingManual fixManual fixAuto rollbackProduction safety improved
Multi-person collaborationDifficultDifficultSmoothTransparency via PR comments

Actual measurement: For 5-file change task, entire flow (plan review → approval → execution) completed in average 3.2 minutes (8-10 minutes manually).

Failure Patterns and Solutions

SymptomCauseSolution
Empty plan generationPrompt extraction failedAdd debug output for steps.prompt.outputs.text
Prompt mismatch at executionComment history retrieval order errorUse .reverse() to search from latest
Execution before approvalLabel condition errorStrictly specify if: github.event.label.name == 'approved-plan'
Commit permission errorGITHUB_TOKEN scope insufficientAdd permissions: contents: write to job

Automation / Extension Ideas

  1. Multi-plan comparison: Generate plan 3 times with same prompt and visualize differences
  2. Semantic versioning integration: Auto-infer version number from change content
  3. Slack notification integration: Auto-post pending plans to Slack channel
  4. Plan approval deadline: Auto-close if not approved within 24 hours
  5. Diff highlighting: Generate HTML report showing plan.txt and git diff side-by-side

Next Steps