Skip to content

Python: Surface oauth_consent_request in non-streaming Foundry responses #581

Python: Surface oauth_consent_request in non-streaming Foundry responses

Python: Surface oauth_consent_request in non-streaming Foundry responses #581

name: DevFlow PR Repair
on:
issue_comment:
types: [created]
permissions:
contents: read
env:
DEVFLOW_REPOSITORY: ${{ vars.DF_REPO }}
TARGET_REPO_PATH: ${{ github.workspace }}
DEVFLOW_PATH: ${{ github.workspace }}/devflow
jobs:
snapshot:
if: >-
github.run_attempt == 1 && github.event.issue.pull_request &&
(github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') &&
contains(github.event.comment.body, '/fix')
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
gateway: ${{ steps.freeze.outputs.gateway }}
gateway_sha256: ${{ steps.freeze.outputs.gateway_sha256 }}
steps:
- name: Freeze public PR revision and command before authorization
id: freeze
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const body = context.payload.comment.body;
const text = typeof body === 'string' ? body.trim() : '';
const command = text === '/fix-ci' ? '/fix-ci' :
/^\/fix\s+\S/.test(text) ? '/fix' : null;
if (!command || Buffer.byteLength(body) > 16000) return;
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: context.payload.issue.number });
const repo = `${context.repo.owner}/${context.repo.repo}`;
if (pr.state !== 'open' || !pr.head.repo || pr.base.repo.full_name !== repo || pr.base.repo.id !== context.payload.repository.id) {
throw new Error('PR is closed or repository identity changed');
}
const snapshot = {
schema_version: 1, repo, repo_id: context.payload.repository.id,
pr_number: context.payload.issue.number,
head_repo: pr.head.repo.full_name, head_repo_id: pr.head.repo.id,
head_ref: pr.head.ref, head_sha: pr.head.sha,
base_ref: pr.base.ref, base_sha: pr.base.sha, merge_sha: pr.merge_commit_sha ?? null,
requester: context.payload.comment.user.login, comment_id: context.payload.comment.id,
command, comment_body: body, comment_created_at: context.payload.comment.created_at,
automation_sha: context.sha, source_run_id: Number(context.runId),
};
const json = JSON.stringify(snapshot);
if (Buffer.byteLength(json) > 24000) throw new Error('Gateway snapshot exceeds size limit');
core.setOutput('gateway', Buffer.from(json).toString('base64'));
core.setOutput('gateway_sha256', require('node:crypto').createHash('sha256').update(json).digest('hex'));
authorize:
needs: snapshot
if: ${{ needs.snapshot.outputs.gateway_sha256 != '' }}
runs-on: ubuntu-latest
environment: github-app-auth
timeout-minutes: 15
permissions:
contents: read
actions: read
id-token: write
outputs:
snapshot: ${{ steps.intake.outputs.snapshot }}
snapshot_sha256: ${{ steps.intake.outputs.snapshot_sha256 }}
controller_sha: ${{ steps.intake.outputs.controller_sha }}
automation_sha: ${{ steps.intake.outputs.automation_sha }}
target_repo: ${{ steps.intake.outputs.target_repo }}
pr_number: ${{ steps.intake.outputs.pr_number }}
command: ${{ steps.intake.outputs.command }}
steps:
- name: Checkout trusted public authorization helpers
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
sparse-checkout: |
.github/actions/github-app-token
.github/scripts/check_team_membership.js
persist-credentials: false
- name: Get source-repository App token
id: auth
uses: ./.github/actions/github-app-token
with:
mode: app
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
contents-permission: read
issues-permission: write
pull-requests-permission: write
- name: Authorize the frozen command requester
id: team
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
GATEWAY: ${{ needs.snapshot.outputs.gateway }}
GATEWAY_SHA256: ${{ needs.snapshot.outputs.gateway_sha256 }}
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
with:
github-token: ${{ steps.auth.outputs.token }}
script: |
const json = Buffer.from(process.env.GATEWAY, 'base64').toString('utf8');
const digest = require('node:crypto').createHash('sha256').update(json).digest('hex');
if (digest !== process.env.GATEWAY_SHA256) throw new Error('Gateway snapshot digest mismatch');
const snapshot = JSON.parse(json);
if (snapshot.repo !== `${context.repo.owner}/${context.repo.repo}` ||
snapshot.comment_id !== context.payload.comment.id || snapshot.requester !== context.payload.comment.user.login ||
snapshot.comment_body !== context.payload.comment.body || snapshot.pr_number !== context.payload.issue.number ||
snapshot.automation_sha !== context.sha || snapshot.source_run_id !== Number(context.runId)) {
throw new Error('Gateway event identity mismatch');
}
const checkTeamMembership = require('./.github/scripts/check_team_membership.js');
const result = await checkTeamMembership({ github, context, core,
teamSlug: process.env.TEAM_NAME, issueNumber: snapshot.pr_number, username: snapshot.requester });
core.setOutput('permitted', result.isTeamMember && result.author === snapshot.requester ? 'true' : 'false');
- name: Checkout authorized DevFlow controller
if: ${{ steps.team.outputs.permitted == 'true' }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ env.DEVFLOW_REPOSITORY }}
ref: main
token: ${{ secrets.DEVFLOW_TOKEN }}
fetch-depth: 1
persist-credentials: false
path: devflow
- name: Pin the checked-out controller
if: ${{ steps.team.outputs.permitted == 'true' }}
id: controller
shell: bash
run: echo "sha=$(git -C "$DEVFLOW_PATH" rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- if: ${{ steps.team.outputs.permitted == 'true' }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
- if: ${{ steps.team.outputs.permitted == 'true' }}
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
version: '0.11.x'
enable-cache: false
- name: Install trusted controller dependencies
if: ${{ steps.team.outputs.permitted == 'true' }}
run: uv sync --frozen --directory "$DEVFLOW_PATH"
- name: Validate and bind the authorized request
if: ${{ steps.team.outputs.permitted == 'true' }}
id: intake
env:
GATEWAY: ${{ needs.snapshot.outputs.gateway }}
GATEWAY_SHA256: ${{ needs.snapshot.outputs.gateway_sha256 }}
DEVFLOW_SHA: ${{ steps.controller.outputs.sha }}
GH_TOKEN: ${{ steps.auth.outputs.token }}
GITHUB_TOKEN: ${{ github.token }}
DEVFLOW_PUBLISH_AUTH_SOURCE: ${{ steps.auth.outputs.source }}
DEVFLOW_PUBLISH_AUTHORIZATION: ${{ steps.auth.outputs.authorization }}
PR_REPAIR_TARGET_REPOSITORY: ${{ github.repository }}
DEVELOPER_TEAM: ${{ secrets.DEVELOPER_TEAM }}
shell: bash
run: |
python3 - <<'PYTHON'
import base64, hashlib, os, pathlib
raw = base64.b64decode(os.environ['GATEWAY'], validate=True)
if len(raw) > 98304 or hashlib.sha256(raw).hexdigest() != os.environ['GATEWAY_SHA256']:
raise SystemExit('Gateway integrity mismatch')
(pathlib.Path(os.environ['RUNNER_TEMP']) / 'repair-gateway.json').write_bytes(raw)
PYTHON
uv run --directory "$DEVFLOW_PATH" python scripts/dispatch_pr_repair.py \
--phase intake --gateway "$RUNNER_TEMP/repair-gateway.json" \
--gateway-sha256 "$GATEWAY_SHA256" --out-dir "$RUNNER_TEMP/repair-intake"
- name: Acknowledge accepted repair command
if: ${{ steps.intake.outputs.mode == 'repair' }}
continue-on-error: true
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.auth.outputs.token }}
script: |
await github.rest.reactions.createForIssueComment({
...context.repo,
comment_id: context.payload.comment.id,
content: 'eyes',
});
repair:
needs: authorize
if: ${{ needs.authorize.outputs.snapshot_sha256 != '' }}
runs-on: ubuntu-latest
environment: github-app-auth
timeout-minutes: 120
permissions:
contents: read
actions: read
checks: read
id-token: write
copilot-requests: write
outputs:
status: ${{ steps.run.outputs.status }}
candidate_sha256: ${{ steps.run.outputs.candidate_sha256 }}
patch_sha256: ${{ steps.run.outputs.patch_sha256 }}
notice_sha256: ${{ steps.run.outputs.notice_sha256 }}
artifact_id: ${{ steps.artifact.outputs.artifact-id }}
steps:
- name: Checkout frozen public automation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.authorize.outputs.automation_sha }}
persist-credentials: false
- name: Checkout pinned DevFlow controller
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ env.DEVFLOW_REPOSITORY }}
ref: ${{ needs.authorize.outputs.controller_sha }}
token: ${{ secrets.DEVFLOW_TOKEN }}
fetch-depth: 1
persist-credentials: false
path: devflow
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
version: '0.11.x'
enable-cache: false
- name: Install trusted controller dependencies
run: uv sync --frozen --directory "$DEVFLOW_PATH"
- name: Materialize the immutable authorized snapshot
env:
SNAPSHOT: ${{ needs.authorize.outputs.snapshot }}
SNAPSHOT_SHA256: ${{ needs.authorize.outputs.snapshot_sha256 }}
shell: bash
run: |
python3 - <<'PYTHON'
import base64, hashlib, os, pathlib
raw = base64.b64decode(os.environ['SNAPSHOT'], validate=True)
if len(raw) > 98304 or hashlib.sha256(raw).hexdigest() != os.environ['SNAPSHOT_SHA256']:
raise SystemExit('Snapshot integrity mismatch')
(pathlib.Path(os.environ['RUNNER_TEMP']) / 'repair-snapshot.json').write_bytes(raw)
PYTHON
- name: Get read-only source-repository App token
id: read-auth
uses: ./.github/actions/github-app-token
with:
mode: app
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
contents-permission: read
issues-permission: read
pull-requests-permission: read
- name: Generate and verify the bounded repair
id: run
env:
SNAPSHOT_SHA256: ${{ needs.authorize.outputs.snapshot_sha256 }}
DEVFLOW_SHA: ${{ needs.authorize.outputs.controller_sha }}
PR_REPAIR_TARGET_REPOSITORY: ${{ github.repository }}
GH_TOKEN: ${{ steps.read-auth.outputs.token }}
GITHUB_TOKEN: ${{ github.token }}
DEVFLOW_MODEL_TOKEN: ${{ github.token }}
DEVFLOW_PUBLISH_AUTH_SOURCE: ${{ steps.read-auth.outputs.source }}
DEVFLOW_PUBLISH_AUTHORIZATION: ${{ steps.read-auth.outputs.authorization }}
run: >-
uv run --directory "$DEVFLOW_PATH" python scripts/trigger_fix_ci.py --phase run
--snapshot "$RUNNER_TEMP/repair-snapshot.json"
--snapshot-sha256 "$SNAPSHOT_SHA256"
--out-dir "$RUNNER_TEMP/repair-output"
- name: Retain bounded candidate and notice artifacts
if: ${{ steps.run.outcome == 'success' }}
id: artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: repair-${{ github.run_id }}
path: |
${{ runner.temp }}/repair-output/candidate.json
${{ runner.temp }}/repair-output/candidate.diff
${{ runner.temp }}/repair-output/notice.json
if-no-files-found: warn
retention-days: 7
- name: Get issue-reporting token
if: ${{ always() }}
id: report-auth
uses: ./.github/actions/github-app-token
with:
mode: app
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
contents-permission: read
issues-permission: write
pull-requests-permission: write
- name: Report exact candidate and approval instructions
if: ${{ steps.run.outputs.status == 'ready' }}
env:
GH_TOKEN: ${{ steps.report-auth.outputs.token }}
DEVFLOW_PUBLISH_AUTH_SOURCE: ${{ steps.report-auth.outputs.source }}
CANDIDATE_SHA256: ${{ steps.run.outputs.candidate_sha256 }}
ARTIFACT_ID: ${{ steps.artifact.outputs.artifact-id }}
PR_REPAIR_TARGET_REPOSITORY: ${{ github.repository }}
run: >-
uv run --directory "$DEVFLOW_PATH" python scripts/report_pr_repair.py
--candidate "$RUNNER_TEMP/repair-output/candidate.json"
--candidate-sha256 "$CANDIDATE_SHA256"
--artifact-id "$ARTIFACT_ID"
--out-dir "$RUNNER_TEMP/report-output"
- name: Report terminal no-change result
if: ${{ steps.run.outputs.status == 'done' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
NOTICE_SHA256: ${{ steps.run.outputs.notice_sha256 }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
with:
github-token: ${{ steps.report-auth.outputs.token }}
script: |
const fs = require('node:fs');
const crypto = require('node:crypto');
const raw = fs.readFileSync(`${process.env.RUNNER_TEMP}/repair-output/notice.json`);
if (raw.length > 16000 || crypto.createHash('sha256').update(raw).digest('hex') !== process.env.NOTICE_SHA256) {
throw new Error('Notice integrity mismatch');
}
const notice = JSON.parse(raw);
if (typeof notice.message !== 'string' || !notice.message || notice.message.length > 4000) throw new Error('Invalid notice');
await github.rest.issues.createComment({ ...context.repo, issue_number: Number(process.env.PR_NUMBER), body: notice.message });
- name: Report bounded generation failure
if: ${{ failure() && steps.report-auth.outputs.token != '' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
with:
github-token: ${{ steps.report-auth.outputs.token }}
script: |
const run = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.issues.createComment({ ...context.repo, issue_number: Number(process.env.PR_NUMBER),
body: `The repair could not complete. Please inspect [the repair run](${run}) before retrying.` });
approve:
needs: [authorize, repair]
if: ${{ needs.repair.outputs.status == 'ready' && needs.authorize.outputs.command == '/fix' }}
runs-on: ubuntu-latest
environment: devflow-pr-repair-publish
timeout-minutes: 5
permissions: {}
steps:
- name: Bind approval to the exact candidate digest
env:
PATCH_SHA256: ${{ needs.repair.outputs.patch_sha256 }}
run: |
[[ "$PATCH_SHA256" =~ ^[0-9a-f]{64}$ ]]
echo "Approved patch-sha256:$PATCH_SHA256"
publish:
needs: [authorize, repair, approve]
if: >-
always() && !cancelled() && needs.repair.outputs.status == 'ready' &&
(needs.authorize.outputs.command == '/fix-ci' || needs.approve.result == 'success')
runs-on: ubuntu-latest
environment: github-app-auth
timeout-minutes: 25
permissions:
contents: read
actions: read
id-token: write
env:
SNAPSHOT_SHA256: ${{ needs.authorize.outputs.snapshot_sha256 }}
CANDIDATE_SHA256: ${{ needs.repair.outputs.candidate_sha256 }}
PR_REPAIR_TARGET_REPOSITORY: ${{ github.repository }}
PR_REPAIR_APPROVAL_ENVIRONMENT: ${{ needs.authorize.outputs.command == '/fix' && 'devflow-pr-repair-publish' || 'github-app-auth' }}
DEVELOPER_TEAM: ${{ secrets.DEVELOPER_TEAM }}
GITHUB_TOKEN: ${{ github.token }}
steps:
- name: Checkout frozen public automation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.authorize.outputs.automation_sha }}
persist-credentials: false
- name: Checkout pinned DevFlow controller
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ env.DEVFLOW_REPOSITORY }}
ref: ${{ needs.authorize.outputs.controller_sha }}
token: ${{ secrets.DEVFLOW_TOKEN }}
fetch-depth: 1
persist-credentials: false
path: devflow
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
version: '0.11.x'
enable-cache: false
- name: Install trusted controller dependencies
run: uv sync --frozen --directory "$DEVFLOW_PATH"
- name: Download immutable candidate from this run
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.repair.outputs.artifact_id }}
path: ${{ runner.temp }}/publication-input
merge-multiple: true
- name: Materialize the original authorized snapshot
env:
SNAPSHOT: ${{ needs.authorize.outputs.snapshot }}
run: |
python3 - <<'PYTHON'
import base64, hashlib, os, pathlib
raw = base64.b64decode(os.environ['SNAPSHOT'], validate=True)
if len(raw) > 98304 or hashlib.sha256(raw).hexdigest() != os.environ['SNAPSHOT_SHA256']:
raise SystemExit('Snapshot integrity mismatch')
(pathlib.Path(os.environ['RUNNER_TEMP']) / 'publication-input/snapshot.json').write_bytes(raw)
PYTHON
- name: Get read-only source-repository App token
id: read-auth
uses: ./.github/actions/github-app-token
with:
mode: app
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
contents-permission: read
issues-permission: read
pull-requests-permission: read
- name: Revalidate exact candidate and approval before write authority
env:
GH_TOKEN: ${{ steps.read-auth.outputs.token }}
DEVFLOW_PUBLISH_AUTH_SOURCE: ${{ steps.read-auth.outputs.source }}
DEVFLOW_PUBLISH_AUTHORIZATION: ${{ steps.read-auth.outputs.authorization }}
run: >-
uv run --directory "$DEVFLOW_PATH" python scripts/trigger_fix_ci.py --phase validate-publication
--candidate "$RUNNER_TEMP/publication-input/candidate.json"
--candidate-sha256 "$CANDIDATE_SHA256"
--snapshot "$RUNNER_TEMP/publication-input/snapshot.json"
--snapshot-sha256 "$SNAPSHOT_SHA256"
- name: Get source-repository write token
id: write-auth
uses: ./.github/actions/github-app-token
with:
mode: app
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
contents-permission: write
issues-permission: write
pull-requests-permission: write
- name: Publish only the revalidated candidate
env:
GH_TOKEN: ${{ steps.write-auth.outputs.token }}
DEVFLOW_PUBLISH_AUTH_SOURCE: ${{ steps.write-auth.outputs.source }}
DEVFLOW_PUBLISH_AUTHORIZATION: ${{ steps.write-auth.outputs.authorization }}
run: >-
uv run --directory "$DEVFLOW_PATH" python scripts/trigger_fix_ci.py --phase publish
--candidate "$RUNNER_TEMP/publication-input/candidate.json"
--candidate-sha256 "$CANDIDATE_SHA256"
--snapshot "$RUNNER_TEMP/publication-input/snapshot.json"
--snapshot-sha256 "$SNAPSHOT_SHA256"
- name: Report uncertain publication
if: ${{ failure() && steps.write-auth.outputs.token != '' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
with:
github-token: ${{ steps.write-auth.outputs.token }}
script: |
const run = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.issues.createComment({ ...context.repo, issue_number: Number(process.env.PR_NUMBER),
body: `I could not confirm completion of this repair publication. Please inspect [the repair run](${run}) and the PR branch before retrying.` });