fix(openai): openai sdk validation error not surfaced in with_structured_output
#1814
Workflow file for this run
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
| # Require external PRs to link to an approved issue or discussion using | |
| # GitHub auto-close keywords (Fixes #NNN, Closes #NNN, Resolves #NNN), | |
| # AND require that the PR author is assigned to the linked issue. | |
| # | |
| # - Reacts to the "external" label applied by tag-external-contributions.yml, | |
| # avoiding a duplicate org membership check. | |
| # - Also re-checks on PR edits/reopens for PRs that already have the label. | |
| # - Bypasses the check for PRs with the "trusted-contributor" label, and | |
| # automatically reopens/cleans up PRs that receive it after enforcement. | |
| # - Validates the PR author is an assignee on at least one linked issue. | |
| # - Adds a "missing-issue-link" label on failure; removes it on pass. | |
| # - Automatically reopens PRs that were closed by this workflow once the | |
| # check passes (e.g. author edits the body to add a valid issue link). | |
| # - Posts a comment explaining the requirement on failure. | |
| # - Deduplicates comments via an HTML marker so re-runs don't spam. | |
| # | |
| # Dependency: tag-external-contributions.yml must run first to apply the | |
| # "external" label on new PRs. Both workflows trigger on pull_request_target | |
| # opened events; this workflow additionally listens for the "labeled" event | |
| # to chain off the external classification. | |
| name: Require Issue Link | |
| on: | |
| pull_request_target: | |
| types: [opened, edited, reopened, labeled] | |
| permissions: | |
| contents: read | |
| jobs: | |
| check-issue-link: | |
| # Run when the "external" label is added, or on edit/reopen if already labeled. | |
| # Skip entirely when the PR already carries "trusted-contributor". | |
| if: >- | |
| !contains(github.event.pull_request.labels.*.name, 'trusted-contributor') && | |
| ( | |
| (github.event.action == 'labeled' && github.event.label.name == 'external') || | |
| (github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'external')) | |
| ) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Check for issue link and assignee | |
| id: check-link | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const prNumber = context.payload.pull_request.number; | |
| // Fetch live labels to handle the race where "external" fires | |
| // before "trusted-contributor" appears in the event payload. | |
| const { data: liveLabels } = await github.rest.issues.listLabelsOnIssue({ | |
| owner, repo, issue_number: prNumber, | |
| }); | |
| if (liveLabels.some(l => l.name === 'trusted-contributor')) { | |
| console.log('PR has trusted-contributor label — bypassing issue link check'); | |
| core.setOutput('has-link', 'true'); | |
| core.setOutput('is-assigned', 'true'); | |
| return; | |
| } | |
| const body = context.payload.pull_request.body || ''; | |
| const pattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)/gi; | |
| const matches = [...body.matchAll(pattern)]; | |
| if (matches.length === 0) { | |
| console.log('No issue link found in PR body'); | |
| core.setOutput('has-link', 'false'); | |
| core.setOutput('is-assigned', 'false'); | |
| return; | |
| } | |
| const issues = matches.map(m => `#${m[1]}`).join(', '); | |
| console.log(`Found issue link(s): ${issues}`); | |
| core.setOutput('has-link', 'true'); | |
| // Check whether the PR author is assigned to at least one linked issue | |
| const prAuthor = context.payload.pull_request.user.login; | |
| const issueNumbers = [...new Set(matches.map(m => parseInt(m[1], 10)))]; | |
| let assignedToAny = false; | |
| for (const num of issueNumbers) { | |
| try { | |
| const { data: issue } = await github.rest.issues.get({ | |
| owner, repo, issue_number: num, | |
| }); | |
| const assignees = issue.assignees.map(a => a.login.toLowerCase()); | |
| if (assignees.includes(prAuthor.toLowerCase())) { | |
| console.log(`PR author "${prAuthor}" is assigned to #${num}`); | |
| assignedToAny = true; | |
| break; | |
| } else { | |
| console.log(`PR author "${prAuthor}" is NOT assigned to #${num} (assignees: ${assignees.join(', ') || 'none'})`); | |
| } | |
| } catch (error) { | |
| console.log(`Could not fetch issue #${num}: ${error.message}`); | |
| } | |
| } | |
| core.setOutput('is-assigned', assignedToAny ? 'true' : 'false'); | |
| - name: Add missing-issue-link label | |
| if: steps.check-link.outputs.has-link != 'true' || steps.check-link.outputs.is-assigned != 'true' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const prNumber = context.payload.pull_request.number; | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number: prNumber, labels: ['missing-issue-link'], | |
| }); | |
| - name: Remove missing-issue-link label and reopen PR | |
| if: steps.check-link.outputs.has-link == 'true' && steps.check-link.outputs.is-assigned == 'true' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const prNumber = context.payload.pull_request.number; | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner, repo, issue_number: prNumber, name: 'missing-issue-link', | |
| }); | |
| } catch (error) { | |
| if (error.status !== 404) throw error; | |
| } | |
| // Reopen PR only if it was previously closed by this workflow | |
| const labels = context.payload.pull_request.labels.map(l => l.name); | |
| if (context.payload.pull_request.state === 'closed' && labels.includes('missing-issue-link')) { | |
| await github.rest.pulls.update({ | |
| owner, | |
| repo, | |
| pull_number: prNumber, | |
| state: 'open', | |
| }); | |
| console.log(`Reopened PR #${prNumber}`); | |
| } | |
| - name: Post comment, close PR, and fail | |
| if: steps.check-link.outputs.has-link != 'true' || steps.check-link.outputs.is-assigned != 'true' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const prNumber = context.payload.pull_request.number; | |
| const hasLink = '${{ steps.check-link.outputs.has-link }}' === 'true'; | |
| const isAssigned = '${{ steps.check-link.outputs.is-assigned }}' === 'true'; | |
| const marker = '<!-- require-issue-link -->'; | |
| let lines; | |
| if (!hasLink) { | |
| lines = [ | |
| marker, | |
| '**This PR has been automatically closed** because it does not link to an approved issue.', | |
| '', | |
| 'All external contributions must reference an approved issue or discussion. Please:', | |
| '1. Find or [open an issue](https://github.com/' + owner + '/' + repo + '/issues/new/choose) describing the change', | |
| '2. Wait for a maintainer to approve and assign you', | |
| '3. Add `Fixes #<issue_number>`, `Closes #<issue_number>`, or `Resolves #<issue_number>` to your PR description and the PR will be reopened automatically', | |
| ]; | |
| } else { | |
| lines = [ | |
| marker, | |
| '**This PR has been automatically closed** because you are not assigned to the linked issue.', | |
| '', | |
| 'External contributors must be assigned to an issue before opening a PR for it. Please:', | |
| '1. Comment on the linked issue to request assignment from a maintainer', | |
| '2. Once assigned, edit your PR description and the PR will be reopened automatically', | |
| ]; | |
| } | |
| const body = lines.join('\n'); | |
| // Deduplicate: check for existing comment with the marker | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { owner, repo, issue_number: prNumber, per_page: 100 }, | |
| ); | |
| const existing = comments.find(c => c.body && c.body.includes(marker)); | |
| if (!existing) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| body, | |
| }); | |
| console.log('Posted requirement comment'); | |
| } else if (existing.body !== body) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| console.log('Updated existing comment with new message'); | |
| } else { | |
| console.log('Comment already exists — skipping'); | |
| } | |
| // Close the PR | |
| if (context.payload.pull_request.state === 'open') { | |
| await github.rest.pulls.update({ | |
| owner, | |
| repo, | |
| pull_number: prNumber, | |
| state: 'closed', | |
| }); | |
| console.log(`Closed PR #${prNumber}`); | |
| } | |
| const reason = !hasLink | |
| ? 'PR must reference an issue using auto-close keywords (e.g., "Fixes #123").' | |
| : 'PR author must be assigned to the linked issue.'; | |
| core.setFailed(reason); | |
| # When a trusted-contributor label is added to a PR that was previously | |
| # closed by check-issue-link, clean up and reopen it. | |
| bypass-trusted-contributor: | |
| if: github.event.action == 'labeled' && github.event.label.name == 'trusted-contributor' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Remove missing-issue-link label and reopen PR | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const prNumber = context.payload.pull_request.number; | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner, repo, issue_number: prNumber, name: 'missing-issue-link', | |
| }); | |
| console.log('Removed missing-issue-link label'); | |
| } catch (error) { | |
| if (error.status !== 404) throw error; | |
| } | |
| if (context.payload.pull_request.state === 'closed') { | |
| await github.rest.pulls.update({ | |
| owner, repo, pull_number: prNumber, state: 'open', | |
| }); | |
| console.log(`Reopened PR #${prNumber}`); | |
| } |