[release/13.1] Undo the changes to bump PostgreSql to 18 until we validate No Data Checksums #399
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: Quarantine/Disable Test | |
| on: | |
| issue_comment: | |
| types: [created] | |
| # Prevent concurrent runs on the same issue/PR to avoid race conditions | |
| # Use issue/pr prefix to distinguish between issue comments and PR comments | |
| concurrency: | |
| group: quarantine-test-${{ github.event.issue.pull_request && 'pr' || 'issue' }}-${{ github.event.issue.number }} | |
| cancel-in-progress: false | |
| jobs: | |
| quarantine_test: | |
| if: >- | |
| github.repository == 'dotnet/aspire' && | |
| ( | |
| startsWith(github.event.comment.body, '/quarantine-test ') || | |
| startsWith(github.event.comment.body, '/unquarantine-test ') || | |
| startsWith(github.event.comment.body, '/disable-test ') || | |
| startsWith(github.event.comment.body, '/enable-test ') | |
| ) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Extract command and arguments | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| id: extract-command | |
| with: | |
| result-encoding: string | |
| script: | | |
| const body = context.payload.comment.body; | |
| // Link to documentation for usage examples | |
| const docsUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/tools/QuarantineTools/README.md`; | |
| // Helper to fail with an error message that can be captured | |
| function failWithError(message) { | |
| const fullMessage = `${message}\n\n📖 See [QuarantineTools README](${docsUrl}) for usage examples.`; | |
| core.setOutput('error_message', fullMessage); | |
| core.setFailed(message); | |
| } | |
| // Unified command pattern: matches /command-name followed by arguments | |
| // Commands: quarantine-test, unquarantine-test, disable-test, enable-test | |
| const commandPattern = /^\/(quarantine-test|unquarantine-test|disable-test|enable-test)\s+(.+)$/m; | |
| // Command configuration lookup | |
| const commandConfig = { | |
| 'quarantine-test': { action: 'quarantine', mode: 'quarantine', requiresUrl: true }, | |
| 'unquarantine-test': { action: 'unquarantine', mode: 'quarantine', requiresUrl: false }, | |
| 'disable-test': { action: 'quarantine', mode: 'activeissue', requiresUrl: true }, | |
| 'enable-test': { action: 'unquarantine', mode: 'activeissue', requiresUrl: false } | |
| }; | |
| const match = commandPattern.exec(body); | |
| if (!match) { | |
| // This shouldn't happen due to job-level 'if' condition - just fail silently | |
| core.setFailed('No valid command found'); | |
| return; | |
| } | |
| const commandName = match[1]; | |
| const args = match[2].trim(); | |
| const matched = commandConfig[commandName]; | |
| // Parse arguments - extract --target-pr option first | |
| let targetPrUrl = ''; | |
| let remainingArgs = args; | |
| // Extract --target-pr <url> if present | |
| const targetPrPattern = /--target-pr\s+(\S+)/; | |
| const targetPrMatch = targetPrPattern.exec(args); | |
| if (targetPrMatch) { | |
| targetPrUrl = targetPrMatch[1]; | |
| remainingArgs = args.replace(targetPrPattern, '').trim(); | |
| } | |
| const parts = remainingArgs.split(/\s+/).filter(p => p.length > 0); | |
| // Check for unknown arguments (anything starting with - or --) | |
| const unknownArgs = parts.filter(p => p.startsWith('-')); | |
| if (unknownArgs.length > 0) { | |
| failWithError(`Unknown argument(s): ${unknownArgs.join(', ')}. Only --target-pr is supported.`); | |
| return; | |
| } | |
| if (parts.length === 0) { | |
| failWithError('No test name(s) provided.'); | |
| return; | |
| } | |
| let testNames = []; | |
| let issueUrl = ''; | |
| // URL pattern for validation | |
| const urlPattern = /^https?:\/\/.+/i; | |
| // GitHub PR URL pattern to extract owner/repo/pr_number | |
| const prUrlPattern = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/; | |
| // Validate target PR URL format if provided | |
| if (targetPrUrl && !prUrlPattern.test(targetPrUrl)) { | |
| failWithError(`Invalid --target-pr URL: "${targetPrUrl}". Must be a GitHub pull request URL (e.g., https://github.com/owner/repo/pull/123).`); | |
| return; | |
| } | |
| if (matched.requiresUrl) { | |
| // For quarantine/disable: need at least test name + issue URL | |
| if (parts.length < 2) { | |
| failWithError(`Missing required arguments. This command requires at least one test name and an issue URL.`); | |
| return; | |
| } | |
| // Last part should be the issue URL | |
| const lastPart = parts[parts.length - 1]; | |
| if (urlPattern.test(lastPart)) { | |
| issueUrl = lastPart; | |
| testNames = parts.slice(0, -1); | |
| } else { | |
| failWithError(`Invalid issue URL: "${lastPart}". The last argument must be a valid issue URL (e.g., https://github.com/owner/repo/issues/123).`); | |
| return; | |
| } | |
| if (testNames.length === 0) { | |
| failWithError('No test name(s) provided.'); | |
| return; | |
| } | |
| } else { | |
| // For unquarantine/enable: test names only | |
| testNames = parts; | |
| } | |
| // Validate target PR URL is from the same repo if provided | |
| if (targetPrUrl) { | |
| const prMatch = prUrlPattern.exec(targetPrUrl); | |
| if (prMatch) { | |
| const [, prOwner, prRepo] = prMatch; | |
| if (prOwner !== context.repo.owner || prRepo !== context.repo.repo) { | |
| failWithError(`Target PR must be from the same repository (${context.repo.owner}/${context.repo.repo}). Got: ${prOwner}/${prRepo}`); | |
| return; | |
| } | |
| } | |
| } | |
| // Determine human-readable action name for PR title | |
| let actionVerb; | |
| if (matched.action === 'quarantine') { | |
| actionVerb = matched.mode === 'quarantine' ? 'Quarantine' : 'Disable'; | |
| } else { | |
| actionVerb = matched.mode === 'quarantine' ? 'Unquarantine' : 'Enable'; | |
| } | |
| // Determine if comment is on a PR (to use as target if no PR URL specified) | |
| const isCommentOnPr = !!context.payload.issue.pull_request; | |
| let commentPrNumber = ''; | |
| if (isCommentOnPr && !targetPrUrl) { | |
| commentPrNumber = context.issue.number.toString(); | |
| } | |
| const result = { | |
| action: matched.action, | |
| mode: matched.mode, | |
| testNames: testNames, | |
| issueUrl: issueUrl, | |
| targetPrUrl: targetPrUrl, | |
| actionVerb: actionVerb, | |
| isCommentOnPr: isCommentOnPr, | |
| commentPrNumber: commentPrNumber | |
| }; | |
| console.log('Parsed command:', JSON.stringify(result, null, 2)); | |
| core.setOutput('action', result.action); | |
| core.setOutput('mode', result.mode); | |
| core.setOutput('test_names', testNames.join(' ')); | |
| core.setOutput('issue_url', issueUrl); | |
| core.setOutput('action_verb', actionVerb); | |
| core.setOutput('target_pr_url', targetPrUrl); | |
| core.setOutput('is_comment_on_pr', isCommentOnPr.toString()); | |
| core.setOutput('comment_pr_number', commentPrNumber); | |
| return 'success'; | |
| - name: Verify user has write access | |
| if: success() | |
| id: verify-permission | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const comment_user = context.payload.comment.user.login; | |
| try { | |
| const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: comment_user | |
| }); | |
| const writePermissions = ['admin', 'write']; | |
| if (!writePermissions.includes(permission.permission)) { | |
| core.setOutput('has_permission', 'false'); | |
| core.setFailed(`@${comment_user} does not have write access to this repo. Required permissions: write or admin.`); | |
| return; | |
| } | |
| core.setOutput('has_permission', 'true'); | |
| console.log(`Verified ${comment_user} has ${permission.permission} access to the repo.`); | |
| } catch (error) { | |
| core.setOutput('has_permission', 'false'); | |
| core.setFailed(`Error checking permissions for @${comment_user}: ${error.message}`); | |
| } | |
| - name: Unlock issue/PR if locked | |
| id: unlock-issue | |
| if: success() && github.event.issue.locked == true | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| // Capture the original lock reason for re-locking later | |
| const lockReason = context.payload.issue.active_lock_reason || 'resolved'; | |
| core.setOutput('original_lock_reason', lockReason); | |
| console.log(`Unlocking locked issue/PR #${context.issue.number} (original reason: ${lockReason}).`); | |
| await github.rest.issues.unlock({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| - name: Add reaction to indicate processing | |
| if: success() | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: 'eyes' | |
| }); | |
| - name: Determine target PR and branch | |
| if: success() | |
| id: determine-target | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| TARGET_PR_URL: ${{ steps.extract-command.outputs.target_pr_url }} | |
| IS_COMMENT_ON_PR: ${{ steps.extract-command.outputs.is_comment_on_pr }} | |
| COMMENT_PR_NUMBER: ${{ steps.extract-command.outputs.comment_pr_number }} | |
| with: | |
| script: | | |
| const targetPrUrl = process.env.TARGET_PR_URL || ''; | |
| const isCommentOnPr = process.env.IS_COMMENT_ON_PR === 'true'; | |
| const commentPrNumber = process.env.COMMENT_PR_NUMBER || ''; | |
| // Determine target PR number (from URL or from comment context) | |
| let targetPrNumber = null; | |
| if (targetPrUrl) { | |
| const prUrlPattern = /\/pull\/(\d+)/; | |
| const match = prUrlPattern.exec(targetPrUrl); | |
| if (match) { | |
| targetPrNumber = parseInt(match[1], 10); | |
| } | |
| } else if (isCommentOnPr && commentPrNumber) { | |
| targetPrNumber = parseInt(commentPrNumber, 10); | |
| } | |
| if (targetPrNumber) { | |
| console.log(`Target PR: #${targetPrNumber}`); | |
| // Get the PR details | |
| let pr; | |
| try { | |
| const { data } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: targetPrNumber | |
| }); | |
| pr = data; | |
| } catch (prError) { | |
| let msg; | |
| if (prError.status === 404) { | |
| msg = `PR #${targetPrNumber} was not found. It may have been deleted or the PR number is invalid.`; | |
| } else if (prError.status === 403) { | |
| msg = `Access denied when fetching PR #${targetPrNumber}. Check repository permissions.`; | |
| } else { | |
| msg = `Failed to get PR #${targetPrNumber}: ${prError.message} (status: ${prError.status || 'unknown'})`; | |
| } | |
| core.setOutput('error_message', msg); | |
| core.setFailed(msg); | |
| return; | |
| } | |
| // Check if PR is still open | |
| if (pr.state !== 'open') { | |
| const msg = `PR #${targetPrNumber} is ${pr.state}. Can only push to open PRs.`; | |
| core.setOutput('error_message', msg); | |
| core.setFailed(msg); | |
| return; | |
| } | |
| // Check if PR is from a fork (not supported) - also handle deleted fork repos | |
| if (!pr.head.repo || pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) { | |
| const msg = `PR #${targetPrNumber} is from a fork or the source repository was deleted. Cannot push to fork branches.`; | |
| core.setOutput('error_message', msg); | |
| core.setFailed(msg); | |
| return; | |
| } | |
| core.setOutput('target_pr_number', targetPrNumber.toString()); | |
| core.setOutput('checkout_ref', pr.head.ref); | |
| core.setOutput('pr_url', pr.html_url); | |
| core.setOutput('is_new_pr', 'false'); | |
| } else { | |
| console.log('No target PR, will create a new one from main'); | |
| core.setOutput('target_pr_number', ''); | |
| core.setOutput('checkout_ref', 'main'); | |
| core.setOutput('pr_url', ''); | |
| core.setOutput('is_new_pr', 'true'); | |
| } | |
| - name: Post started comment | |
| if: success() | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ACTION_VERB: ${{ steps.extract-command.outputs.action_verb }} | |
| TEST_NAMES: ${{ steps.extract-command.outputs.test_names }} | |
| TARGET_PR_NUMBER: ${{ steps.determine-target.outputs.target_pr_number }} | |
| with: | |
| script: | | |
| const actionVerb = process.env.ACTION_VERB; | |
| const testNames = (process.env.TEST_NAMES || '').split(' '); | |
| const workflow_run_url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const targetPrNumber = process.env.TARGET_PR_NUMBER || ''; | |
| const testList = testNames.map(t => `\`${t}\``).join(', '); | |
| let body = `⏳ Started ${actionVerb.toLowerCase()} operation for ${testList}`; | |
| if (targetPrNumber) { | |
| body += ` (will push to PR #${targetPrNumber})`; | |
| } | |
| body += `... ([workflow run](${workflow_run_url}))`; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| # Credentials must persist for git push operations later in workflow | |
| - name: Checkout repo | |
| if: success() | |
| uses: actions/checkout@v4 # zizmor: ignore[artipacked] | |
| with: | |
| ref: ${{ steps.determine-target.outputs.checkout_ref }} | |
| fetch-depth: 0 | |
| - name: Setup .NET | |
| if: success() | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: global.json | |
| - name: Run QuarantineTools | |
| if: success() | |
| id: run-tool | |
| shell: bash | |
| working-directory: ${{ github.workspace }} | |
| env: | |
| ACTION: ${{ steps.extract-command.outputs.action }} | |
| MODE: ${{ steps.extract-command.outputs.mode }} | |
| TEST_NAMES: ${{ steps.extract-command.outputs.test_names }} | |
| ISSUE_URL: ${{ steps.extract-command.outputs.issue_url }} | |
| run: | | |
| # Build the command | |
| if [ "$ACTION" = "quarantine" ]; then | |
| FLAG="-q" | |
| else | |
| FLAG="-u" | |
| fi | |
| # Convert space-separated test names to array for proper quoting | |
| IFS=' ' read -ra TEST_ARRAY <<< "$TEST_NAMES" | |
| # Run the tool and capture output | |
| if [ "$ACTION" = "quarantine" ]; then | |
| echo "Running: dotnet run --project ${{ github.workspace }}/tools/QuarantineTools/QuarantineTools.csproj -- $FLAG -m $MODE -i \"$ISSUE_URL\" ${TEST_ARRAY[*]}" | |
| OUTPUT=$(dotnet run --project ${{ github.workspace }}/tools/QuarantineTools/QuarantineTools.csproj -- $FLAG -m "$MODE" -i "$ISSUE_URL" "${TEST_ARRAY[@]}" 2>&1) && EXIT_CODE=0 || EXIT_CODE=$? | |
| else | |
| echo "Running: dotnet run --project ${{ github.workspace }}/tools/QuarantineTools/QuarantineTools.csproj -- $FLAG -m $MODE ${TEST_ARRAY[*]}" | |
| OUTPUT=$(dotnet run --project ${{ github.workspace }}/tools/QuarantineTools/QuarantineTools.csproj -- $FLAG -m "$MODE" "${TEST_ARRAY[@]}" 2>&1) && EXIT_CODE=0 || EXIT_CODE=$? | |
| fi | |
| echo "$OUTPUT" | |
| # Save output for failure comment (escape for GitHub Actions) | |
| EOF="EOF_$(date +%s%N)" | |
| echo "tool_output<<$EOF" >> $GITHUB_OUTPUT | |
| echo "$OUTPUT" >> $GITHUB_OUTPUT | |
| echo "$EOF" >> $GITHUB_OUTPUT | |
| exit $EXIT_CODE | |
| - name: Check for changes | |
| if: success() | |
| id: check-changes | |
| run: | | |
| if git diff --quiet; then | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| git diff --name-only | |
| fi | |
| - name: Create or Update Pull Request | |
| if: steps.check-changes.outputs.has_changes == 'true' | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ACTION_VERB: ${{ steps.extract-command.outputs.action_verb }} | |
| TEST_NAMES: ${{ steps.extract-command.outputs.test_names }} | |
| ISSUE_URL: ${{ steps.extract-command.outputs.issue_url }} | |
| MODE: ${{ steps.extract-command.outputs.mode }} | |
| TARGET_PR_NUMBER: ${{ steps.determine-target.outputs.target_pr_number }} | |
| TARGET_PR_URL: ${{ steps.determine-target.outputs.pr_url }} | |
| CHECKOUT_REF: ${{ steps.determine-target.outputs.checkout_ref }} | |
| IS_NEW_PR: ${{ steps.determine-target.outputs.is_new_pr }} | |
| with: | |
| script: | | |
| const { spawnSync } = require('child_process'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const os = require('os'); | |
| const actionVerb = process.env.ACTION_VERB; | |
| const testNames = process.env.TEST_NAMES.split(' '); | |
| const issueUrl = process.env.ISSUE_URL; | |
| const mode = process.env.MODE; | |
| const targetPrNumber = process.env.TARGET_PR_NUMBER ? parseInt(process.env.TARGET_PR_NUMBER, 10) : null; | |
| const targetPrUrl = process.env.TARGET_PR_URL; | |
| const checkoutRef = process.env.CHECKOUT_REF; | |
| const isNewPr = process.env.IS_NEW_PR === 'true'; | |
| const comment_user = context.payload.comment.user.login; | |
| const issue_number = context.issue.number; | |
| const testList = testNames.length === 1 ? testNames[0] : `${testNames.length} tests`; | |
| const commitMessage = `[automated] ${actionVerb} ${testList}`; | |
| // Helper to run git commands safely using spawnSync (avoids shell injection) | |
| function runGit(args) { | |
| const result = spawnSync('git', args, { encoding: 'utf-8', stdio: 'pipe' }); | |
| if (result.status !== 0) { | |
| const error = new Error(result.stderr || result.stdout || 'Git command failed'); | |
| error.status = result.status; | |
| throw error; | |
| } | |
| return result.stdout; | |
| } | |
| // Helper to commit with a message (uses temp file for safety) | |
| function gitCommit(message) { | |
| const msgFile = path.join(process.env.RUNNER_TEMP || os.tmpdir(), 'commit-msg.txt'); | |
| fs.writeFileSync(msgFile, message); | |
| try { | |
| runGit(['commit', '-F', msgFile]); | |
| } finally { | |
| fs.unlinkSync(msgFile); | |
| } | |
| } | |
| // Configure git | |
| runGit(['config', 'user.name', 'github-actions']); | |
| runGit(['config', 'user.email', '[email protected]']); | |
| let prUrl = ''; | |
| let prNumber = null; | |
| let createdNewPr = false; | |
| if (!isNewPr && targetPrNumber) { | |
| // Push to existing PR - we're already on the PR branch | |
| console.log(`Committing and pushing to existing PR #${targetPrNumber}`); | |
| prUrl = targetPrUrl; | |
| prNumber = targetPrNumber; | |
| // Stage and commit | |
| runGit(['add', '-A']); | |
| gitCommit(commitMessage); | |
| // Push to the PR branch (checkoutRef comes from PR, use spawnSync for safety) | |
| try { | |
| runGit(['push', 'origin', `HEAD:${checkoutRef}`]); | |
| } catch (pushError) { | |
| core.setFailed(`Failed to push to PR #${targetPrNumber}: ${pushError.message}. The branch may be protected or have been force-pushed.`); | |
| return; | |
| } | |
| console.log(`Pushed new commit to PR #${prNumber}`); | |
| } else { | |
| // Create a new PR | |
| createdNewPr = true; | |
| // Branch name is safe: actionVerb is from controlled lookup (Quarantine/Disable/etc), runId is numeric | |
| // Branch name format: automated/{quarantine|unquarantine|disable|enable}-test-{runId} | |
| // Using runId instead of timestamp to guarantee uniqueness across parallel workflow runs | |
| const branchName = `automated/${actionVerb.toLowerCase()}-test-${context.runId}`; | |
| // Create and checkout branch (use spawnSync for safety) | |
| runGit(['checkout', '-b', branchName]); | |
| // Stage and commit changes | |
| runGit(['add', '-A']); | |
| gitCommit(commitMessage); | |
| // Push branch | |
| try { | |
| runGit(['push', 'origin', branchName]); | |
| } catch (pushError) { | |
| core.setFailed(`Failed to push branch '${branchName}': ${pushError.message}`); | |
| return; | |
| } | |
| // Create PR description | |
| const testListFormatted = testNames.map(t => `- \`${t}\``).join('\n'); | |
| const issueRef = issueUrl ? `\n\nRelated issue: ${issueUrl}` : ''; | |
| const triggerRef = `\n\nTriggered by: #${issue_number} (comment by @${comment_user})`; | |
| const attributeType = mode === 'quarantine' ? 'QuarantinedTest' : 'ActiveIssue'; | |
| const prBody = `## ${actionVerb} Test(s) | |
| This PR was automatically generated to ${actionVerb.toLowerCase()} the following test(s) using the \`${attributeType}\` attribute: | |
| ${testListFormatted} | |
| ${issueRef}${triggerRef} | |
| --- | |
| ⚠️ Please review the changes before merging.`; | |
| // Create PR | |
| const { data: pr } = await github.rest.pulls.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `[automated] ${actionVerb} ${testList}`, | |
| body: prBody, | |
| head: branchName, | |
| base: 'main' | |
| }); | |
| prUrl = pr.html_url; | |
| prNumber = pr.number; | |
| console.log(`Created PR #${prNumber}: ${prUrl}`); | |
| // Add labels | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: ['area-codeflow'] | |
| }); | |
| } catch (labelError) { | |
| console.log(`Note: Could not add labels: ${labelError.message}`); | |
| } | |
| } | |
| // Post success comment | |
| const action = createdNewPr ? 'Created' : 'Updated'; | |
| const successBody = `✅ ${actionVerb} operation completed! ${action} PR #${prNumber}: ${prUrl}`; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: successBody | |
| }); | |
| // Add success reaction to the command comment | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: 'rocket' | |
| }); | |
| - name: Post no-changes comment | |
| if: steps.check-changes.outputs.has_changes == 'false' | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ACTION_VERB: ${{ steps.extract-command.outputs.action_verb }} | |
| TARGET_PR_NUMBER: ${{ steps.determine-target.outputs.target_pr_number }} | |
| TARGET_PR_URL: ${{ steps.determine-target.outputs.pr_url }} | |
| with: | |
| script: | | |
| const actionVerb = process.env.ACTION_VERB; | |
| const targetPrNumber = process.env.TARGET_PR_NUMBER || ''; | |
| const targetPrUrl = process.env.TARGET_PR_URL || ''; | |
| let body; | |
| if (targetPrNumber && targetPrUrl) { | |
| body = `ℹ️ No changes were needed on PR #${targetPrNumber}. The test(s) may already be in the desired state.`; | |
| } else { | |
| body = `ℹ️ No changes were needed. The test(s) may already be in the desired state.`; | |
| } | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| // Add reaction to indicate completion | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: 'rocket' | |
| }); | |
| - name: Post failure comment | |
| if: failure() | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ACTION_VERB: ${{ steps.extract-command.outputs.action_verb || '' }} | |
| TOOL_OUTPUT: ${{ steps.run-tool.outputs.tool_output || '' }} | |
| EXTRACT_ERROR: ${{ steps.extract-command.outputs.error_message || '' }} | |
| TARGET_ERROR: ${{ steps.determine-target.outputs.error_message || '' }} | |
| with: | |
| script: | | |
| const comment_user = context.payload.comment.user.login; | |
| const workflow_run_url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const actionVerb = process.env.ACTION_VERB || 'Operation'; | |
| const toolOutput = process.env.TOOL_OUTPUT || ''; | |
| const extractError = process.env.EXTRACT_ERROR || ''; | |
| const targetError = process.env.TARGET_ERROR || ''; | |
| let body = `@${comment_user} ❌ ${actionVerb} failed.\n\n`; | |
| // Check for errors from various steps | |
| const errorMessage = extractError || targetError; | |
| if (errorMessage) { | |
| body += `**Error:** ${errorMessage}\n\n`; | |
| } else if (toolOutput) { | |
| // Extract just the error lines (skip build output) | |
| const lines = toolOutput.split('\n'); | |
| const errorLines = lines.filter(line => | |
| line.includes('Error') || | |
| line.includes('error') || | |
| line.includes('No method found') || | |
| line.includes('failed') || | |
| line.includes('Invalid') | |
| ).slice(0, 10); // Limit to 10 error lines | |
| if (errorLines.length > 0) { | |
| body += `**Error:**\n\`\`\`\n${errorLines.join('\n')}\n\`\`\`\n\n`; | |
| } | |
| } | |
| body += `See the [workflow run](${workflow_run_url}) for full details.`; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| - name: Re-lock issue/PR if it was locked | |
| if: github.event.issue.locked == true && (success() || failure()) | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| env: | |
| ORIGINAL_LOCK_REASON: ${{ steps.unlock-issue.outputs.original_lock_reason }} | |
| with: | |
| script: | | |
| // Use the original lock reason, falling back to 'resolved' if not captured | |
| const lockReason = process.env.ORIGINAL_LOCK_REASON || 'resolved'; | |
| console.log(`Re-locking previously locked issue/PR #${context.issue.number} with reason: ${lockReason}.`); | |
| await github.rest.issues.lock({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| lock_reason: lockReason | |
| }); |