TESTING 19.0-dev build testbed #37
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
| name: Checks Handler | |
| permissions: {} # Default to zero permissions as per github actions best practices | |
| on: | |
| # If we use pull_request here, then we will have no access to secrets which means | |
| # that we will be unable to generate tokens. Also, the github token will not have any | |
| # permission to write checks. Using pull_request_target gives us access to secrets. | |
| pull_request_target: | |
| types: [opened, synchronize, reopened] | |
| workflow_run: | |
| # List the wokflows you need update checks for here. Unfortunately, I don't think there's a way to automate this | |
| # You could, of course, define just the workflows that you would NOT like to trigger this workflow, but | |
| # I have not tested this and it would still require manual updating if you added workflows that trigger on | |
| # pull_request that get their own automatic checks (with no secrets). | |
| workflows: | |
| - "Build and Push Docker Image" | |
| types: | |
| - completed # Only update checks on completion. | |
| # - in_progress # You could add this + some logic to update checks to in progress state on start | |
| jobs: | |
| setup-checks: | |
| # This job name will show up as a check name because it runs on pull_request_target. | |
| # Unfortunately, having this rather useless "check" is unavoidable as far as I can tell. | |
| # I have compacted all the checks hanling into one job specifically so that it would only show up as a single check | |
| name: Setup Checks | |
| runs-on: ubuntu-latest | |
| permissions: | |
| checks: write | |
| contents: read | |
| pull-requests: read | |
| actions: read | |
| # This step looks trough all workflow files and finds jobs that begin with _isCheck_ | |
| # It runs for both pull_request_target and workflow_run events. | |
| steps: | |
| # First we have to get the workflow files | |
| - name: Get Workflow files | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | # We avoid checking out the entire repository | |
| .github/workflows | |
| - name: Identify Slash Command Checks | |
| id: get_check_jobs | |
| run: | | |
| all_workflow_checks="{}" # This will store the final JSON output | |
| workflow_dir="${{ github.workspace }}/.github/workflows" | |
| check_job_prefix="_isCheck_" # This is the prefix that identifies a job as a check | |
| for file in "$workflow_dir"/*.yml "$workflow_dir"/*.yaml; do | |
| if [ ! -f "$file" ]; then continue; fi | |
| workflow_name=$(yq '.name' "$file") | |
| if [ -z "$workflow_name" ] || [ "$workflow_name" == "null" ]; then | |
| echo "::warning file=$file::Workflow file has no 'name' property. Skipping." | |
| continue | |
| fi | |
| check_job_ids=$(yq '(.jobs | keys)[]' "$file" | grep -E "^${check_job_prefix}" || true) | |
| dispatch_type=$(yq '.on.repository_dispatch.types[0]' "$file") | |
| if [ -n "$check_job_ids" ]; then | |
| command="" # Default to empty for non-slash-command workflows | |
| if [ -n "$dispatch_type" ] && [ "$dispatch_type" != "null" ]; then | |
| # This is a slash-command workflow, extract the command. | |
| # Use shell parameter expansion to remove the '-command' suffix. | |
| command=${dispatch_type%-command} | |
| fi | |
| workflow_checks_array="[]" # Array to hold checks for the current workflow | |
| for job_id in $check_job_ids; do | |
| check_job_name=$(yq -o=json "$file" | jq --arg id "$job_id" -r '.jobs[$id].name // $id') | |
| workflow_checks_array=$(echo "$workflow_checks_array" | jq --arg job_id "$job_id" --arg check_name "$check_job_name" '. + [{"job_id": $job_id, "check_name": $check_name}]') | |
| done | |
| workflow_object=$(jq -n --arg cmd "$command" --argjson checks_array "$workflow_checks_array" '{"command": $cmd, "checks": $checks_array}') | |
| all_workflow_checks=$(echo "$all_workflow_checks" | jq --arg wf_name "$workflow_name" --argjson wf_obj "$workflow_object" '. + {($wf_name): $wf_obj}') | |
| fi | |
| done | |
| echo "check_jobs=$(echo "$all_workflow_checks" | jq -c .)" >> $GITHUB_OUTPUT | |
| # If use the default token all the checs are in the same group. | |
| # If you uncomment this step and toggle the lines below with github-token: ${{ steps.generate_token.outputs.token }} | |
| # your generated checks will be in their own section under the name of your Github App | |
| # - name: Generate token | |
| # id: generate_token | |
| # uses: actions/create-github-app-token@v3 | |
| # with: | |
| # app-id: ${{ vars.PYXIRIS_APP_ID }} | |
| # private-key: ${{ secrets.PYXIRIS_APP_SECRET }} | |
| # owner: ${{ github.repository_owner }} | |
| # The following steps run only for pull request events to create the initial pending checks. | |
| - name: Create Pending Status Checks | |
| if: github.event_name == 'pull_request_target' | |
| uses: actions/github-script@v9 | |
| with: | |
| # Toggle if using app token | |
| # github-token: ${{ steps.generate_token.outputs.token }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const allCheckJobs = JSON.parse('${{ steps.get_check_jobs.outputs.check_jobs }}'); | |
| if (Object.keys(allCheckJobs).length === 0) { | |
| console.log('No _isCheck_ jobs found.'); | |
| return; | |
| } | |
| // this runs on pull_request_target, so it uses the direct context. | |
| const sha = "${{ github.event.pull_request.head.sha }}"; | |
| const pr_number = context.payload.pull_request.number; | |
| for (const workflowName in allCheckJobs) { // e.g., "Build" | |
| const workflowData = allCheckJobs[workflowName]; // e.g., { "command": "build", "checks": [...] } | |
| const command = workflowData.command; | |
| const checksInWorkflow = workflowData.checks; | |
| const isSlashCommand = command && command.length > 0; | |
| for (const check of checksInWorkflow) { // Iterate through each check defined in the workflow | |
| // Create a unique external_id rather than rely on the display name to reliably find this check later. | |
| // TODO: We could add the sha in here and use it to prevent a check from an old commit from updating | |
| // a check run from a newer commit, but I am not sure how that would behave. Anyways, I have sha checking | |
| // set up independantly further down, so we are already protected against this. | |
| const external_id = `custom_check:${workflowName}:${check.job_id}:${pr_number}`; | |
| await github.rest.checks.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: check.check_name, // Use the specific check's display name | |
| head_sha: sha, | |
| status: 'queued', | |
| external_id: external_id, | |
| output: { | |
| title: isSlashCommand ? 'Awaiting Slash Command' : 'Automatic Check', | |
| summary: isSlashCommand | |
| ? `This check is awaiting a slash command to be triggered.` | |
| : 'This check will run automatically based on code changes.', | |
| text: isSlashCommand | |
| ? `To run this check, comment \`/${command}\` + any required args on the pull request.` | |
| : 'Results will be reported here upon completion.' | |
| } | |
| }); | |
| } | |
| } | |
| # The following steps run only after a slash command workflow completes, to update the final check status. | |
| - name: Download client_payload artifact | |
| if: github.event_name == 'workflow_run' | |
| id: download_artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: client-payload | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| continue-on-error: true # Continue to allow graceful skipping if artifact is not found | |
| - name: Update Checks from Workflow Run | |
| if: | |
| github.event_name == 'workflow_run' && steps.download_artifact.outcome == | |
| 'success' | |
| uses: actions/github-script@v9 | |
| with: | |
| # Toggle if using app token | |
| # github-token: ${{ steps.generate_token.outputs.token }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| // Get PR info from the downloaded payload | |
| if (!fs.existsSync('client_payload.json')) { | |
| core.info('client_payload.json not found. Skipping.'); | |
| return; | |
| } | |
| const payload = JSON.parse(fs.readFileSync('client_payload.json', 'utf8')); | |
| const payload_sha = payload.pull_request.head.sha; | |
| const payload_pr_number = payload.pull_request.number; | |
| if (!payload_pr_number) { | |
| core.info('Could not determine PR number. Skipping.'); | |
| return; | |
| } | |
| // Verify the PR hasn't been updated with new commits | |
| const { data: current_pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: payload_pr_number, | |
| }); | |
| const current_head_sha = current_pr.head.sha; | |
| if (payload_sha !== current_head_sha) { | |
| core.info('PR has been updated since the workflow was triggered. The check status will not be updated.'); | |
| core.info(`SHA of workflow run: ${payload_sha}`); | |
| core.info(`Current head SHA: ${current_head_sha}`); | |
| return; | |
| } | |
| // Find and update the corresponding check run | |
| const triggeredWorkflowName = context.payload.workflow_run.name; | |
| core.info(`Triggered by workflow: ${triggeredWorkflowName}`); | |
| const allCheckJobs = JSON.parse('${{ steps.get_check_jobs.outputs.check_jobs }}'); // e.g., {"Build": {"command": "build", "checks": [...]}} | |
| core.info(`All identified check jobs: ${JSON.stringify(allCheckJobs, null, 2)}`); | |
| const workflowData = allCheckJobs[triggeredWorkflowName]; | |
| if (!workflowData || !workflowData.checks || workflowData.checks.length === 0) { | |
| core.info(`Workflow "${triggeredWorkflowName}" does not contain any jobs prefixed with '_isCheck_'. Skipping.`); | |
| return; | |
| } | |
| core.info(`Found data for this workflow: ${JSON.stringify(workflowData, null, 2)}`); | |
| const run_id = context.payload.workflow_run.id; | |
| core.info(`Workflow run ID: ${run_id}`); | |
| core.info(`Operating on SHA: ${payload_sha}`); | |
| core.info(`Operating on PR Number: ${payload_pr_number}`); | |
| // Fetch all existing check runs for this commit to find the one we need to update. | |
| const { data: { check_runs: existing_checks } } = await github.rest.checks.listForRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: payload_sha, | |
| }); | |
| core.info(`Found ${existing_checks.length} existing checks for SHA ${payload_sha}.`); | |
| // Fetch all jobs for the completed workflow run to get individual job conclusions and URLs | |
| const { data: { jobs: completedJobs } } = await github.rest.actions.listJobsForWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: run_id | |
| }); | |
| core.info(`Found ${completedJobs.length} completed jobs in the workflow run.`); | |
| core.info(`Completed jobs: ${JSON.stringify(completedJobs.map(j => ({name: j.name, conclusion: j.conclusion})), null, 2)}`); | |
| for (const expectedCheck of workflowData.checks) { // Iterate through each expected check | |
| core.info(`\nProcessing expected check: ${JSON.stringify(expectedCheck, null, 2)}`); | |
| // Construct the same unique external_id to find the check we created. | |
| const external_id = `custom_check:${triggeredWorkflowName}:${expectedCheck.job_id}:${payload_pr_number}`; | |
| const checkToUpdate = existing_checks.find(cr => cr.external_id === external_id); | |
| if (!checkToUpdate) { | |
| core.warning(`Could not find a pre-existing check run with external_id "${external_id}" to update.`); | |
| continue; | |
| } | |
| core.info(`Found existing check run "${checkToUpdate.name}" with ID ${checkToUpdate.id} via external_id.`); | |
| const matchingCompletedJob = completedJobs.find(job => job.name === expectedCheck.check_name); | |
| if (matchingCompletedJob) { | |
| core.info(`Found matching completed job: ${matchingCompletedJob.name}`); | |
| const job_run_id = matchingCompletedJob.id; | |
| core.info(`Found matching job with integer ID: ${job_run_id}`); | |
| const jobConclusion = matchingCompletedJob.conclusion; | |
| core.info(`Job conclusion: ${jobConclusion}`); | |
| const details_url = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${run_id}/job/${job_run_id}?pr=${payload_pr_number}`; | |
| core.info(`Constructed Job URL: ${details_url}`); | |
| const checkUpdatePayload = { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| check_run_id: checkToUpdate.id, | |
| status: 'completed', | |
| conclusion: jobConclusion, | |
| output: { | |
| title: `Job run ${jobConclusion}`, | |
| summary: `The "${expectedCheck.check_name}" job in workflow "${triggeredWorkflowName}" finished with status: ${jobConclusion}.\n\n[Click to see job run logs](${details_url})`, | |
| }, | |
| details_url: details_url // This only works when using a Github App token for some reason and is kinda pointless anyways | |
| }; | |
| core.info(`Updating check with payload: ${JSON.stringify(checkUpdatePayload, null, 2)}`); | |
| await github.rest.checks.update(checkUpdatePayload); | |
| core.info(`Successfully updated check: ${expectedCheck.check_name}`); | |
| } else { | |
| core.warning(`Could not find a completed job matching check "${expectedCheck.check_name}" in workflow run ${run_id}. Updating check to 'skipped'.`); | |
| await github.rest.checks.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| check_run_id: checkToUpdate.id, | |
| status: 'completed', | |
| conclusion: 'skipped', | |
| output: { | |
| title: 'Job Skipped', | |
| summary: `The job for this check was not found in the completed workflow, it may have been skipped.` | |
| } | |
| }); | |
| } | |
| } |