Skip to content

Port ControllerDifferentialIK into newton.controllers module #1265

Port ControllerDifferentialIK into newton.controllers module

Port ControllerDifferentialIK into newton.controllers module #1265

Workflow file for this run

name: Public API Change Detection
permissions: {}
on:
pull_request_target:
branches:
- main
- "release-*"
concurrency:
group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
# Safe under pull_request_target only because the trusted base-branch
# detector parses the head statically; never execute head code here.
detect-api-changes:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
if: github.repository == 'newton-physics/newton'
steps:
- name: Harden Runner
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
github.com:443
release-assets.githubusercontent.com:443
- name: Checkout PR head
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
path: head
fetch-depth: 0
persist-credentials: false
- name: Checkout base branch history
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: "base/.python-version"
- name: Checkout PR merge base
id: merge_base
run: |
script_path="${RUNNER_TEMP}/detect_api_changes.py"
git -C base show "${{ github.event.pull_request.base.sha }}:scripts/ci/detect_api_changes.py" > "$script_path"
git -C base fetch --no-tags ../head HEAD:refs/remotes/pr/head
merge_base=$(git -C base merge-base HEAD refs/remotes/pr/head)
git -C base checkout --detach "$merge_base"
echo "script_path=$script_path" >> "$GITHUB_OUTPUT"
- name: Detect API changes
id: detect
env:
API_COMMENT_PATH: ${{ runner.temp }}/api-comment.md
run: |
output=$(python "${{ steps.merge_base.outputs.script_path }}" base head)
needs_review=$(printf '%s' "$output" | python -c "import sys, json; print(str(json.load(sys.stdin)['needs_review']).lower())")
echo "needs_review=$needs_review" >> "$GITHUB_OUTPUT"
printf '%s' "$output" | python -c "
import sys, json
data = json.load(sys.stdin)
print(data['comment'])
" > "$API_COMMENT_PATH"
- name: Sync API review
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
API_COMMENT_PATH: ${{ runner.temp }}/api-comment.md
NEEDS_REVIEW: ${{ steps.detect.outputs.needs_review }}
with:
script: |
const fs = require('fs');
const needsReview = process.env.NEEDS_REVIEW === 'true';
const repo = {
owner: context.repo.owner,
repo: context.repo.repo,
};
const issue = {
...repo,
issue_number: context.issue.number,
};
if (needsReview) {
await github.rest.issues.addLabels({
...issue,
labels: ['api-changes'],
});
} else {
try {
await github.rest.issues.removeLabel({
...issue,
name: 'api-changes',
});
} catch (error) {
// The current state is already correct when the label is absent.
if (error.status !== 404) throw error;
}
}
const marker = '<!-- newton-api-changes -->';
const comments = await github.paginate(github.rest.issues.listComments, {
...issue,
per_page: 100,
});
const existing = comments.find((comment) =>
comment.user?.login === 'github-actions[bot]' &&
comment.user?.type === 'Bot' &&
comment.body?.startsWith(marker)
);
if (!needsReview) {
if (existing) {
await github.rest.issues.deleteComment({
...repo,
comment_id: existing.id,
});
}
return;
}
const body = fs.readFileSync(process.env.API_COMMENT_PATH, 'utf8').trim();
if (!body) return;
const fullBody = `${marker}\n${body}`;
if (existing) {
await github.rest.issues.updateComment({
...repo,
comment_id: existing.id,
body: fullBody,
});
} else {
await github.rest.issues.createComment({
...issue,
body: fullBody,
});
}