Skip to content

feat: add Novita AI as an LLM provider #456

feat: add Novita AI as an LLM provider

feat: add Novita AI as an LLM provider #456

name: External Contributor PR
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- closed
workflow_run:
workflows:
- External Contributor PR Approval Handoff
types:
- completed
permissions:
actions: read
contents: write
pull-requests: write
issues: write
env:
ECPR_LIB: |
(() => {
const LABELS = [
{ name: 'external-contributor', color: '8b949e', description: 'Tracks PRs mirrored from external contributor forks.' },
{ name: 'external-contributor:awaiting-approval', color: 'd29922', description: 'Waiting for a stagehand team member to approve the latest external commit.' },
{ name: 'external-contributor:mirrored', color: '1f6feb', description: 'An internal mirrored PR currently exists for this external contributor PR.' },
{ name: 'external-contributor:stale', color: 'db6d28', description: 'The mirrored PR is stale and waiting for a fresh approval to refresh.' },
{ name: 'external-contributor:completed', color: '2da44e', description: 'The mirrored PR has been merged and the external contributor flow is complete.' },
];
const MANAGED_LABELS = new Set(LABELS.map((label) => label.name));
const MANAGED_COMMENT_AUTHOR = 'github-actions[bot]';
const CLAIM_RE = /<!-- external-contributor-pr:claim owned-pr=(\d+) source-sha=([0-9a-f]{40}) claimer=([A-Za-z0-9-]+) branch=([^ ]+) -->/;
const OWNED_RE = /<!-- external-contributor-pr:owned source-pr=(\d+) source-sha=([0-9a-f]{40}) claimer=([A-Za-z0-9-]+) -->/;
const NOTICE_MARKER = '<!-- external-contributor-pr:notice -->';
const NOTICE_LINES = [
'This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run.',
'Approving the latest commit mirrors it into an internal PR owned by the approver.',
'If new commits are pushed later, the internal PR stays open but is marked stale until someone approves the latest external commit and refreshes it.',
];
async function ensureLabels(github, context) {
for (const label of LABELS) {
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label.name });
} catch (error) {
if (error.status !== 404) throw error;
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
color: label.color,
description: label.description,
});
} catch (createError) {
if (createError.status !== 422) throw createError;
}
}
}
}
async function listComments(github, context, issueNumber) {
return github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100,
});
}
function isManagedComment(comment) {
return comment.user?.login === MANAGED_COMMENT_AUTHOR;
}
function defaultManagedBranch(prNumber) {
return `external-contributor-pr-${prNumber}`;
}
function sanitizeManagedBranch(prNumber, branch) {
const fallback = defaultManagedBranch(prNumber);
if (!branch) return fallback;
const allowed = new RegExp(`^external-contributor-pr-${prNumber}(?:-[A-Za-z0-9._-]+)?$`);
return allowed.test(branch) ? branch : fallback;
}
async function upsertComment(github, context, issueNumber, marker, lines) {
const comments = await listComments(github, context, issueNumber);
const body = [marker, ...lines].join('\n');
const existing = comments.find((comment) => isManagedComment(comment) && comment.body?.includes(marker));
if (!existing) {
await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body });
return;
}
if (existing.body !== body) {
await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body });
}
}
async function syncLabels(github, context, issueNumber, desiredLabels) {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
const existingNames = issue.labels.map((label) => typeof label === 'string' ? label : label.name).filter(Boolean);
const preserved = existingNames.filter((label) => !MANAGED_LABELS.has(label));
await github.rest.issues.setLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: [...preserved, ...desiredLabels],
});
}
async function findLatestClaim(github, context, issueNumber) {
const comments = await listComments(github, context, issueNumber);
return [...comments]
.reverse()
.map((comment) => {
if (!isManagedComment(comment)) return null;
const match = comment.body?.match(CLAIM_RE);
if (!match) return null;
const sourcePrNumber = issueNumber;
return {
ownedPrNumber: Number(match[1]),
sourceSha: match[2],
claimer: match[3],
branch: sanitizeManagedBranch(sourcePrNumber, match[4]),
};
})
.find(Boolean);
}
async function externalLifecycle({ github, context }) {
const pr = context.payload.pull_request;
await ensureLabels(github, context);
if (context.payload.action === 'opened' || context.payload.action === 'reopened') {
await upsertComment(github, context, pr.number, NOTICE_MARKER, NOTICE_LINES);
const latestClaim = await findLatestClaim(github, context, pr.number);
if (context.payload.action === 'reopened' && latestClaim && latestClaim.sourceSha === pr.head.sha) {
const { data: ownedPr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: latestClaim.ownedPrNumber,
});
if (ownedPr.state === 'open') {
await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:mirrored']);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `This external contributor PR is already mirrored to ${ownedPr.html_url}. Closing it again so discussion stays on the internal PR until fresh commits require another approval.`,
});
await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, state: 'closed' });
return;
}
}
await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:awaiting-approval']);
return;
}
const latestClaim = await findLatestClaim(github, context, pr.number);
if (!latestClaim || latestClaim.sourceSha === pr.head.sha) return;
const { data: ownedPr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: latestClaim.ownedPrNumber,
});
if (ownedPr.state !== 'open') return;
await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:awaiting-approval']);
await syncLabels(github, context, ownedPr.number, ['external-contributor', 'external-contributor:stale']);
await upsertComment(github, context, ownedPr.number, '<!-- external-contributor-pr:owned-status -->', [
`This mirrored PR is stale because the original external contributor PR #${pr.number} received new commits (\`${latestClaim.sourceSha}\` -> \`${pr.head.sha}\`).`,
`Original PR: ${pr.html_url}`,
'',
'Approve the latest external commit to refresh this same internal PR in place.',
]);
if (pr.state === 'closed') {
await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, state: 'open' });
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ownedPr.number,
body: `New commits landed on external contributor PR #${pr.number} (\`${latestClaim.sourceSha}\` -> \`${pr.head.sha}\`). This mirrored PR stays open but is now stale until the latest external commit is approved and copied over.`,
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `New commits were pushed to this external contributor PR (\`${latestClaim.sourceSha}\` -> \`${pr.head.sha}\`). The mirrored PR ${ownedPr.html_url} remains open but is marked stale. A stagehand team member with write access must approve the latest commit to refresh that internal PR.`,
});
}
async function prepareClaim({ github, context, core, artifactPath }) {
const fs = require('fs');
const handoff = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
core.setOutput('should-claim', 'false');
if (!handoff.shouldClaim || !handoff.prNumber || !handoff.reviewer || !handoff.approvedSha) return;
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: Number(handoff.prNumber),
});
if (pr.head.repo.full_name === context.payload.repository.full_name || pr.state !== 'open') return;
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: handoff.reviewer,
});
if (!new Set(['admin', 'maintain', 'write']).has(permission.permission)) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `@${handoff.reviewer} submitted an approving review, but only stagehand team members with write access can claim external contributor PRs. A maintainer with write access must approve the latest commit to proceed.`,
});
return;
}
if (pr.head.sha !== handoff.approvedSha) return;
const latestClaim = await findLatestClaim(github, context, pr.number);
const branch = sanitizeManagedBranch(pr.number, latestClaim?.branch);
const title = `[Claimed #${pr.number}] ${pr.title}`;
const body = [
`Mirrored from external contributor PR #${pr.number} after approval by @${handoff.reviewer}.`,
'',
`Original author: @${pr.user.login}`,
`Original PR: ${pr.html_url}`,
`Approved source head SHA: \`${pr.head.sha}\``,
'',
`@${pr.user.login}, please continue any follow-up discussion on this mirrored PR. When the external PR gets new commits, this same internal PR will be marked stale until the latest external commit is approved and refreshed here.`,
'',
'## Original description',
pr.body?.trim() || '_No description provided._',
'',
`<!-- external-contributor-pr:owned source-pr=${pr.number} source-sha=${pr.head.sha} claimer=${handoff.reviewer} -->`,
].join('\n');
const { data: ownedPrs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'all',
head: `${context.repo.owner}:${branch}`,
base: 'main',
per_page: 100,
});
core.setOutput('should-claim', 'true');
core.setOutput('claimer', handoff.reviewer);
core.setOutput('pr-number', String(pr.number));
core.setOutput('source-sha', pr.head.sha);
core.setOutput('previous-source-sha', latestClaim?.sourceSha || '');
core.setOutput('branch', branch);
core.setOutput('title', title);
core.setOutput('body', body);
core.setOutput('owned-pr-number', ownedPrs[0] ? String(ownedPrs[0].number) : '');
core.setOutput('owned-pr-merged', ownedPrs[0]?.merged_at ? 'true' : 'false');
}
async function finalizeClaim({ github, context, input }) {
await ensureLabels(github, context);
const {
prNumber,
sourceSha,
branch,
claimer,
title,
body,
existingNumber,
existingMerged,
refreshStatus,
refreshReason,
} = input;
if (refreshStatus !== 'updated') {
if (existingNumber) {
await syncLabels(github, context, Number(existingNumber), ['external-contributor', 'external-contributor:stale']);
await upsertComment(github, context, Number(existingNumber), '<!-- external-contributor-pr:owned-status -->', [
`This mirrored PR could not be refreshed automatically after approval by @${claimer}.`,
'',
`Refresh reason: \`${refreshReason || 'unknown'}\``,
'Resolve the branch manually, then keep using this same mirrored PR.',
]);
}
await syncLabels(github, context, prNumber, ['external-contributor', 'external-contributor:awaiting-approval']);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `The latest approval by @${claimer} could not refresh the mirrored PR automatically (${refreshReason || 'unknown reason'}). The external PR stays open, and the mirrored PR should be updated manually before work continues.`,
});
return;
}
let ownedPr;
if (existingNumber && !existingMerged) {
const { data } = await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: Number(existingNumber),
title,
body,
base: 'main',
state: 'open',
});
ownedPr = data;
} else {
const { data } = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
head: branch,
base: 'main',
});
ownedPr = data;
}
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ownedPr.number,
assignees: [claimer],
});
await syncLabels(github, context, prNumber, ['external-contributor', 'external-contributor:mirrored']);
await syncLabels(github, context, ownedPr.number, ['external-contributor', 'external-contributor:mirrored']);
await upsertComment(github, context, ownedPr.number, '<!-- external-contributor-pr:owned-status -->', [
`This mirrored PR tracks external contributor PR #${prNumber} at source SHA \`${sourceSha}\`, approved by @${claimer}.`,
`Original PR: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`,
'',
'When the external PR gets new commits, this same internal PR will be refreshed in place after the latest external commit is approved.',
]);
const marker = `<!-- external-contributor-pr:claim owned-pr=${ownedPr.number} source-sha=${sourceSha} claimer=${claimer} branch=${branch} -->`;
const comments = await listComments(github, context, prNumber);
if (!comments.some((comment) => comment.body?.includes(marker))) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: [marker, `This PR was approved by @${claimer} and mirrored to ${ownedPr.html_url}. All further discussion should happen on that PR.`].join('\n'),
});
}
const { data: externalPr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
if (externalPr.state !== 'closed') {
await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber, state: 'closed' });
}
}
async function syncOwnedPr({ github, context }) {
const pr = context.payload.pull_request;
const match = pr.body?.match(OWNED_RE);
if (!match) return;
const sourcePrNumber = Number(match[1]);
const sourceSha = match[2];
await ensureLabels(github, context);
const { data: externalPr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: sourcePrNumber,
});
if (context.payload.action === 'reopened') {
await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:mirrored']);
await syncLabels(github, context, sourcePrNumber, ['external-contributor', 'external-contributor:mirrored']);
if (externalPr.state !== 'closed') {
await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: sourcePrNumber, state: 'closed' });
}
return;
}
if (pr.merged) {
await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:completed']);
await syncLabels(github, context, sourcePrNumber, ['external-contributor', 'external-contributor:completed']);
await upsertComment(github, context, pr.number, '<!-- external-contributor-pr:owned-status -->', [
`This mirrored PR has been merged into \`main\`. The original external PR ${externalPr.html_url} is now completed.`,
]);
await upsertComment(github, context, sourcePrNumber, `<!-- external-contributor-pr:completed owned-pr=${pr.number} -->`, [
`The mirrored PR ${pr.html_url} has been merged into \`main\`. This original external contributor PR will stay closed as completed.`,
]);
return;
}
await syncLabels(github, context, pr.number, ['external-contributor', 'external-contributor:stale']);
await syncLabels(github, context, sourcePrNumber, ['external-contributor', 'external-contributor:awaiting-approval']);
if (externalPr.head.sha !== sourceSha) {
await upsertComment(github, context, pr.number, '<!-- external-contributor-pr:owned-status -->', [
`This mirrored PR is stale because the original external PR ${externalPr.html_url} now points at a different source SHA.`,
'Approve the latest external commit to refresh this same internal PR.',
]);
return;
}
if (externalPr.state === 'closed') {
await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: sourcePrNumber, state: 'open' });
}
await upsertComment(github, context, sourcePrNumber, `<!-- external-contributor-pr:owned-closed owned-pr=${pr.number} -->`, [
`The mirrored PR ${pr.html_url} was closed without merge. This original PR has been reopened and is awaiting a fresh approving review from a stagehand team member with write access.`,
]);
await upsertComment(github, context, pr.number, '<!-- external-contributor-pr:owned-status -->', [
`This mirrored PR was closed without merge. The original external PR ${externalPr.html_url} has been reopened and relabeled as awaiting approval.`,
]);
}
return { externalLifecycle, prepareClaim, finalizeClaim, syncOwnedPr };
})()
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.id }}
cancel-in-progress: false
jobs:
manage-external-pr:
if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- name: Sync external PR lifecycle
if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const lib = eval(process.env.ECPR_LIB);
await lib.externalLifecycle({ github, context });
claim-approved-pr:
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Download approval handoff artifact
uses: actions/download-artifact@v4
with:
name: approved-review
path: approval-handoff
github-token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
run-id: ${{ github.event.workflow_run.id }}
- name: Prepare approved claim
id: prepare-claim
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const lib = eval(process.env.ECPR_LIB);
await lib.prepareClaim({ github, context, core, artifactPath: 'approval-handoff/approval-handoff.json' });
- name: Checkout repository for branch operations
if: steps.prepare-claim.outputs.should-claim == 'true'
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
- name: Refresh internal branch
if: steps.prepare-claim.outputs.should-claim == 'true'
id: refresh-branch
continue-on-error: true
env:
INTERNAL_BRANCH: ${{ steps.prepare-claim.outputs.branch }}
PR_NUMBER: ${{ steps.prepare-claim.outputs.pr-number }}
PREVIOUS_SOURCE_SHA: ${{ steps.prepare-claim.outputs.previous-source-sha }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -uo pipefail
refresh_status="conflict"
refresh_reason="unknown"
write_outputs() {
echo "refresh-status=${refresh_status}" >> "$GITHUB_OUTPUT"
if [ -n "${refresh_reason}" ]; then
echo "reason=${refresh_reason}" >> "$GITHUB_OUTPUT"
fi
}
trap write_outputs EXIT
if ! git config user.name "github-actions[bot]"; then
refresh_reason="git-config-failed"
exit 0
fi
if ! git config user.email "41898282+github-actions[bot]@users.noreply.github.com"; then
refresh_reason="git-config-failed"
exit 0
fi
if ! git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"; then
refresh_reason="remote-auth-failed"
exit 0
fi
if ! git fetch origin "pull/${PR_NUMBER}/head:refs/remotes/origin/external-pr-head-${PR_NUMBER}"; then
refresh_reason="fetch-external-failed"
exit 0
fi
external_ref="refs/remotes/origin/external-pr-head-${PR_NUMBER}"
branch_exists=false
if git ls-remote --exit-code --heads origin "${INTERNAL_BRANCH}" >/dev/null 2>&1; then
branch_exists=true
if ! git fetch origin "${INTERNAL_BRANCH}:refs/remotes/origin/${INTERNAL_BRANCH}"; then
refresh_reason="fetch-internal-failed"
exit 0
fi
fi
if [ "${branch_exists}" = false ]; then
if ! git checkout -B "${INTERNAL_BRANCH}" "${external_ref}"; then
refresh_reason="checkout-failed"
exit 0
fi
if ! git push --force-with-lease origin "HEAD:refs/heads/${INTERNAL_BRANCH}"; then
refresh_reason="push-failed"
exit 0
fi
refresh_status="updated"
refresh_reason=""
exit 0
fi
if ! git checkout -B "${INTERNAL_BRANCH}" "refs/remotes/origin/${INTERNAL_BRANCH}"; then
refresh_reason="checkout-failed"
exit 0
fi
if [ -z "${PREVIOUS_SOURCE_SHA}" ]; then
refresh_reason="missing-previous-source"
exit 0
fi
if git rebase --onto "${external_ref}" "${PREVIOUS_SOURCE_SHA}" "${INTERNAL_BRANCH}"; then
if ! git push --force-with-lease origin "HEAD:refs/heads/${INTERNAL_BRANCH}"; then
refresh_reason="push-failed"
exit 0
fi
refresh_status="updated"
refresh_reason=""
exit 0
fi
git rebase --abort || true
refresh_reason="rebase-conflict"
- name: Finalize approved claim
if: always() && steps.prepare-claim.outputs.should-claim == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const lib = eval(process.env.ECPR_LIB);
await lib.finalizeClaim({
github,
context,
input: {
prNumber: Number('${{ steps.prepare-claim.outputs.pr-number }}'),
sourceSha: ${{ toJson(steps.prepare-claim.outputs.source-sha) }},
branch: ${{ toJson(steps.prepare-claim.outputs.branch) }},
claimer: ${{ toJson(steps.prepare-claim.outputs.claimer) }},
title: ${{ toJson(steps.prepare-claim.outputs.title) }},
body: ${{ toJson(steps.prepare-claim.outputs.body) }},
existingNumber: ${{ toJson(steps.prepare-claim.outputs.owned-pr-number) }},
existingMerged: '${{ steps.prepare-claim.outputs.owned-pr-merged }}' === 'true',
refreshStatus: ${{ toJson(steps.refresh-branch.outputs.refresh-status) }},
refreshReason: ${{ toJson(steps.refresh-branch.outputs.reason) }},
},
});
sync-owned-pr:
if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name == github.repository && (github.event.action == 'closed' || github.event.action == 'reopened')
runs-on: ubuntu-latest
steps:
- name: Sync mirrored PR lifecycle
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const lib = eval(process.env.ECPR_LIB);
await lib.syncOwnedPr({ github, context });