feat(sdk): add ask_oracle tool #5214
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: Python API breakage checks | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| jobs: | |
| sdk-api: | |
| name: Python API | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| with: | |
| enable-cache: true | |
| - name: Install workspace deps (dev) | |
| run: uv sync --frozen --group dev | |
| - name: Run Python API breakage check | |
| id: api_breakage | |
| # Let this step fail so CI is visibly red on breakage. | |
| # Later reporting steps still run because they use if: always(). | |
| env: | |
| ACP_VERSION_CHECK_BASE_REF: ${{ github.event_name == 'pull_request' && github.base_ref || github.event.before }} | |
| ACP_VERSION_CHECK_SKIP: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.body || '', 'skip-acp-check') | |
| }} | |
| SDK_API_BREAKAGE_REPORT_PATH: sdk-api-breakage-report.json | |
| run: | | |
| uv run python .github/scripts/check_sdk_api_breakage.py 2>&1 | tee api-breakage.log | |
| exit_code=${PIPESTATUS[0]} | |
| echo "exit_code=${exit_code}" >> "$GITHUB_OUTPUT" | |
| exit "${exit_code}" | |
| - name: Write API breakage summary | |
| if: ${{ always() }} | |
| env: | |
| EXIT_CODE: ${{ steps.api_breakage.outputs.exit_code }} | |
| IS_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} | |
| LOG_PATH: api-breakage.log | |
| REPORT_PATH: sdk-api-breakage-report.json | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| python3 <<'PY' >> "$GITHUB_STEP_SUMMARY" | |
| import json | |
| import os | |
| from pathlib import Path | |
| exit_code = int(os.environ.get('EXIT_CODE', '0') or '0') | |
| is_fork = os.environ.get('IS_FORK', 'false') == 'true' | |
| run_url = os.environ['RUN_URL'] | |
| status = '✅ **PASSED**' if exit_code == 0 else '❌ **FAILED**' | |
| try: | |
| report = json.loads(Path(os.environ['REPORT_PATH']).read_text()) | |
| except Exception: | |
| report = {} | |
| default_changes = report.get('field_default_changes', []) | |
| default_changes_since_base = report.get( | |
| 'field_default_changes_since_base', | |
| default_changes, | |
| ) | |
| print(f'## Python API breakage checks — {status}') | |
| print() | |
| print(f"**Result:** {status}") | |
| if exit_code != 0: | |
| print() | |
| print('> ⚠️ Breaking API changes or policy violations detected.') | |
| print() | |
| if default_changes_since_base: | |
| print('### Behavioral default changes detected') | |
| print() | |
| print( | |
| 'These public `Field(default=...)` changes were introduced ' | |
| 'by this changeset and were auto-marked with the ' | |
| '`release-note-required` label:' | |
| ) | |
| print() | |
| for change in default_changes_since_base: | |
| print( | |
| '- `{object_path}`: `{old_default}` → `{new_default}`'.format( | |
| **change, | |
| ) | |
| ) | |
| print() | |
| elif default_changes: | |
| print('### Behavioral default changes detected') | |
| print() | |
| print( | |
| 'These public `Field(default=...)` changes differ from the ' | |
| 'latest released baseline, but they were already present ' | |
| 'before this changeset, so the workflow did not add the ' | |
| '`release-note-required` label on this run:' | |
| ) | |
| print() | |
| for change in default_changes: | |
| print( | |
| '- `{object_path}`: `{old_default}` → `{new_default}`'.format( | |
| **change, | |
| ) | |
| ) | |
| print() | |
| if is_fork: | |
| print( | |
| '_Fork PR detected: sticky PR comment was skipped because ' | |
| 'the GitHub token is read-only for `pull_request` workflows ' | |
| 'from forks._' | |
| ) | |
| print() | |
| if exit_code != 0: | |
| try: | |
| log = Path(os.environ['LOG_PATH']).read_text() | |
| except Exception as exc: | |
| log = f'Unable to read log file: {exc}' | |
| excerpt = log[:1000].replace('```', '``\\`') | |
| print('<details><summary>Log excerpt (first 1000 characters)</summary>') | |
| print() | |
| print('```text') | |
| print(excerpt) | |
| print('```') | |
| print() | |
| print('</details>') | |
| print() | |
| print(f'[Action log]({run_url})') | |
| PY | |
| - name: Sync release-note-required label | |
| if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} | |
| uses: actions/github-script@v9 | |
| env: | |
| REPORT_PATH: sdk-api-breakage-report.json | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let defaultChangesSinceBase = []; | |
| try { | |
| const report = JSON.parse(fs.readFileSync(process.env.REPORT_PATH, 'utf8')); | |
| defaultChangesSinceBase = report.field_default_changes_since_base || report.field_default_changes || []; | |
| } catch (_error) { | |
| defaultChangesSinceBase = []; | |
| } | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const label = 'release-note-required'; | |
| const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ | |
| owner, | |
| repo, | |
| issue_number, | |
| per_page: 100, | |
| }); | |
| const hasLabel = currentLabels.some((item) => item.name === label); | |
| if (defaultChangesSinceBase.length > 0 && !hasLabel) { | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number, | |
| labels: [label], | |
| }); | |
| } | |
| if (defaultChangesSinceBase.length === 0 && hasLabel) { | |
| await github.rest.issues.removeLabel({ | |
| owner, | |
| repo, | |
| issue_number, | |
| name: label, | |
| }); | |
| } | |
| - name: Post API breakage report to PR | |
| if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} | |
| uses: actions/github-script@v9 | |
| env: | |
| EXIT_CODE: ${{ steps.api_breakage.outputs.exit_code }} | |
| LOG_PATH: api-breakage.log | |
| REPORT_PATH: sdk-api-breakage-report.json | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- api-breakage-report -->'; | |
| const exitCode = Number(process.env.EXIT_CODE || '0'); | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const status = exitCode === 0 ? '✅ **PASSED**' : '❌ **FAILED**'; | |
| let defaultChanges = []; | |
| let defaultChangesSinceBase = []; | |
| try { | |
| const report = JSON.parse(fs.readFileSync(process.env.REPORT_PATH, 'utf8')); | |
| defaultChanges = report.field_default_changes || []; | |
| defaultChangesSinceBase = report.field_default_changes_since_base || defaultChanges; | |
| } catch (_error) { | |
| defaultChanges = []; | |
| defaultChangesSinceBase = []; | |
| } | |
| let body = `${marker}\n## Python API breakage checks — ${status}\n\n**Result:** ${status}\n`; | |
| if (exitCode !== 0) { | |
| body += `\n> ⚠️ Breaking API changes or policy violations detected.\n`; | |
| let log = ''; | |
| try { | |
| log = fs.readFileSync(process.env.LOG_PATH, 'utf8'); | |
| } catch (e) { | |
| log = `Unable to read log file: ${e}`; | |
| } | |
| const excerpt = log.slice(0, 1000).replace(/```/g, '``\\`'); | |
| body += `\n<details><summary>Log excerpt (first 1000 characters)</summary>\n\n\`\`\`text\n${excerpt}\n\`\`\`\n\n</details>\n`; | |
| } | |
| if (defaultChangesSinceBase.length > 0) { | |
| body += '\n### Behavioral default changes detected\n\n'; | |
| body += 'These public `Field(default=...)` changes were introduced by this PR and were auto-marked with the `release-note-required` label:\n\n'; | |
| for (const change of defaultChangesSinceBase) { | |
| body += `- \`${change.object_path}\`: \`${change.old_default}\` → \`${change.new_default}\`\n`; | |
| } | |
| } else if (defaultChanges.length > 0) { | |
| body += '\n### Behavioral default changes detected\n\n'; | |
| body += 'These public `Field(default=...)` changes differ from the latest released baseline, but they were already present on the base branch, so this PR was not auto-marked with the `release-note-required` label:\n\n'; | |
| for (const change of defaultChanges) { | |
| body += `- \`${change.object_path}\`: \`${change.old_default}\` → \`${change.new_default}\`\n`; | |
| } | |
| } | |
| body += `\n[Action log](${runUrl})\n`; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner, | |
| repo, | |
| issue_number, | |
| per_page: 100, | |
| }); | |
| const existing = comments.find((c) => c.body && c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number, | |
| body, | |
| }); | |
| } |