Unicode confusable characters can bypass exec policy matching #8854
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: Issue Deduplicator | |
| on: | |
| issues: | |
| types: | |
| - opened | |
| - labeled | |
| jobs: | |
| gather-duplicates: | |
| name: Identify potential duplicates | |
| # Prevent runs on forks (requires OpenAI API key, wastes Actions minutes) | |
| if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-deduplicate')) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| codex_output: ${{ steps.select-final.outputs.codex_output }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Prepare Codex inputs | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| run: | | |
| set -eo pipefail | |
| CURRENT_ISSUE_FILE=codex-current-issue.json | |
| EXISTING_ALL_FILE=codex-existing-issues-all.json | |
| EXISTING_OPEN_FILE=codex-existing-issues-open.json | |
| gh issue list --repo "$REPO" \ | |
| --json number,title,body,createdAt,updatedAt,state,labels \ | |
| --limit 1000 \ | |
| --state all \ | |
| --search "sort:created-desc" \ | |
| | jq '[.[] | { | |
| number, | |
| title, | |
| body: ((.body // "")[0:4000]), | |
| createdAt, | |
| updatedAt, | |
| state, | |
| labels: ((.labels // []) | map(.name)) | |
| }]' \ | |
| > "$EXISTING_ALL_FILE" | |
| gh issue list --repo "$REPO" \ | |
| --json number,title,body,createdAt,updatedAt,state,labels \ | |
| --limit 1000 \ | |
| --state open \ | |
| --search "sort:created-desc" \ | |
| | jq '[.[] | { | |
| number, | |
| title, | |
| body: ((.body // "")[0:4000]), | |
| createdAt, | |
| updatedAt, | |
| state, | |
| labels: ((.labels // []) | map(.name)) | |
| }]' \ | |
| > "$EXISTING_OPEN_FILE" | |
| gh issue view "$ISSUE_NUMBER" \ | |
| --repo "$REPO" \ | |
| --json number,title,body \ | |
| | jq '{number, title, body: ((.body // "")[0:4000])}' \ | |
| > "$CURRENT_ISSUE_FILE" | |
| echo "Prepared duplicate detection input files." | |
| echo "all_issue_count=$(jq 'length' "$EXISTING_ALL_FILE")" | |
| echo "open_issue_count=$(jq 'length' "$EXISTING_OPEN_FILE")" | |
| # Prompt instructions are intentionally inline in this workflow. The old | |
| # .github/prompts/issue-deduplicator.txt file is obsolete and removed. | |
| - id: codex-all | |
| name: Find duplicates (pass 1, all issues) | |
| uses: openai/codex-action@main | |
| with: | |
| openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }} | |
| allow-users: "*" | |
| prompt: | | |
| You are an assistant that triages new GitHub issues by identifying potential duplicates. | |
| You will receive the following JSON files located in the current working directory: | |
| - `codex-current-issue.json`: JSON object describing the newly created issue (fields: number, title, body). | |
| - `codex-existing-issues-all.json`: JSON array of recent issues with states, timestamps, and labels. | |
| Instructions: | |
| - Compare the current issue against the existing issues to find up to five that appear to describe the same underlying problem or request. | |
| - Prioritize concrete overlap in symptoms, reproduction details, error signatures, and user intent. | |
| - Prefer active unresolved issues when confidence is similar. | |
| - Closed issues can still be valid duplicates if they clearly match. | |
| - Return fewer matches rather than speculative ones. | |
| - If confidence is low, return an empty list. | |
| - Include at most five issue numbers. | |
| - After analysis, provide a short reason for your decision. | |
| output-schema: | | |
| { | |
| "type": "object", | |
| "properties": { | |
| "issues": { | |
| "type": "array", | |
| "items": { | |
| "type": "string" | |
| } | |
| }, | |
| "reason": { "type": "string" } | |
| }, | |
| "required": ["issues", "reason"], | |
| "additionalProperties": false | |
| } | |
| - id: normalize-all | |
| name: Normalize pass 1 output | |
| env: | |
| CODEX_OUTPUT: ${{ steps.codex-all.outputs.final-message }} | |
| CURRENT_ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| run: | | |
| set -eo pipefail | |
| raw=${CODEX_OUTPUT//$'\r'/} | |
| parsed=false | |
| issues='[]' | |
| reason='' | |
| if [ -n "$raw" ] && printf '%s' "$raw" | jq -e 'type == "object" and (.issues | type == "array")' >/dev/null 2>&1; then | |
| parsed=true | |
| issues=$(printf '%s' "$raw" | jq -c '[.issues[] | tostring]') | |
| reason=$(printf '%s' "$raw" | jq -r '.reason // ""') | |
| else | |
| reason='Pass 1 output was empty or invalid JSON.' | |
| fi | |
| filtered=$(jq -cn --argjson issues "$issues" --arg current "$CURRENT_ISSUE_NUMBER" '[ | |
| $issues[] | |
| | tostring | |
| | select(. != $current) | |
| ] | reduce .[] as $issue ([]; if index($issue) then . else . + [$issue] end) | .[:5]') | |
| has_matches=false | |
| if [ "$(jq 'length' <<< "$filtered")" -gt 0 ]; then | |
| has_matches=true | |
| fi | |
| echo "Pass 1 parsed: $parsed" | |
| echo "Pass 1 matches after filtering: $(jq 'length' <<< "$filtered")" | |
| echo "Pass 1 reason: $reason" | |
| { | |
| echo "issues_json=$filtered" | |
| echo "reason<<EOF" | |
| echo "$reason" | |
| echo "EOF" | |
| echo "has_matches=$has_matches" | |
| } >> "$GITHUB_OUTPUT" | |
| - id: codex-open | |
| name: Find duplicates (pass 2, open issues) | |
| if: ${{ steps.normalize-all.outputs.has_matches != 'true' }} | |
| uses: openai/codex-action@main | |
| with: | |
| openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }} | |
| allow-users: "*" | |
| prompt: | | |
| You are an assistant that triages new GitHub issues by identifying potential duplicates. | |
| This is a fallback pass because a broad search did not find convincing matches. | |
| You will receive the following JSON files located in the current working directory: | |
| - `codex-current-issue.json`: JSON object describing the newly created issue (fields: number, title, body). | |
| - `codex-existing-issues-open.json`: JSON array of open issues only. | |
| Instructions: | |
| - Search only these active unresolved issues for duplicates of the current issue. | |
| - Prioritize concrete overlap in symptoms, reproduction details, error signatures, and user intent. | |
| - Prefer fewer, higher-confidence matches. | |
| - If confidence is low, return an empty list. | |
| - Include at most five issue numbers. | |
| - After analysis, provide a short reason for your decision. | |
| output-schema: | | |
| { | |
| "type": "object", | |
| "properties": { | |
| "issues": { | |
| "type": "array", | |
| "items": { | |
| "type": "string" | |
| } | |
| }, | |
| "reason": { "type": "string" } | |
| }, | |
| "required": ["issues", "reason"], | |
| "additionalProperties": false | |
| } | |
| - id: normalize-open | |
| name: Normalize pass 2 output | |
| if: ${{ steps.normalize-all.outputs.has_matches != 'true' }} | |
| env: | |
| CODEX_OUTPUT: ${{ steps.codex-open.outputs.final-message }} | |
| CURRENT_ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| run: | | |
| set -eo pipefail | |
| raw=${CODEX_OUTPUT//$'\r'/} | |
| parsed=false | |
| issues='[]' | |
| reason='' | |
| if [ -n "$raw" ] && printf '%s' "$raw" | jq -e 'type == "object" and (.issues | type == "array")' >/dev/null 2>&1; then | |
| parsed=true | |
| issues=$(printf '%s' "$raw" | jq -c '[.issues[] | tostring]') | |
| reason=$(printf '%s' "$raw" | jq -r '.reason // ""') | |
| else | |
| reason='Pass 2 output was empty or invalid JSON.' | |
| fi | |
| filtered=$(jq -cn --argjson issues "$issues" --arg current "$CURRENT_ISSUE_NUMBER" '[ | |
| $issues[] | |
| | tostring | |
| | select(. != $current) | |
| ] | reduce .[] as $issue ([]; if index($issue) then . else . + [$issue] end) | .[:5]') | |
| has_matches=false | |
| if [ "$(jq 'length' <<< "$filtered")" -gt 0 ]; then | |
| has_matches=true | |
| fi | |
| echo "Pass 2 parsed: $parsed" | |
| echo "Pass 2 matches after filtering: $(jq 'length' <<< "$filtered")" | |
| echo "Pass 2 reason: $reason" | |
| { | |
| echo "issues_json=$filtered" | |
| echo "reason<<EOF" | |
| echo "$reason" | |
| echo "EOF" | |
| echo "has_matches=$has_matches" | |
| } >> "$GITHUB_OUTPUT" | |
| - id: select-final | |
| name: Select final duplicate set | |
| env: | |
| PASS1_ISSUES: ${{ steps.normalize-all.outputs.issues_json }} | |
| PASS1_REASON: ${{ steps.normalize-all.outputs.reason }} | |
| PASS2_ISSUES: ${{ steps.normalize-open.outputs.issues_json }} | |
| PASS2_REASON: ${{ steps.normalize-open.outputs.reason }} | |
| PASS1_HAS_MATCHES: ${{ steps.normalize-all.outputs.has_matches }} | |
| PASS2_HAS_MATCHES: ${{ steps.normalize-open.outputs.has_matches }} | |
| run: | | |
| set -eo pipefail | |
| selected_issues='[]' | |
| selected_reason='No plausible duplicates found.' | |
| selected_pass='none' | |
| if [ "$PASS1_HAS_MATCHES" = "true" ]; then | |
| selected_issues=${PASS1_ISSUES:-'[]'} | |
| selected_reason=${PASS1_REASON:-'Pass 1 found duplicates.'} | |
| selected_pass='all' | |
| fi | |
| if [ "$PASS2_HAS_MATCHES" = "true" ]; then | |
| selected_issues=${PASS2_ISSUES:-'[]'} | |
| selected_reason=${PASS2_REASON:-'Pass 2 found duplicates.'} | |
| selected_pass='open-fallback' | |
| fi | |
| final_json=$(jq -cn \ | |
| --argjson issues "$selected_issues" \ | |
| --arg reason "$selected_reason" \ | |
| --arg pass "$selected_pass" \ | |
| '{issues: $issues, reason: $reason, pass: $pass}') | |
| echo "Final pass used: $selected_pass" | |
| echo "Final duplicate count: $(jq '.issues | length' <<< "$final_json")" | |
| echo "Final reason: $(jq -r '.reason' <<< "$final_json")" | |
| { | |
| echo "codex_output<<EOF" | |
| echo "$final_json" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| comment-on-issue: | |
| name: Comment with potential duplicates | |
| needs: gather-duplicates | |
| if: ${{ needs.gather-duplicates.result != 'skipped' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - name: Comment on issue | |
| uses: actions/github-script@v8 | |
| env: | |
| CODEX_OUTPUT: ${{ needs.gather-duplicates.outputs.codex_output }} | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const raw = process.env.CODEX_OUTPUT ?? ''; | |
| let parsed; | |
| try { | |
| parsed = JSON.parse(raw); | |
| } catch (error) { | |
| core.info(`Codex output was not valid JSON. Raw output: ${raw}`); | |
| core.info(`Parse error: ${error.message}`); | |
| return; | |
| } | |
| const issues = Array.isArray(parsed?.issues) ? parsed.issues : []; | |
| const currentIssueNumber = String(context.payload.issue.number); | |
| const passUsed = typeof parsed?.pass === 'string' ? parsed.pass : 'unknown'; | |
| const reason = typeof parsed?.reason === 'string' ? parsed.reason : ''; | |
| console.log(`Current issue number: ${currentIssueNumber}`); | |
| console.log(`Pass used: ${passUsed}`); | |
| if (reason) { | |
| console.log(`Reason: ${reason}`); | |
| } | |
| console.log(issues); | |
| const filteredIssues = [...new Set(issues.map((value) => String(value)))].filter((value) => value !== currentIssueNumber).slice(0, 5); | |
| if (filteredIssues.length === 0) { | |
| core.info('Codex reported no potential duplicates.'); | |
| return; | |
| } | |
| const lines = [ | |
| 'Potential duplicates detected. Please review them and close your issue if it is a duplicate.', | |
| '', | |
| ...filteredIssues.map((value) => `- #${String(value)}`), | |
| '', | |
| '*Powered by [Codex Action](https://github.com/openai/codex-action)*']; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: lines.join("\n"), | |
| }); | |
| - name: Remove codex-deduplicate label | |
| if: ${{ always() && github.event.action == 'labeled' && github.event.label.name == 'codex-deduplicate' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| gh issue edit "${{ github.event.issue.number }}" --remove-label codex-deduplicate || true | |
| echo "Attempted to remove label: codex-deduplicate" |