-
Notifications
You must be signed in to change notification settings - Fork 265
ci: standardize custom-code budget reporting #1136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| schema_version: 1 | ||
| generation_id: 37722dc7-eca2-41b0-bfce-0f6baee2f2aa | ||
| generation_id: d52034e7-c4ef-4643-b0a5-177be0c8df24 | ||
| openapi_spec_hash: 641f7e0f4f9849ca42dc9d9ba4f938f6 | ||
| openapi_transformed_spec_hash: e9bfbf2ff383ae7da0b32dec03b168e9 | ||
| config_hash: e51f06a3c5b16edb58e2aebe45a67903 | ||
| codegen_sha: f09aa003172bb9fe7c58d8d394b439b0f5892b67 | ||
| codegen_hash: d5980a6ee9429c1ab2cc493e826c10245a3608e19a1540cd38a23e857f0d484a | ||
| public_codegen_sha: 813494a2e0ea4ff52130a75c3e959b4db72534de | ||
| codegen_sha: 0bc5285b80b377a352358b04f36fa219e2c96a7c | ||
| codegen_hash: d97aec850c8938ce86ee2a2703797a71015582003e7e323b81005c0b4bf1d3b2 | ||
| public_codegen_sha: af5a23f281b751563e117b974a795e26fa8943d5 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ permissions: {} | |
| concurrency: | ||
| group: castiron-custom-code-comment-${{ github.event.workflow_run.head_repository.id }}-${{ github.event.workflow_run.head_branch }} | ||
| cancel-in-progress: false | ||
| queue: max # Preserve newer pending evaluations if older runs arrive out of order. | ||
|
|
||
| jobs: | ||
| compute: | ||
|
|
@@ -31,13 +32,15 @@ jobs: | |
| isolation: ${{ steps.budget.outputs.isolation }} | ||
| budget: ${{ steps.budget.outputs.budget }} | ||
| steps: | ||
| # Selecting main here pins both the executable checker and PR policy base. | ||
| # Subsequent steps use this checkout's SHA even if main advances. | ||
| - name: Check out the trusted reporter | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: main | ||
| persist-credentials: false | ||
|
|
||
| - name: Compute from the current pull request Git objects | ||
| - name: Compute from the selected main and pull request Git objects | ||
| id: report | ||
| if: github.event.workflow_run.event == 'pull_request' | ||
| env: | ||
|
|
@@ -53,6 +56,7 @@ jobs: | |
| run: | | ||
| python3 -I scripts/castiron/custom_code_report.py trusted-report \ | ||
| --repo "$RUNNER_TEMP/castiron-objects.git" \ | ||
| --base "$(git rev-parse HEAD)" \ | ||
| --repository "$REPOSITORY" --run-id "$RUN_ID" --run-attempt "$RUN_ATTEMPT" \ | ||
| --out "$RUNNER_TEMP/castiron-custom-code" | ||
| if test -f "$RUNNER_TEMP/castiron-custom-code/context.json"; then | ||
|
|
@@ -133,52 +137,61 @@ jobs: | |
| HEAD_SHA: ${{ needs.compute.outputs.head-sha }} | ||
| ISOLATION_RESULT: ${{ needs.compute.outputs.isolation }} | ||
| BUDGET_RESULT: ${{ needs.compute.outputs.budget }} | ||
| PUBLISH_ATTEMPT: ${{ github.run_attempt }} | ||
| with: | ||
| script: | | ||
| const event = context.payload.workflow_run; | ||
| const {data: run} = await github.rest.actions.getWorkflowRun({...context.repo, run_id: event.id}); | ||
| if (run.head_sha !== event.head_sha || run.run_attempt !== event.run_attempt || | ||
| if (run.event !== event.event || run.head_sha !== event.head_sha || run.run_attempt !== event.run_attempt || | ||
| run.status !== 'completed' || run.path.split('@', 1)[0] !== '.github/workflows/castiron-custom-code.yml' || | ||
| run.repository.full_name !== `${context.repo.owner}/${context.repo.repo}`) return; | ||
| const head = run.head_sha; | ||
| if (!/^[0-9a-f]{40}$/.test(head)) throw new Error('Invalid candidate SHA'); | ||
| const {data: repository} = await github.rest.repos.get(context.repo); | ||
| const branch = repository.default_branch; | ||
| const {data: main} = await github.rest.git.getRef({...context.repo, ref: `heads/${branch}`}); | ||
| const base = main.object.sha; | ||
| const base = process.env.BASE_SHA; | ||
| let fresh = /^[0-9a-f]{40}$/.test(base) && head === process.env.HEAD_SHA; | ||
| if (run.event === 'pull_request') { | ||
| const headRepository = run.head_repository; | ||
| if (!headRepository || !Number.isInteger(headRepository.id) || headRepository.id <= 0 || | ||
| !headRepository.full_name || !headRepository.owner?.login || !run.head_branch) return; | ||
| const pulls = run.pull_requests.length ? run.pull_requests : await github.paginate( | ||
| github.rest.pulls.list, {...context.repo, state: 'open', | ||
| head: `${headRepository.owner.login}:${run.head_branch}`, base: branch, per_page: 100}); | ||
| let pulls = run.pull_requests.length ? run.pull_requests : await github.paginate( | ||
| github.rest.repos.listPullRequestsAssociatedWithCommit, {...context.repo, commit_sha: head}); | ||
| if (!pulls.length) pulls = await github.paginate(github.rest.pulls.list, { | ||
| ...context.repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`, | ||
| }); | ||
| const current = []; | ||
| for (const number of [...new Set(pulls.map(pull => pull.number))].sort((a, b) => a - b)) { | ||
| if (!Number.isInteger(number) || number <= 0) return; | ||
| const {data: pr} = await github.rest.pulls.get({...context.repo, pull_number: number}); | ||
| if (pr.state === 'open' && pr.head.sha === head && pr.base.sha === base && | ||
| pr.head.ref === run.head_branch && pr.head.repo?.id === headRepository.id && | ||
| pr.head.repo?.full_name === headRepository.full_name && | ||
| pr.base.ref === branch && | ||
| pr.base.repo.full_name === `${context.repo.owner}/${context.repo.repo}`) current.push(pr); | ||
| for (const pull of pulls) { | ||
| const {data: pr} = await github.rest.pulls.get({...context.repo, pull_number: pull.number}); | ||
| if (pr.state === 'open' && pr.head.sha === head && | ||
| pr.base.ref === 'main' && pr.base.repo.full_name === `${context.repo.owner}/${context.repo.repo}`) current.push(pr); | ||
| } | ||
| if (current.length !== 1) return; | ||
| } else if (run.event !== 'merge_group' || !run.head_branch.startsWith(`gh-readonly-queue/${branch}/`)) { | ||
| } else if (run.event !== 'merge_group' || !run.head_branch.startsWith('gh-readonly-queue/main/')) { | ||
| return; | ||
| } else { | ||
| const {data: main} = await github.rest.git.getRef({...context.repo, ref: 'heads/main'}); | ||
| fresh = fresh && base === main.object.sha; | ||
| } | ||
| const fresh = base === process.env.BASE_SHA && head === process.env.HEAD_SHA; | ||
| const url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | ||
| // Concurrency serializes writes, but does not order runs or retries. | ||
| // Keep an older evaluation from overwriting a newer result for this head. | ||
| const order = [run.id, run.run_attempt, Number(context.runId), Number(process.env.PUBLISH_ATTEMPT)]; | ||
| const marker = `[evaluation ${order.join(':')}]`; | ||
| const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, {...context.repo, ref: head}); | ||
| const names = ['Castiron / budget-only change', 'Castiron / custom-code budget']; | ||
| for (const status of statuses) { | ||
| if (!names.includes(status.context) || status.creator?.login !== 'github-actions[bot]') continue; | ||
| const prior = status.description?.match(/\[evaluation (\d+):(\d+):(\d+):(\d+)\]$/); | ||
| if (!prior || status.target_url !== `${url.slice(0, url.lastIndexOf('/') + 1)}${prior[3]}`) continue; | ||
| const previous = prior.slice(1).map(Number); | ||
| const different = previous.findIndex((value, index) => value !== order[index]); | ||
| if (different !== -1 && previous[different] > order[different]) return; | ||
| } | ||
| for (const [name, result] of [ | ||
| ['Castiron / budget-only change', process.env.ISOLATION_RESULT], | ||
| ['Castiron / custom-code budget', process.env.BUDGET_RESULT], | ||
| ]) { | ||
| const state = fresh && result === 'success' ? 'success' : 'failure'; | ||
| const description = !fresh ? 'Evaluation unavailable or base changed; rerun against current main.' | ||
| : state === 'success' ? 'Passed against main policy. See the trusted run summary.' | ||
| : 'Budget check failed. See the trusted run summary.'; | ||
| const description = !fresh ? 'Evaluation unavailable or queue base changed; inspect the trusted run and rerun.' | ||
| : `${state === 'success' ? 'Passed' : 'Failed'} against main ${base.slice(0, 12)}. See the trusted run summary.`; | ||
| await github.rest.repos.createCommitStatus({...context.repo, sha: head, context: name, | ||
| state, description, target_url: url}); | ||
| state, description: `${description} ${marker}`, target_url: url}); | ||
| } | ||
|
|
||
| comment: | ||
|
|
@@ -193,7 +206,7 @@ jobs: | |
| pull-requests: write | ||
| steps: | ||
| - name: Check out the trusted publisher | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ github.workflow_sha }} | ||
| persist-credentials: false | ||
|
|
@@ -230,34 +243,15 @@ jobs: | |
| with: | ||
| script: | | ||
| const marker = '<!-- castiron:custom-code-report:v1 -->'; | ||
| const event = context.payload.workflow_run; | ||
| const {data: run} = await github.rest.actions.getWorkflowRun({...context.repo, run_id: event.id}); | ||
| if (run.event !== 'pull_request' || | ||
| run.head_sha !== event.head_sha || run.run_attempt !== event.run_attempt || | ||
| run.status !== 'completed' || | ||
| run.path.split('@', 1)[0] !== '.github/workflows/castiron-custom-code.yml' || | ||
| run.repository.full_name !== `${context.repo.owner}/${context.repo.repo}`) return; | ||
| const {data: repository} = await github.rest.repos.get(context.repo); | ||
| const branch = repository.default_branch; | ||
| const {data: main} = await github.rest.git.getRef({...context.repo, ref: `heads/${branch}`}); | ||
| const headRepository = run.head_repository; | ||
| if (!headRepository || !Number.isInteger(headRepository.id) || headRepository.id <= 0 || | ||
| !headRepository.full_name || !headRepository.owner?.login || !run.head_branch) return; | ||
| const pulls = run.pull_requests?.length ? run.pull_requests : await github.paginate( | ||
| github.rest.pulls.list, {...context.repo, state: 'open', | ||
| head: `${headRepository.owner.login}:${run.head_branch}`, base: branch, per_page: 100}); | ||
| const current = []; | ||
| for (const number of [...new Set(pulls.map(pull => pull.number))].sort((a, b) => a - b)) { | ||
| if (!Number.isInteger(number) || number <= 0) return; | ||
| const {data: pr} = await github.rest.pulls.get({...context.repo, pull_number: number}); | ||
| if (pr.state === 'open' && pr.head.sha === run.head_sha && | ||
| pr.head.ref === run.head_branch && pr.head.repo?.id === headRepository.id && | ||
| pr.head.repo?.full_name === headRepository.full_name && | ||
| pr.base.sha === main.object.sha && pr.base.ref === branch && | ||
| pr.base.repo.full_name === `${context.repo.owner}/${context.repo.repo}`) current.push(pr); | ||
| } | ||
| if (current.length !== 1) return; | ||
| for (const pull of current) { | ||
| const run = context.payload.workflow_run; | ||
| if (run.event !== 'pull_request' || run.path !== '.github/workflows/castiron-custom-code.yml') return; | ||
| let pulls = run.pull_requests?.length ? run.pull_requests : await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, {...context.repo, commit_sha: run.head_sha}); | ||
| if (!pulls.length) pulls = await github.paginate(github.rest.pulls.list, { | ||
| ...context.repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`, | ||
| }); | ||
| for (const pull of pulls) { | ||
| const {data: current} = await github.rest.pulls.get({...context.repo, pull_number: pull.number}); | ||
| if (current.state !== 'open' || current.head.sha !== run.head_sha) continue; | ||
|
Comment on lines
+252
to
+254
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When AGENTS.md reference: AGENTS.md:L41-L45 Useful? React with 👍 / 👎.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Keep failure reports behind the same live-run/PR validation This replaces the previous live-run and unique-target checks with the event payload and an open/same-SHA test. A delayed attempt-1 publisher can reach Please fix this in the shared template and regenerate: resolve the current source run/attempt and exactly one matching source-repository/branch PR targeting main once, and reuse that decision for success and failure publication. Recheck freshness before writing. Cover a newer retry still in progress and an unrelated same-SHA PR; the current failure fixtures omit these identity fields. |
||
| const comments = await github.paginate(github.rest.issues.listComments, {...context.repo, issue_number: pull.number}); | ||
| const previous = comments.find(c => c.user?.type === 'Bot' && c.user?.login === 'github-actions[bot]' && c.body?.startsWith(marker)); | ||
| const prior = previous?.body?.match(/<!-- castiron:run:v1:(\d+):(\d+) -->/); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GitHub's workflow syntax only supports
groupandcancel-in-progressin theconcurrencymapping;queueis not valid there. This makes the trustedworkflow_runworkflow invalid, preventing all custom-code status and comment jobs from running. Remove this key or implement ordering with supported workflow logic, and update the authoritative generator template rather than only its generated output.AGENTS.md reference: AGENTS.md:L5-L8
Useful? React with 👍 / 👎.