Skip to content

fix(sdk): surface RemoteConversation.fork() title from ConversationInfo #8209

fix(sdk): surface RemoteConversation.fork() title from ConversationInfo

fix(sdk): surface RemoteConversation.fork() title from ConversationInfo #8209

---
name: REST API breakage checks
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
agent-server-rest-api:
name: REST API (OpenAPI)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v7
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: Install oasdiff
# Pin the version so install.sh skips its GitHub API "latest release"
# lookup, which intermittently hits the 60/hr unauthenticated rate
# limit on shared runners and fails the whole check. Bump as needed.
run: |
curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | version=1.19.1 sh -s -- -b /usr/local/bin
oasdiff --version
- name: Run agent server REST 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:
AGENT_SERVER_REST_API_BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before
}}
AGENT_SERVER_REST_TYPE_WIDENING_REPORT_PATH: rest-api-type-widening-report.json
run: |
uv run --with packaging python .github/scripts/check_agent_server_rest_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 REST 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: rest-api-type-widening-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 = {}
type_widenings_since_base = report.get(
'additive_response_property_type_widenings_since_base',
[],
)
print(f'## REST API breakage checks (OpenAPI) — {status}')
print()
print(f"**Result:** {status}")
if exit_code != 0:
print()
print('> ⚠️ Breaking REST API changes or policy violations detected.')
print()
if type_widenings_since_base:
print('### Additive response property type widenings accepted')
print()
print(
'These REST response fields now accept an additional type. '
'The old type remains valid, and this PR was auto-marked '
'with the `release-note-required` label:'
)
print()
for change in type_widenings_since_base:
print(
'- `{property_path}` adds `{added_types}` in response '
'`{response_status}`'.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: Post REST 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: rest-api-type-widening-report.json
with:
script: |
const fs = require('fs');
const marker = '<!-- agent-server-rest-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 typeWideningsSinceBase = [];
try {
const report = JSON.parse(fs.readFileSync(process.env.REPORT_PATH, 'utf8'));
typeWideningsSinceBase = report.additive_response_property_type_widenings_since_base || [];
} catch (_error) {
typeWideningsSinceBase = [];
}
let body = `${marker}\n## REST API breakage checks (OpenAPI) — ${status}\n\n**Result:** ${status}\n`;
if (exitCode !== 0) {
body += `\n> ⚠️ Breaking REST 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 (typeWideningsSinceBase.length > 0) {
body += '\n### Additive response property type widenings accepted\n\n';
body += 'These REST response fields now accept an additional type. The old type remains valid, and this PR was auto-marked with the `release-note-required` label:\n\n';
for (const change of typeWideningsSinceBase) {
body += `- \`${change.property_path}\` adds \`${change.added_types}\` in response \`${change.response_status}\`\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,
});
}
- name: Apply release-note-required label for REST type widenings
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: rest-api-type-widening-report.json
with:
script: |
const fs = require('fs');
let typeWideningsSinceBase = [];
try {
const report = JSON.parse(fs.readFileSync(process.env.REPORT_PATH, 'utf8'));
typeWideningsSinceBase = report.additive_response_property_type_widenings_since_base || [];
} catch (_error) {
typeWideningsSinceBase = [];
}
if (typeWideningsSinceBase.length === 0) {
return;
}
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 (!hasLabel) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number,
labels: [label],
});
}
- name: Generate REST API contract PR summary
if: ${{ always() && github.event_name == 'pull_request' }}
env:
BASE_REF: ${{ github.event.pull_request.base.sha }}
run: |
touch rest-api-contract-summary.md
uv run --with packaging python .github/scripts/generate_agent_server_rest_api_contract_summary.py \
--base-ref "$BASE_REF" \
--output rest-api-contract-summary.md || true
- name: Update REST API contract summary in PR description
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPOSITORY: ${{ github.repository }}
run: |
gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}" --jq .body > pr-body.md
python .github/scripts/update_pr_body_with_rest_api_summary.py \
--body-file pr-body.md \
--summary-file rest-api-contract-summary.md \
--output updated-pr-body.md
if cmp -s pr-body.md updated-pr-body.md; then
echo "REST API contract summary in PR description is already current."
else
jq -n --rawfile body updated-pr-body.md '{body: $body}' \
| gh api --method PATCH "repos/${REPOSITORY}/pulls/${PR_NUMBER}" --input - >/dev/null
echo "Updated REST API contract summary in PR description."
fi