ci: use CTO_ADMIN_TOKEN for repo variables + restore formal APPROVE/REQUEST_CHANGES
Default GITHUB_TOKEN cannot mutate Repository Variables (always 403). Use a PAT-backed secret CTO_ADMIN_TOKEN for the budget counter steps. Also restores formal APPROVE/REQUEST_CHANGES verdicts now that repo setting can_approve_pull_request_reviews is enabled. Agent-Session: cowork-reviewer-ab-20260414
This commit is contained in:
39
.github/workflows/cto-review.yml
vendored
39
.github/workflows/cto-review.yml
vendored
@@ -21,7 +21,6 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
actions: write # needed to update repo variables for the spend counter
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: cto-review-pr-${{ github.event.pull_request.number }}
|
group: cto-review-pr-${{ github.event.pull_request.number }}
|
||||||
@@ -37,10 +36,22 @@ jobs:
|
|||||||
timeout-minutes: 12
|
timeout-minutes: 12
|
||||||
steps:
|
steps:
|
||||||
- name: Monthly spend cap — read counter, bail if exceeded
|
- name: Monthly spend cap — read counter, bail if exceeded
|
||||||
|
# Counter lives in Repository Variables (CTO_REVIEW_MONTH,
|
||||||
|
# CTO_REVIEW_SPEND_CENTS, CTO_REVIEW_CAP_CENTS). Auth via CTO_ADMIN_TOKEN
|
||||||
|
# (a PAT) because default GITHUB_TOKEN cannot mutate Repo Variables.
|
||||||
id: budget
|
id: budget
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
|
env:
|
||||||
|
ADMIN_TOKEN: ${{ secrets.CTO_ADMIN_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
github-token: ${{ secrets.CTO_ADMIN_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
if (!process.env.ADMIN_TOKEN) {
|
||||||
|
core.warning('CTO_ADMIN_TOKEN not set — skipping budget cap (reviewer will still run but spend is untracked).');
|
||||||
|
core.setOutput('skip', 'false');
|
||||||
|
core.setOutput('tracking', 'false');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const month = new Date().toISOString().slice(0,7);
|
const month = new Date().toISOString().slice(0,7);
|
||||||
const get = async (name) => {
|
const get = async (name) => {
|
||||||
try {
|
try {
|
||||||
@@ -76,22 +87,22 @@ jobs:
|
|||||||
core.setOutput('month', month);
|
core.setOutput('month', month);
|
||||||
core.setOutput('spend_cents', String(spendCents));
|
core.setOutput('spend_cents', String(spendCents));
|
||||||
core.setOutput('cap_cents', String(capCents));
|
core.setOutput('cap_cents', String(capCents));
|
||||||
|
core.setOutput('tracking', 'true');
|
||||||
|
|
||||||
if (spendCents >= capCents) {
|
if (spendCents >= capCents) {
|
||||||
const body = [
|
const body = [
|
||||||
`## CTO Review — Skipped (monthly cap hit)`,
|
`## CTO Review — Skipped (monthly cap hit)`,
|
||||||
``,
|
``,
|
||||||
`The reviewer's monthly spend has reached **\$${(spendCents/100).toFixed(2)}** of its **\$${(capCents/100).toFixed(2)}** cap for ${month}.`,
|
`The reviewer's monthly spend has reached **\$${(spendCents/100).toFixed(2)}** of its **\$${(capCents/100).toFixed(2)}** cap for ${month}.`,
|
||||||
``,
|
``,
|
||||||
`To unblock: wait for month rollover, raise \`CTO_REVIEW_CAP_CENTS\` in repo settings, or reset \`CTO_REVIEW_SPEND_CENTS\` to 0.`,
|
`To unblock: wait for the cap to reset on the 1st of next month; raise \`CTO_REVIEW_CAP_CENTS\` in Settings → Variables; or reset \`CTO_REVIEW_SPEND_CENTS\` to 0.`,
|
||||||
``,
|
|
||||||
`_Auto-paused by \`.github/workflows/cto-review.yml\`._`,
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
await github.rest.pulls.createReview({
|
await github.rest.pulls.createReview({
|
||||||
owner: context.repo.owner, repo: context.repo.repo,
|
owner: context.repo.owner, repo: context.repo.repo,
|
||||||
pull_number: context.issue.number, event: 'COMMENT', body,
|
pull_number: context.issue.number, event: 'COMMENT', body,
|
||||||
});
|
});
|
||||||
core.setOutput('skip', 'true');
|
core.setOutput('skip', 'true');
|
||||||
core.notice(`Reviewer paused: $${(spendCents/100).toFixed(2)} >= cap $${(capCents/100).toFixed(2)}`);
|
core.notice(`Paused: $${(spendCents/100).toFixed(2)} >= cap $${(capCents/100).toFixed(2)}`);
|
||||||
} else {
|
} else {
|
||||||
core.setOutput('skip', 'false');
|
core.setOutput('skip', 'false');
|
||||||
}
|
}
|
||||||
@@ -358,20 +369,27 @@ jobs:
|
|||||||
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
|
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
|
||||||
f.write(f"added_cents={int(round(total_cents))}\n")
|
f.write(f"added_cents={int(round(total_cents))}\n")
|
||||||
f.write(f"skipped=0\n")
|
f.write(f"skipped=0\n")
|
||||||
|
f.write(f"primary_verdict={heading_verdict}\n")
|
||||||
PY
|
PY
|
||||||
|
|
||||||
- name: Post PR review
|
- name: Post PR review (formal APPROVE / REQUEST_CHANGES / COMMENT)
|
||||||
|
# Uses primary model's verdict as the formal event. Falls back to COMMENT
|
||||||
|
# if the primary model failed or returned something unexpected.
|
||||||
if: steps.budget.outputs.skip != 'true' && steps.review.outputs.skipped != '1'
|
if: steps.budget.outputs.skip != 'true' && steps.review.outputs.skipped != '1'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
|
env:
|
||||||
|
PRIMARY_VERDICT: ${{ steps.review.outputs.primary_verdict }}
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const body = fs.readFileSync('/tmp/review-body.md', 'utf8');
|
const body = fs.readFileSync('/tmp/review-body.md', 'utf8');
|
||||||
|
const v = process.env.PRIMARY_VERDICT || 'COMMENT';
|
||||||
|
const eventMap = { APPROVE: 'APPROVE', REQUEST_CHANGES: 'REQUEST_CHANGES', COMMENT: 'COMMENT' };
|
||||||
await github.rest.pulls.createReview({
|
await github.rest.pulls.createReview({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
pull_number: context.issue.number,
|
pull_number: context.issue.number,
|
||||||
event: 'COMMENT',
|
event: eventMap[v] || 'COMMENT',
|
||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -390,18 +408,21 @@ jobs:
|
|||||||
});
|
});
|
||||||
|
|
||||||
- name: Update monthly spend counter
|
- name: Update monthly spend counter
|
||||||
if: steps.budget.outputs.skip != 'true' && always()
|
if: steps.budget.outputs.skip != 'true' && steps.budget.outputs.tracking == 'true'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
env:
|
env:
|
||||||
PRIOR: ${{ steps.budget.outputs.spend_cents }}
|
PRIOR: ${{ steps.budget.outputs.spend_cents }}
|
||||||
ADDED: ${{ steps.review.outputs.added_cents }}
|
ADDED: ${{ steps.review.outputs.added_cents }}
|
||||||
|
CAP: ${{ steps.budget.outputs.cap_cents }}
|
||||||
with:
|
with:
|
||||||
|
github-token: ${{ secrets.CTO_ADMIN_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
const prior = parseInt(process.env.PRIOR || '0', 10);
|
const prior = parseInt(process.env.PRIOR || '0', 10);
|
||||||
const added = parseInt(process.env.ADDED || '0', 10);
|
const added = parseInt(process.env.ADDED || '0', 10);
|
||||||
|
const cap = parseInt(process.env.CAP || '1000', 10);
|
||||||
const total = prior + added;
|
const total = prior + added;
|
||||||
await github.rest.actions.updateRepoVariable({
|
await github.rest.actions.updateRepoVariable({
|
||||||
owner: context.repo.owner, repo: context.repo.repo,
|
owner: context.repo.owner, repo: context.repo.repo,
|
||||||
name: 'CTO_REVIEW_SPEND_CENTS', value: String(total),
|
name: 'CTO_REVIEW_SPEND_CENTS', value: String(total),
|
||||||
});
|
});
|
||||||
core.notice(`Spend this month: $${(total/100).toFixed(2)} (added $${(added/100).toFixed(2)})`);
|
core.notice(`Budget: $${(total/100).toFixed(2)} / $${(cap/100).toFixed(2)} this month (added $${(added/100).toFixed(2)})`);
|
||||||
|
|||||||
Reference in New Issue
Block a user