Codex CLI Upstream Watch #1
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: Codex CLI Upstream Watch | |
| on: | |
| schedule: | |
| - cron: '0 6 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| issues: write | |
| concurrency: | |
| group: codex-cli-upstream-watch | |
| cancel-in-progress: false | |
| jobs: | |
| detect-and-open-issue: | |
| name: Detect Codex CLI Upstream Updates | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository with submodules | |
| uses: actions/checkout@v5 | |
| with: | |
| submodules: recursive | |
| fetch-depth: 0 | |
| - name: Detect updates in openai/codex | |
| id: detect | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| SUBMODULE_PATH="submodules/openai-codex" | |
| SURFACE_PATHS=( | |
| "codex-rs" | |
| "docs" | |
| "README.md" | |
| "CHANGELOG.md" | |
| "Cargo.toml" | |
| "Cargo.lock" | |
| "package.json" | |
| ) | |
| if [ ! -d "$SUBMODULE_PATH/.git" ] && [ ! -f "$SUBMODULE_PATH/.git" ]; then | |
| echo "Submodule not initialized: $SUBMODULE_PATH" | |
| echo "has_update=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| current_sha="$(git -C "$SUBMODULE_PATH" rev-parse HEAD)" | |
| default_branch="$(git -C "$SUBMODULE_PATH" remote show origin | sed -n '/HEAD branch/s/.*: //p')" | |
| if [ -z "$default_branch" ]; then | |
| default_branch="main" | |
| fi | |
| git -C "$SUBMODULE_PATH" fetch origin "$default_branch" --depth=512 | |
| latest_sha="$(git -C "$SUBMODULE_PATH" rev-parse FETCH_HEAD)" | |
| echo "current_sha=$current_sha" >> "$GITHUB_OUTPUT" | |
| echo "latest_sha=$latest_sha" >> "$GITHUB_OUTPUT" | |
| echo "default_branch=$default_branch" >> "$GITHUB_OUTPUT" | |
| if [ "$current_sha" = "$latest_sha" ]; then | |
| echo "No upstream updates." | |
| echo "has_update=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| changed_files="$(git -C "$SUBMODULE_PATH" diff --name-only "$current_sha" "$latest_sha")" | |
| if [ -z "$changed_files" ]; then | |
| echo "has_update=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "has_update=true" >> "$GITHUB_OUTPUT" | |
| cli_changed_files="$(printf '%s\n' "$changed_files" | grep -E '^(codex-rs/|docs/|README\.md|CHANGELOG\.md|Cargo\.toml|Cargo\.lock|package\.json)' || true)" | |
| if [ -z "$cli_changed_files" ]; then | |
| cli_changed_files="$(printf '%s\n' "$changed_files" | head -n 200)" | |
| fi | |
| commits="$(git -C "$SUBMODULE_PATH" log --no-merges --date=short --pretty=format:'- %h %s (%ad)' "$current_sha..$latest_sha" | head -n 100)" | |
| if [ -z "$commits" ]; then | |
| commits='- No commit summary available.' | |
| fi | |
| surface_diff="$(git -C "$SUBMODULE_PATH" diff --unified=0 "$current_sha" "$latest_sha" -- "${SURFACE_PATHS[@]}" || true)" | |
| changed_flags="$(printf '%s\n' "$surface_diff" | grep -Eo -- '--[a-z0-9][a-z0-9-]*' | sort -u | head -n 200 || true)" | |
| changed_models="$(printf '%s\n' "$surface_diff" | grep -Eo -- 'gpt-[0-9][a-z0-9.-]*' | sort -u | head -n 200 || true)" | |
| changed_features="$(printf '%s\n' "$surface_diff" | grep -Eo -- 'features\.[a-zA-Z0-9_.-]+' | sed 's/^features\.//' | sort -u | head -n 200 || true)" | |
| if [ -z "$changed_flags" ]; then | |
| changed_flags='- (no CLI flag tokens detected from diff)' | |
| else | |
| changed_flags="$(printf '%s\n' "$changed_flags" | sed 's/^/- `/' | sed 's/$/`/')" | |
| fi | |
| if [ -z "$changed_models" ]; then | |
| changed_models='- (no model tokens detected from diff)' | |
| else | |
| changed_models="$(printf '%s\n' "$changed_models" | sed 's/^/- `/' | sed 's/$/`/')" | |
| fi | |
| if [ -z "$changed_features" ]; then | |
| changed_features='- (no feature tokens detected from diff)' | |
| else | |
| changed_features="$(printf '%s\n' "$changed_features" | sed 's/^/- `/' | sed 's/$/`/')" | |
| fi | |
| { | |
| echo "cli_changed_files<<EOF" | |
| echo "$cli_changed_files" | |
| echo "EOF" | |
| echo "commits<<EOF" | |
| echo "$commits" | |
| echo "EOF" | |
| echo "changed_flags<<EOF" | |
| echo "$changed_flags" | |
| echo "EOF" | |
| echo "changed_models<<EOF" | |
| echo "$changed_models" | |
| echo "EOF" | |
| echo "changed_features<<EOF" | |
| echo "$changed_features" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create issue for Codex CLI update | |
| if: steps.detect.outputs.has_update == 'true' | |
| uses: actions/github-script@v7 | |
| env: | |
| CURRENT_SHA: ${{ steps.detect.outputs.current_sha }} | |
| LATEST_SHA: ${{ steps.detect.outputs.latest_sha }} | |
| DEFAULT_BRANCH: ${{ steps.detect.outputs.default_branch }} | |
| CLI_CHANGED_FILES: ${{ steps.detect.outputs.cli_changed_files }} | |
| COMMITS: ${{ steps.detect.outputs.commits }} | |
| CHANGED_FLAGS: ${{ steps.detect.outputs.changed_flags }} | |
| CHANGED_MODELS: ${{ steps.detect.outputs.changed_models }} | |
| CHANGED_FEATURES: ${{ steps.detect.outputs.changed_features }} | |
| ASSIGNEE: ${{ vars.SDK_SYNC_ASSIGNEE != '' && vars.SDK_SYNC_ASSIGNEE || 'copilot' }} | |
| with: | |
| script: | | |
| const currentSha = process.env.CURRENT_SHA; | |
| const latestSha = process.env.LATEST_SHA; | |
| const defaultBranch = process.env.DEFAULT_BRANCH; | |
| const changedFilesRaw = process.env.CLI_CHANGED_FILES || ''; | |
| const commitsRaw = process.env.COMMITS || ''; | |
| const changedFlags = process.env.CHANGED_FLAGS || '- (not provided)'; | |
| const changedModels = process.env.CHANGED_MODELS || '- (not provided)'; | |
| const changedFeatures = process.env.CHANGED_FEATURES || '- (not provided)'; | |
| const assignee = process.env.ASSIGNEE || 'copilot'; | |
| const shortCurrent = currentSha.slice(0, 7); | |
| const shortLatest = latestSha.slice(0, 7); | |
| const marker = `<!-- codexsharp-codex-cli-update:${latestSha} -->`; | |
| const labelName = 'codex-cli-sync'; | |
| const changedFiles = changedFilesRaw | |
| .split('\n') | |
| .map(x => x.trim()) | |
| .filter(Boolean) | |
| .slice(0, 200) | |
| .map(x => `- \`${x}\``) | |
| .join('\n'); | |
| const commits = commitsRaw.trim() || '- No commit summary available.'; | |
| const compareUrl = `https://github.com/openai/codex/compare/${currentSha}...${latestSha}`; | |
| const latestCommitUrl = `https://github.com/openai/codex/commit/${latestSha}`; | |
| try { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: labelName, | |
| color: '0e8a16', | |
| description: 'Tracks upstream Codex CLI changes from openai/codex', | |
| }); | |
| } catch (error) { | |
| core.info(`Label create skipped: ${error.message}`); | |
| } | |
| const openIssues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: labelName, | |
| per_page: 100, | |
| }); | |
| const duplicate = openIssues.find(issue => | |
| !issue.pull_request && issue.body && issue.body.includes(marker) | |
| ); | |
| if (duplicate) { | |
| core.info(`Issue already exists for ${latestSha}: #${duplicate.number}`); | |
| return; | |
| } | |
| const title = `Sync Codex CLI upstream changes (${shortCurrent} -> ${shortLatest})`; | |
| const body = [ | |
| marker, | |
| '', | |
| 'Detected upstream updates in `openai/codex` affecting CLI surface tracking.', | |
| '', | |
| `- Submodule path: \`submodules/openai-codex\``, | |
| `- Watched branch: \`${defaultBranch}\``, | |
| `- Current pinned commit: \`${currentSha}\``, | |
| `- Latest upstream commit: \`${latestSha}\``, | |
| `- Compare: ${compareUrl}`, | |
| `- Latest commit: ${latestCommitUrl}`, | |
| '', | |
| '## Changed files (CLI-relevant)', | |
| changedFiles || '- (No file list available)', | |
| '', | |
| '## Potential CLI flag changes from diff', | |
| changedFlags, | |
| '', | |
| '## Potential model changes from diff', | |
| changedModels, | |
| '', | |
| '## Potential feature changes from diff', | |
| changedFeatures, | |
| '', | |
| '## Commits', | |
| commits, | |
| '', | |
| '## Action required', | |
| '- [ ] Validate latest `codex --help` and `codex exec --help` output', | |
| '- [ ] Sync C# SDK constants/options/models with upstream CLI changes', | |
| '- [ ] Add or update tests for new flags/models/features', | |
| '- [ ] Update docs (README + docs/Features + docs/Architecture if needed)', | |
| '', | |
| `_Opened automatically by scheduled workflow 'Codex CLI Upstream Watch'._`, | |
| ].join('\n'); | |
| let issue; | |
| try { | |
| issue = await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title, | |
| body, | |
| labels: [labelName], | |
| assignees: [assignee], | |
| }); | |
| core.info(`Created and assigned issue #${issue.data.number} to @${assignee}`); | |
| } catch (error) { | |
| core.warning(`Issue assignment failed (${error.message}), creating without assignee.`); | |
| issue = await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title, | |
| body, | |
| labels: [labelName], | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.data.number, | |
| body: `Could not auto-assign @${assignee}. Please assign manually.`, | |
| }); | |
| } | |
| core.info(`Issue URL: ${issue.data.html_url}`); |