Skip to content

Codex CLI Upstream Watch #1

Codex CLI Upstream Watch

Codex CLI Upstream Watch #1

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}`);