Castiron custom code comment #1486
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details. | |
| name: Castiron custom code comment | |
| on: | |
| workflow_run: | |
| workflows: [Castiron custom code] | |
| types: [completed] | |
| 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: | |
| name: Compute trusted custom-code report | |
| if: contains(fromJSON('["pull_request", "merge_group"]'), github.event.workflow_run.event) && github.event.workflow_run.path == '.github/workflows/castiron-custom-code.yml' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| actions: read | |
| pull-requests: read | |
| outputs: | |
| number: ${{ steps.report.outputs.number }} | |
| artifact-id: ${{ steps.artifact.outputs.artifact-id }} | |
| artifact-run-attempt: ${{ github.run_attempt }} | |
| base-sha: ${{ steps.budget.outputs.base_sha }} | |
| head-sha: ${{ steps.budget.outputs.head_sha }} | |
| 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 # v7.0.1 | |
| with: | |
| ref: main | |
| persist-credentials: false | |
| - name: Compute from the selected main and pull request Git objects | |
| id: report | |
| if: github.event.workflow_run.event == 'pull_request' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GIT_CONFIG_COUNT: '2' | |
| GIT_CONFIG_KEY_0: credential.helper | |
| GIT_CONFIG_VALUE_0: '' | |
| GIT_CONFIG_KEY_1: credential.https://github.com.helper | |
| GIT_CONFIG_VALUE_1: '!gh auth git-credential' | |
| REPOSITORY: ${{ github.repository }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} | |
| 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 | |
| number=$(jq -er '.pr' "$RUNNER_TEMP/castiron-custom-code/context.json") | |
| printf 'number=%s\n' "$number" >> "$GITHUB_OUTPUT" | |
| cat "$RUNNER_TEMP/castiron-custom-code/summary.md" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Upload the trusted report and patch | |
| id: artifact | |
| if: steps.report.outputs.number != '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: castiron-custom-code-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/castiron-custom-code/ | |
| if-no-files-found: error | |
| retention-days: 7 | |
| # The prior step's report was computed here from | |
| # Git objects by main's reporter, not downloaded from the candidate run. | |
| - name: Evaluate main's custom-code budget | |
| id: budget | |
| if: always() | |
| continue-on-error: true # A budget failure must not suppress the existing report comment. | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| SOURCE_EVENT: ${{ github.event.workflow_run.event }} | |
| GIT_CONFIG_COUNT: '2' | |
| GIT_CONFIG_KEY_0: credential.helper | |
| GIT_CONFIG_VALUE_0: '' | |
| GIT_CONFIG_KEY_1: credential.https://github.com.helper | |
| GIT_CONFIG_VALUE_1: '!gh auth git-credential' | |
| run: | | |
| trusted_sha=$(git rev-parse HEAD) | |
| reuse=() | |
| if [[ "$SOURCE_EVENT" == pull_request ]]; then | |
| reuse=(--trusted-report-dir "$RUNNER_TEMP/castiron-custom-code") | |
| fi | |
| python3 -I scripts/castiron/custom_code_budget.py github \ | |
| --repository "$REPOSITORY" --event-path "$GITHUB_EVENT_PATH" \ | |
| --trusted-sha "$trusted_sha" --repo "$RUNNER_TEMP/castiron-objects.git" \ | |
| "${reuse[@]}" --out "$RUNNER_TEMP/custom-code-budget" | |
| - name: Add the budget to the run summary | |
| if: always() | |
| run: | | |
| if test -f "$RUNNER_TEMP/custom-code-budget/summary.md"; then | |
| cat "$RUNNER_TEMP/custom-code-budget/summary.md" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Upload trusted budget measurements | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: custom-code-budget-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/custom-code-budget/ | |
| if-no-files-found: error | |
| retention-days: 7 | |
| budget-status: | |
| name: Publish custom-code budget checks | |
| needs: compute | |
| if: always() && !cancelled() && needs.compute.result != 'skipped' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| actions: read | |
| pull-requests: read | |
| statuses: write | |
| steps: | |
| # This workflow definition is from main. No candidate checkout/artifacts. | |
| - name: Publish exact-head statuses after checking freshness | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| BASE_SHA: ${{ needs.compute.outputs.base-sha }} | |
| 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.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 base = process.env.BASE_SHA; | |
| let fresh = /^[0-9a-f]{40}$/.test(base) && head === process.env.HEAD_SHA; | |
| if (run.event === 'pull_request') { | |
| 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 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/main/')) { | |
| return; | |
| } else { | |
| const {data: main} = await github.rest.git.getRef({...context.repo, ref: 'heads/main'}); | |
| fresh = fresh && base === main.object.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 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: `${description} ${marker}`, target_url: url}); | |
| } | |
| comment: | |
| name: Update custom-code comment | |
| needs: compute | |
| if: always() && !cancelled() && github.event.workflow_run.event == 'pull_request' && (needs.compute.result == 'failure' || needs.compute.outputs.number != '') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| actions: read | |
| pull-requests: write | |
| steps: | |
| - name: Check out the trusted publisher | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.workflow_sha }} | |
| persist-credentials: false | |
| - name: Download this workflow's trusted report | |
| if: needs.compute.result == 'success' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| artifact-ids: ${{ needs.compute.outputs.artifact-id }} | |
| merge-multiple: true | |
| path: ${{ runner.temp }}/castiron-custom-code | |
| - name: Create or update the single report comment | |
| id: publish | |
| if: needs.compute.result == 'success' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| PR_NUMBER: ${{ needs.compute.outputs.number }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} | |
| ARTIFACT_RUN_ID: ${{ github.run_id }} | |
| ARTIFACT_RUN_ATTEMPT: ${{ needs.compute.outputs.artifact-run-attempt }} | |
| run: | | |
| python3 -I scripts/castiron/custom_code_report.py comment \ | |
| --report "$RUNNER_TEMP/castiron-custom-code/report.json" \ | |
| --repository "$REPOSITORY" --pr "$PR_NUMBER" --run-id "$RUN_ID" \ | |
| --run-attempt "$RUN_ATTEMPT" \ | |
| --artifact-run-id "$ARTIFACT_RUN_ID" --artifact-run-attempt "$ARTIFACT_RUN_ATTEMPT" | |
| - name: Publish a trusted failure status | |
| if: always() && !cancelled() && steps.publish.outcome != 'success' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const marker = '<!-- castiron:custom-code-report:v1 -->'; | |
| 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; | |
| 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+) -->/); | |
| if (prior && (Number(prior[1]) > run.id || (Number(prior[1]) === run.id && Number(prior[2]) > run.run_attempt))) continue; | |
| const url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${run.id}`; | |
| const body = `${marker}\n\n## Castiron custom code\n\n⚠️ Report unavailable for \`${run.head_sha.slice(0, 12)}\`.\n\nThe report setup or validation failed. [Inspect the workflow run](${url}).\n\n<!-- castiron:run:v1:${run.id}:${run.run_attempt} -->`; | |
| if (previous) await github.rest.issues.updateComment({...context.repo, comment_id: previous.id, body}); | |
| else await github.rest.issues.createComment({...context.repo, issue_number: pull.number, body}); | |
| } |