name: PR slow CI - Suggestion on: pull_request_target: types: [opened, synchronize, reopened] permissions: contents: read jobs: get-pr-number: name: Get PR number uses: ./.github/workflows/get-pr-number.yml get-pr-info: name: Get PR commit SHA needs: get-pr-number if: ${{ needs.get-pr-number.outputs.PR_NUMBER != ''}} uses: ./.github/workflows/get-pr-info.yml with: pr_number: ${{ needs.get-pr-number.outputs.PR_NUMBER }} get-jobs: name: Get test files to run runs-on: ubuntu-22.04 needs: [get-pr-number, get-pr-info] outputs: jobs: ${{ steps.get_jobs.outputs.jobs_to_run }} steps: # This checkout to the main branch - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: "0" persist-credentials: false # Refetch the PR file list from the API instead of receiving it as an # input from `get-pr-info.yml`. The previous approaches either expanded # attacker-controlled content into a shell/JS source via `${{ ... }}` # (template injection, fixed in #45956) or passed the full JSON through # an env var (`E2BIG` / "Argument list too long" once the patch payload # exceeds the per-env-var kernel limit, ~128KB). Fetching inside the # action keeps the JSON in Node heap memory and writes it straight to # disk, so it never crosses an execve argv/envp boundary. - name: Write pr_files file uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 env: PR_NUMBER: ${{ needs.get-pr-number.outputs.PR_NUMBER }} with: script: | const fs = require('node:fs'); const files = await github.paginate(github.rest.pulls.listFiles, { owner: context.repo.owner, repo: context.repo.repo, pull_number: parseInt(process.env.PR_NUMBER, 10), }); fs.writeFileSync('pr_files.txt', JSON.stringify(files)); - name: Get repository content id: repo_content uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 env: PR_HEAD_REPO_OWNER: ${{ needs.get-pr-info.outputs.PR_HEAD_REPO_OWNER }} PR_HEAD_REPO_NAME: ${{ needs.get-pr-info.outputs.PR_HEAD_REPO_NAME }} PR_HEAD_SHA: ${{ needs.get-pr-info.outputs.PR_HEAD_SHA }} with: script: | const fs = require('node:fs'); const { PR_HEAD_REPO_OWNER, PR_HEAD_REPO_NAME, PR_HEAD_SHA } = process.env; const { data: tests_dir } = await github.rest.repos.getContent({ owner: PR_HEAD_REPO_OWNER, repo: PR_HEAD_REPO_NAME, path: 'tests', ref: PR_HEAD_SHA, }); const { data: tests_models_dir } = await github.rest.repos.getContent({ owner: PR_HEAD_REPO_OWNER, repo: PR_HEAD_REPO_NAME, path: 'tests/models', ref: PR_HEAD_SHA, }); const { data: tests_quantization_dir } = await github.rest.repos.getContent({ owner: PR_HEAD_REPO_OWNER, repo: PR_HEAD_REPO_NAME, path: 'tests/quantization', ref: PR_HEAD_SHA, }); // Write to files instead of outputs fs.writeFileSync('tests_dir.txt', JSON.stringify(tests_dir, null, 2)); fs.writeFileSync('tests_models_dir.txt', JSON.stringify(tests_models_dir, null, 2)); fs.writeFileSync('tests_quantization_dir.txt', JSON.stringify(tests_quantization_dir, null, 2)); - name: Run script to get jobs to run id: get_jobs run: | python utils/get_pr_run_slow_jobs.py | tee output.txt echo "jobs_to_run: $(tail -n 1 output.txt)" echo "jobs_to_run=$(tail -n 1 output.txt)" >> $GITHUB_OUTPUT send_comment: # Will delete the previous comment and send a new one if: # - either the content is changed # - or the previous comment is 30 minutes or more old name: Send a comment to suggest jobs to run if: ${{ needs.get-jobs.outputs.jobs != '' }} needs: [get-pr-number, get-jobs] permissions: pull-requests: write runs-on: ubuntu-22.04 steps: - name: Check and update comment if needed uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: BODY: "\n\nrun-slow: ${{ needs.get-jobs.outputs.jobs }}" PR_NUMBER: ${{ needs.get-pr-number.outputs.PR_NUMBER }} with: script: | const prNumber = parseInt(process.env.PR_NUMBER, 10); const commentPrefix = "**[For maintainers]** Suggested jobs to run (before merge)"; const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000); // 30 minutes ago const newBody = `${commentPrefix}${process.env.BODY}`; // Get all comments on the PR const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); // Find existing comments that start with our prefix const existingComments = comments.filter(comment => comment.user.login === 'github-actions[bot]' && comment.body.startsWith(commentPrefix) ); let shouldCreateNewComment = true; let commentsToDelete = []; if (existingComments.length > 0) { // Get the most recent comment const mostRecentComment = existingComments .sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0]; const commentDate = new Date(mostRecentComment.created_at); const isOld = commentDate < thirtyMinutesAgo; const isDifferentContent = mostRecentComment.body !== newBody; console.log(`Most recent comment created: ${mostRecentComment.created_at}`); console.log(`Is older than 30 minutes: ${isOld}`); console.log(`Has different content: ${isDifferentContent}`); if (isOld || isDifferentContent) { // Delete all existing comments and create new one commentsToDelete = existingComments; console.log(`Will delete ${commentsToDelete.length} existing comment(s) and create new one`); } else { // Content is same and comment is recent, skip shouldCreateNewComment = false; console.log('Comment is recent and content unchanged, skipping update'); } } else { console.log('No existing comments found, will create new one'); } // Delete old comments if needed for (const comment of commentsToDelete) { console.log(`Deleting comment #${comment.id} (created: ${comment.created_at})`); await github.rest.issues.deleteComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: comment.id }); } // Create new comment if needed if (shouldCreateNewComment) { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: newBody }); console.log('✅ New comment created'); } else { console.log('ℹ️ No comment update needed'); }