]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
releng: refactor audit workflow routing and decouple state management 70144/head
authorPatrick Donnelly <pdonnell@ibm.com>
Sun, 12 Jul 2026 18:54:28 +0000 (14:54 -0400)
committerPatrick Donnelly <pdonnell@ibm.com>
Mon, 13 Jul 2026 17:17:25 +0000 (13:17 -0400)
Previously, workflow routing and PR state management were split between
the GitHub Actions workflow and the underlying PTL script, leading to
stale branch protection states, API pagination bugs, and blocked merges
during manual overrides.

This refactors the workflow into a centralized state machine and decouples
status reporting from the audit tool:

* Decouple State Management: Remove `--audit-label` from `ptl-tool.py` and
  move all commit status and label transitions (`releng-audit-pass`,
  `releng-audit-fail`) into a dedicated workflow reporter step.
* Non-Blocking Review Comments: Update `ptl-tool.py` in CI mode to post
  audit findings as `COMMENT` events rather than `REQUEST_CHANGES`,
  preventing stale review states from blocking PRs after an override.
* Add Step Summaries: Implement `write_ci_summary()` in `ptl-tool.py` to
  output rich Markdown diagnostic reports directly to `GITHUB_STEP_SUMMARY`.
* Atomic Override Handling: Add `setOverrideStatus()` and `executeOverride()`
  to push an immediate `success` commit status when `/audit override` or the
  override label is applied, unblocking required branch protection checks.
* Safe SHA & Label Lookup: Replace the unpaginated Bash `curl` request with
  a reusable `getPrSha()` helper and evaluate overrides directly within the
  JavaScript router.
* Handle Cancellations Safely: Map `cancelled` workflow outcomes to an
  `error` commit status rather than defaulting to `failure`.

Signed-off-by: Patrick Donnelly <pdonnell@ibm.com>
.github/workflows/releng-audit.yaml
src/script/ptl-tool.py

index 205e437b1b7680f8850dfe4fd289f0f69aed781e..97c83980a290cc49e6dbeb99e19f964a0d72ee04 100644 (file)
@@ -17,15 +17,16 @@ concurrency:
 
 jobs:
   audit:
-    name: Backport Audit
+    name: Execution and Routing
     runs-on: ubuntu-latest
     permissions:
+      contents: read
       pull-requests: write
       issues: write
       statuses: write
     steps:
       - id: router
-        name: Evaluate Workflow Routing & Overrides
+        name: Evaluate Workflow Routing
         uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
         env:
           ORG_TOKEN: ${{ secrets.ORG_READ_PAT }}
@@ -35,63 +36,192 @@ jobs:
             const payload = context.payload;
             const actor = context.actor;
             const isBot = actor === 'github-actions[bot]' || actor === 'github-actions';
+            const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
             
+            // Safely removes a label and logs execution, ignoring 404s if label is not attached
+            async function removeLabelSafely(labelName) {
+              try {
+                core.info(`[Router] Attempting to remove label '${labelName}'...`);
+                await github.rest.issues.removeLabel({
+                  owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: labelName
+                });
+                core.info(`[Router] Successfully removed label '${labelName}'.`);
+              } catch (e) {
+                if (e.status === 404) {
+                  core.info(`[Router] Label '${labelName}' was not present on PR (404 ignored).`);
+                } else {
+                  core.warning(`[Router] Failed to remove label '${labelName}': ${e.message}`);
+                }
+              }
+            }
+
+            // Verifies if the user has maintainer/admin collaborator permissions or is an active
+            // member of the ceph-release-manager team. Required for /audit override and test-branch.
             async function checkAuthorization(username) {
+              core.info(`[Router] Checking collaborator permission level for @${username}...`);
               let authorized = false;
               try {
                 const { data: permData } = await github.rest.repos.getCollaboratorPermissionLevel({
                   owner: context.repo.owner, repo: context.repo.repo, username: username
                 });
-                authorized = (permData.permission === 'admin' || permData.permission === 'maintain')
+                core.info(`[Router] Collaborator permission level for @${username}: ${permData.permission}`);
+                authorized = (permData.permission === 'admin' || permData.permission === 'maintain');
               } catch (e) { 
                 core.info(`[Router] Failed to fetch repo permissions: ${e.message}`); 
               }
 
               if (!authorized && context.repo.owner === 'ceph' && process.env.ORG_TOKEN) {
+                core.info(`[Router] @${username} does not have maintain/admin repo rights. Checking ceph-release-manager team membership...`);
                 try {
-                  const orgOctokit = getOctokit(process.env.ORG_TOKEN);
+                  const orgOctokit = github.getOctokit(process.env.ORG_TOKEN);
                   const { data: teamData } = await orgOctokit.rest.teams.getMembershipForUserInOrg({
                     org: 'ceph', team_slug: 'ceph-release-manager', username: username
                   });
+                  core.info(`[Router] Org team membership state for @${username}: ${teamData.state}`);
                   authorized = (teamData.state === 'active');
                 } catch (e) { 
                   core.info(`[Router] Failed to fetch org team membership: ${e.message}`); 
                 }
               }
+              core.info(`[Router] Authorization result for @${username}: ${authorized ? 'AUTHORIZED' : 'NOT AUTHORIZED'}`);
               return authorized;
             }
 
-            async function triggerAuditRun(description) {
-              try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-fail' }); } catch (e) {}
-              try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-pass' }); } catch (e) {}
+            // Retrieves the HEAD SHA of the pull request. Falls back to an API lookup
+            // when triggered by issue_comment events where the PR object is omitted from payload.
+            async function getPrSha() {
+              let sha = context.payload.pull_request?.head?.sha;
+              if (!sha) {
+                core.info(`[Router] PR SHA not found in payload. Fetching from API for issue #${context.issue.number}...`);
+                try {
+                  const { data: pr } = await github.rest.pulls.get({
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    pull_number: context.issue.number
+                  });
+                  sha = pr.head.sha;
+                  core.info(`[Router] Successfully retrieved SHA from API: ${sha}`);
+                } catch (e) {
+                  core.error(`[Router] Failed to fetch PR details from API: ${e.message}`);
+                }
+              } else {
+                core.info(`[Router] Retrieved PR SHA from payload: ${sha}`);
+              }
+              if (sha) core.setOutput('pr_sha', sha);
+              return sha;
+            }
+
+            // Pushes a pending commit status to the PR HEAD SHA to provide visual feedback
+            // and block branch protection while the audit executes. For standard audit runs,
+            // strips pass/fail state labels before starting.
+            async function triggerAuditRun(description, statusContext = 'Backport Audit') {
+              core.info(`[Router] Initiating audit run trigger for context '${statusContext}': ${description}`);
+              const isStandard = (statusContext === 'Backport Audit');
+              if (isStandard) {
+                core.info('[Router] Clearing any previous labels before starting standard audit run...');
+                await removeLabelSafely('releng-audit-fail');
+                await removeLabelSafely('releng-audit-override');
+                await removeLabelSafely('releng-audit-pass');
+                await removeLabelSafely('releng-audit-queue');
+              }
+              const sha = await getPrSha();
+              if (!sha) {
+                core.error('[Router] Cannot initiate audit: PR SHA could not be retrieved.');
+                core.setOutput('run_audit', 'false');
+                return;
+              }
+              core.setOutput('status_context', statusContext);
+
+              if (isStandard) {
+                try {
+                  core.info(`[Router] Setting commit status to 'pending' on SHA ${sha}...`);
+                  await github.rest.repos.createCommitStatus({
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    sha: sha,
+                    state: 'pending',
+                    context: statusContext,
+                    description: description,
+                    target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
+                  });
+                  core.info(`[Router] Set pending commit status (${statusContext}) on SHA: ${sha}`);
+                } catch (e) {
+                  core.error(`[Router] Failed to set commit status: ${e.message}`);
+                }
+              } else {
+                core.info(`[Router] Non-standard audit run ('${statusContext}') ā€” skipping pending commit status creation.`);
+              }
+              core.setOutput('run_audit', 'true');
+            }
+
+            // Pushes an immediate success commit status when an authorized override occurs,
+            // unblocking required branch protection rules without executing the audit script.
+            async function setOverrideStatus(actorName) {
+              core.info(`[Router] Setting override success commit status for @${actorName}...`);
               try {
-                const { data: pr } = await github.rest.pulls.get({
-                  owner: context.repo.owner,
-                  repo: context.repo.repo,
-                  pull_number: context.issue.number
-                });
-                await github.rest.repos.createCommitStatus({
-                  owner: context.repo.owner,
-                  repo: context.repo.repo,
-                  sha: pr.head.sha,
-                  state: 'pending',
-                  context: 'Backport Audit',
-                  description: description
-                });
-                core.info(`[Router] Set pending commit status on SHA: ${pr.head.sha}`);
+                const sha = await getPrSha();
+                if (sha) {
+                  await github.rest.repos.createCommitStatus({
+                    owner: context.repo.owner,
+                    repo: context.repo.repo,
+                    sha: sha,
+                    state: 'success',
+                    context: 'Backport Audit',
+                    description: `Audit requirement overridden by @${actorName}`,
+                    target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
+                  });
+                  core.info(`[Router] Set success (override) commit status on SHA: ${sha}`);
+                } else {
+                  core.error('[Router] Cannot set override commit status: SHA is undefined or could not be retrieved.');
+                }
               } catch (e) {
-                core.info(`[Router] Failed to set commit status: ${e.message}`);
+                core.error(`[Router] Failed to set override commit status: ${e.message}`);
+              }
+            }
+
+            // Performs the complete override state transition atomically: applies the override label,
+            // strips pass/fail labels, updates commit status to success, and posts a confirmation comment.
+            async function executeOverride(actorName, addLabel = true, postComment = true) {
+              core.info(`[Router] Executing override lifecycle for @${actorName} (addLabel=${addLabel}, postComment=${postComment})...`);
+              if (addLabel) {
+                try {
+                  core.info('[Router] Applying releng-audit-override label...');
+                  await github.rest.issues.addLabels({
+                    owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, labels: ['releng-audit-override']
+                  });
+                  core.info('[Router] Successfully added releng-audit-override label.');
+                } catch (e) {
+                  core.error(`[Router] Failed to add override label: ${e.message}`);
+                }
+              }
+              core.info('[Router] Stripping pass/fail labels as part of override execution...');
+              await removeLabelSafely('releng-audit-fail');
+              await removeLabelSafely('releng-audit-pass');
+              await setOverrideStatus(actorName);
+              if (postComment) {
+                try {
+                  core.info('[Router] Posting override confirmation comment...');
+                  await github.rest.issues.createComment({
+                    owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
+                    body: `āœ… **Audit Override Applied** by @${actorName}.\n\n[View workflow run](${runUrl})`
+                  });
+                  core.info('[Router] Successfully posted override confirmation comment.');
+                } catch (e) {
+                  core.error(`[Router] Failed to post override comment: ${e.message}`);
+                }
               }
-              core.setOutput('run_audit', 'true');
             }
 
             core.info(`[Router] Evaluating event: ${eventName}, action: ${payload.action || 'N/A'}`);
 
             // ==========================================
-            // 1. HANDLE ISSUE COMMENTS (/audit commands)
+            // 1. HANDLE ISSUE COMMENTS (State Machine Drivers)
             // ==========================================
+            // Issue comments (/audit commands) act strictly as state machine drivers.
+            // For standard commands (/audit retest, /audit override), we only apply labels
+            // here. This routes execution cleanly through the 'labeled' event handler,
+            // ensuring a single source of truth for audit triggers and override state changes.
             if (eventName === 'issue_comment') {
-              core.info('[Router] Processing issue_comment event.');
               if (!payload.issue.pull_request) {
                 core.info('[Router] Comment is not on a pull request. Skipping.');
                 core.setOutput('run_audit', 'false');
@@ -101,240 +231,211 @@ jobs:
               const commentBody = payload.comment.body.trim();
               
               if (commentBody.startsWith('/audit retest')) {
-                core.info('[Router] Detected /audit retest command. Triggering audit.');
-                await triggerAuditRun('Running backport audit retest...');
-                return;
+                // Directly trigger the audit run instead of bouncing through an ephemeral label
+                core.info('[Router] /audit retest detected. Triggering immediate audit execution...');
+                try {
+                  await github.rest.issues.createComment({
+                    owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
+                    body: `šŸ”„ **Audit Retest Triggered** by @${actor}.\n\n[View workflow run](${runUrl})`
+                  });
+                } catch (e) {
+                  core.error(`[Router] Failed to post retest comment: ${e.message}`);
+                }
+                await triggerAuditRun('Running backport audit...');
+              } else if (commentBody.startsWith('/audit override')) {
+                // Execute the full override lifecycle atomically within this run
+                core.info(`[Router] /audit override detected. Validating @${actor}...`);
+                if (await checkAuthorization(actor)) {
+                  core.info(`[Router] @${actor} is authorized. Executing override...`);
+                  await executeOverride(actor, true, true);
+                } else {
+                  core.error(`User @${actor} is not authorized to override audits.`);
+                }
+                core.setOutput('run_audit', 'false');
               } else if (commentBody.startsWith('/audit test-branch')) {
-                core.info(`[Router] Validating if user @${actor} is authorized to activate test branch.`);
-                const isAuthorized = await checkAuthorization(actor);
-                
-                if (isAuthorized) {
+                // Test branch mode: Execute PTL tool from an alternative branch for testing/debugging.
+                // To prevent side effects on PR mergeability, this mode does NOT touch state labels
+                // and will report a success commit status even if issues are found (findings are posted as PR comments).
+                core.info(`[Router] /audit test-branch detected. Validating @${actor}...`);
+                if (await checkAuthorization(actor)) {
                   const parts = commentBody.split(/\s+/);
                   const testBranch = parts[2] || 'testing/releng-audit';
                   
                   try {
-                    await github.rest.repos.getBranch({
-                      owner: context.repo.owner, repo: context.repo.repo, branch: testBranch
-                    });
+                    core.info(`[Router] Validating test branch '${testBranch}' exists...`);
+                    await github.rest.repos.getBranch({ owner: context.repo.owner, repo: context.repo.repo, branch: testBranch });
+                    core.info(`[Router] Test branch '${testBranch}' verified.`);
                   } catch (e) {
-                    core.setFailed(`Test branch '${testBranch}' does not exist in ${context.repo.owner}/${context.repo.repo}.`);
+                    core.error(`Test branch '${testBranch}' does not exist: ${e.message}`);
                     await github.rest.issues.createComment({
                       owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
-                      body: `āŒ **Audit Test Mode Failed**\n\nBranch \`${testBranch}\` was not found in \`${context.repo.owner}/${context.repo.repo}\`. Please push the branch before triggering.`
+                      body: `āŒ **Audit Test Mode Failed**\n\nBranch \`${testBranch}\` was not found in \`${context.repo.owner}/${context.repo.repo}\`.\n\n[View workflow run](${runUrl})`
                     });
                     core.setOutput('run_audit', 'false');
                     return;
                   }
 
-                  core.info(`[Router] User @${actor} is authorized. Activating test mode with branch: ${testBranch}`);
-                  await github.rest.issues.createComment({
-                    owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
-                    body: `🧪 **Audit Test Mode Activated** by @${actor}.\n\nExecuting audit using tooling checked out from branch \`${testBranch}\`.`
-                  });
-                  core.setOutput('checkout_ref', testBranch);
-                  await triggerAuditRun(`Running audit using branch ${testBranch}...`);
-                } else {
-                  core.info(`[Router] User @${actor} NOT authorized to use test branches.`);
-                  core.setFailed(`User @${actor} is not authorized to invoke test branches.`);
-                  core.setOutput('run_audit', 'false');
-                }
-                return;
-              } else if (commentBody.startsWith('/audit override')) {
-                core.info(`[Router] Validating if user @${actor} is authorized to apply override.`);
-                const isAuthorized = await checkAuthorization(actor);
-                
-                if (isAuthorized) {
-                  core.info(`[Router] User @${actor} is authorized. Applying override and stripping fail label.`);
-                  await github.rest.issues.addLabels({
-                    owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, labels: ['releng-audit-override']
-                  });
                   try {
-                    await github.rest.issues.removeLabel({
-                      owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-fail'
+                    core.info('[Router] Posting test mode activation comment...');
+                    await github.rest.issues.createComment({
+                      owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
+                      body: `🧪 **Audit Test Mode Activated** by @${actor}.\n\nExecuting audit using tooling checked out from branch \`${testBranch}\`. *This run will not affect required PR commit statuses or state labels.*\n\n[View workflow run](${runUrl})`
                     });
-                  } catch (e) {}
-                  await github.rest.issues.createComment({
-                    owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
-                    body: `āœ… **Audit Override Applied** by @${actor}.`
-                  });
+                    core.info('[Router] Successfully posted test mode activation comment.');
+                  } catch (e) {
+                    core.error(`[Router] Failed to post test mode comment: ${e.message}`);
+                  }
+
+                  core.setOutput('checkout_ref', testBranch);
+                  await triggerAuditRun(`Running audit using branch ${testBranch}...`, `Audit Test Mode (${testBranch})`);
                 } else {
-                  core.info(`[Router] User @${actor} NOT authorized. Removing override label.`);
-                  core.setFailed(`User @${actor} is not authorized to override audits.`);
+                  core.error(`User @${actor} is not authorized to invoke test branches.`);
+                  core.setOutput('run_audit', 'false');
                 }
+              } else {
+                core.info('[Router] Comment is not a recognized /audit command. Skipping.');
                 core.setOutput('run_audit', 'false');
-                return;
               }
 
-              core.info('[Router] Comment is not an audit command. Skipping.');
-              core.setOutput('run_audit', 'false');
               return;
             }
 
             // ==========================================
-            // 2. HANDLE PR EVENTS
+            // 2. HANDLE PR EVENTS & LABELS
             // ==========================================
             const hasFailLabel = payload.pull_request?.labels.some(l => l.name === 'releng-audit-fail');
             const hasPassLabel = payload.pull_request?.labels.some(l => l.name === 'releng-audit-pass');
             const hasOverrideLabel = payload.pull_request?.labels.some(l => l.name === 'releng-audit-override');
             const hasQueueLabel = payload.pull_request?.labels.some(l => l.name === 'releng-audit-queue');
 
-            core.info(`[Router] Current labels - Fail: ${hasFailLabel}, Pass: ${hasPassLabel}, Override: ${hasOverrideLabel}, Queue: ${hasQueueLabel}`);
+            core.info(`[Router] Current state labels -> Fail: ${hasFailLabel}, Pass: ${hasPassLabel}, Override: ${hasOverrideLabel}, Queue: ${hasQueueLabel}`);
+            core.setOutput('has_override', hasOverrideLabel ? 'true' : 'false');
 
-            // --- SYNCHRONIZE (New Commits) ---
-            if (eventName === 'pull_request_target' && payload.action === 'synchronize') {
-              core.info('[Router] Processing synchronize event (new commits).');
-              if (hasOverrideLabel) {
-                core.info('[Router] PR had override label. Removing it because of new commits.');
-                try {
-                  await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-override' });
-                  await github.rest.issues.createComment({
-                    owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
-                    body: 'āš ļø **Audit Override Removed**\n\nNew commits were pushed to this PR, so the previous `releng-audit-override` has been removed.'
-                  });
-                } catch (e) {
-                  core.info(`[Router] Failed to remove override label: ${e.message}`);
-                }
+            // --- LABELED EVENTS ---
+            if (eventName === 'pull_request_target' && payload.action === 'labeled') {
+              const labelName = payload.label.name;
+              core.info(`[Router] Labeled event triggered for label '${labelName}' by @${actor}.`);
+
+              // The releng-audit-queue label acts as the primary trigger for manual retests
+              if (labelName === 'releng-audit-queue') {
+                core.info('[Router] Queue label detected. Stripping label and triggering audit...');
+                await removeLabelSafely('releng-audit-queue');
+                await triggerAuditRun('Running backport audit...');
+                return;
               }
 
-              if (hasFailLabel) {
-                core.info('[Router] PR is currently in a failed audit state. Halting automated checks until failure label is removed.');
-                core.setFailed("PR is currently in a failed audit state. Remove the releng-audit-fail label to re-run.");
+              // Handle authorized overrides applied via label or /audit override comment
+              if (labelName === 'releng-audit-override') {
+                core.info(`[Router] Evaluating override label application by @${actor}...`);
+                if (!isBot && !(await checkAuthorization(actor))) {
+                  core.error(`[Router] User @${actor} is not authorized to apply releng-audit-override. Removing label...`);
+                  await removeLabelSafely(labelName);
+                  core.error(`User @${actor} is not authorized to override audits.`);
+                } else {
+                  core.info(`[Router] Override authorized for @${actor}. Executing override state transition...`);
+                  // Execute override state transition without re-adding label (addLabel = false).
+                  // Only post a confirmation comment if applied by a human user in the UI (!isBot).
+                  await executeOverride(actor, false, !isBot);
+                }
                 core.setOutput('run_audit', 'false');
                 return;
               }
 
-              if (hasPassLabel) {
-                core.info('[Router] Removing pass label so PR reflects pending state.');
-                try { 
-                  await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-pass' }); 
-                } catch (e) {
-                  core.info(`[Router] Failed to remove pass label: ${e.message}`);
+              // Prevent unauthorized users from manually spoofing machine-owned state labels
+              if (!isBot && (labelName === 'releng-audit-pass' || labelName === 'releng-audit-fail')) {
+                core.warning(`[Router] User @${actor} cannot manually apply machine-owned label '${labelName}'. Removing...`);
+                await removeLabelSafely(labelName);
+                if (!hasOverrideLabel) {
+                  core.info('[Router] No override active. Triggering fresh audit run...');
+                  await triggerAuditRun('Running backport audit...');
+                } else {
+                  core.info('[Router] Override is active. Skipping fresh audit run.');
                 }
+                return;
               }
 
-              core.info('[Router] Triggering audit for new commits.');
-              core.setOutput('run_audit', 'true');
+              core.info(`[Router] Labeled event for '${labelName}' requires no action. Skipping.`);
+              core.setOutput('run_audit', 'false');
               return;
             }
 
-            // --- UNLABELED ---
-            if (eventName === 'pull_request_target' && payload.action === 'unlabeled') {
-              const removedLabel = payload.label.name;
-              core.info(`[Router] Processing unlabeled event for label: ${removedLabel}`);
-              if (['releng-audit-fail', 'releng-audit-pass', 'releng-audit-override'].includes(removedLabel)) {
-                if (isBot) {
-                  core.info(`[Router] Label ${removedLabel} removed by bot. Skipping audit trigger.`);
-                  core.setOutput('run_audit', 'false');
-                  return;
-                }
-                if (hasOverrideLabel && removedLabel === 'releng-audit-fail') {
-                  core.info(`[Router] PR has releng-audit-override but removed releng-audit-fail. Skipping audit.`);
-                  core.setOutput('run_audit', 'false');
-                  return;
+            // --- SYNCHRONIZE (New Commits) ---
+            // When new commits are pushed, existing overrides are revoked and audits re-evaluate.
+            // If the PR is already failed, execution halts to avoid comment spam until re-requested.
+            if (eventName === 'pull_request_target' && payload.action === 'synchronize') {
+              core.info('[Router] Processing synchronize event (new commits pushed)...');
+              if (hasOverrideLabel) {
+                core.info('[Router] PR had active override label. Revoking override due to new commits...');
+                await removeLabelSafely('releng-audit-override');
+                core.setOutput('has_override', 'false');
+                try {
+                  core.info('[Router] Posting override removal notification comment...');
+                  await github.rest.issues.createComment({
+                    owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
+                    body: `āš ļø **Audit Override Removed**\n\nNew commits were pushed to this PR, so the previous override has been removed.\n\n[View workflow run](${runUrl})`
+                  });
+                  core.info('[Router] Override removal notification posted.');
+                } catch (e) {
+                  core.error(`[Router] Failed to post override removal comment: ${e.message}`);
                 }
-                
-                core.info(`[Router] User @${context.actor} removed ${removedLabel}. Stripping other state labels and triggering fresh audit.`);
-                try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-fail' }); } catch (e) {}
-                try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-pass' }); } catch (e) {}
-                core.setOutput('run_audit', 'true');
-                return;
               }
-            }
 
-            // --- LABELED ---
-            if (eventName === 'pull_request_target' && payload.action === 'labeled') {
-              const labelName = payload.label.name;
-              core.info(`[Router] Processing labeled event for label: ${labelName} by actor: ${actor}`);
-              
-              if (labelName === 'releng-audit-queue') {
-                core.info(`[Router] Detected releng-audit-queue label. Removing label and triggering audit.`);
-                try {
-                  await github.rest.issues.removeLabel({
+              if (hasFailLabel) {
+                core.warning("[Router] PR is currently in a failed audit state. Halting automated execution on synchronize.");
+                core.info("PR is currently in a failed audit state. Remove the releng-audit-fail label or comment /audit retest to re-run.");
+                const sha = await getPrSha();
+                if (sha) {
+                  core.info(`[Router] Setting commit status to 'failure' on new SHA ${sha}...`);
+                  await github.rest.repos.createCommitStatus({
                     owner: context.repo.owner,
                     repo: context.repo.repo,
-                    issue_number: context.issue.number,
-                    name: 'releng-audit-queue'
+                    sha: sha,
+                    state: 'failure',
+                    context: 'Backport Audit',
+                    description: 'PR is in a failed audit state. Remove releng-audit-fail label or comment /audit retest to re-run.',
+                    target_url: runUrl
                   });
-                } catch (e) {}
-                core.setOutput('run_audit', 'true');
+                }
+                core.setOutput('run_audit', 'false');
                 return;
               }
 
-              // Block humans from applying machine labels
-              if (!isBot && (labelName === 'releng-audit-pass' || labelName === 'releng-audit-fail')) {
-                core.warning(`[Router] User @${actor} cannot manually apply ${labelName}.`);
-                try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-fail' }); } catch (e) {}
-                try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-pass' }); } catch (e) {}
+              await triggerAuditRun('Running backport audit...');
+              return;
+            }
 
-                if (hasOverrideLabel) {
-                  core.info(`[Router] override label applied, skipping audit.`);
+            // --- UNLABELED ---
+            // If a user manually strips a status label, trigger a fresh audit evaluation
+            if (eventName === 'pull_request_target' && payload.action === 'unlabeled') {
+              const removed = payload.label.name;
+              core.info(`[Router] Processing unlabeled event: label '${removed}' was removed by @${actor}.`);
+              if (['releng-audit-fail', 'releng-audit-pass', 'releng-audit-override'].includes(removed) && !isBot) {
+                if (hasOverrideLabel && removed === 'releng-audit-fail') {
+                  core.info('[Router] PR has active override label; ignoring manual removal of releng-audit-fail.');
                   core.setOutput('run_audit', 'false');
-                } else {
-                  core.info(`[Router] forcing audit.`);
-                  core.setOutput('run_audit', 'true');
+                  return;
                 }
+                core.info(`[Router] Status label '${removed}' removed by human. Triggering fresh audit evaluation...`);
+                await triggerAuditRun('Running backport audit...');
                 return;
-              }
-
-              // Enforce permissions for manual override label application
-              if (labelName === 'releng-audit-override') {
-                if (!isBot) {
-                  core.info(`[Router] Validating if user @${actor} is authorized to apply override.`);
-                  const isAuthorized = await checkAuthorization(actor);
-
-                  if (!isAuthorized) {
-                    core.info(`[Router] User @${actor} NOT authorized. Removing override label.`);
-                    try {
-                      await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: labelName });
-                    } catch (e) {
-                      core.info(`[Router] Label removal skipped or already deleted: ${e.message}`);
-                    }
-                    core.setFailed(`User @${actor} is not authorized to override audits.`);
-                  } else {
-                    core.info(`[Router] User @${actor} is authorized. Stripping fail/pass labels to visually unblock PR.`);
-                    try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-fail' }); } catch (e) {}
-                    try { await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-pass' }); } catch (e) {}
-                  }
-                } else {
-                  core.info(`[Router] Bot applied ${labelName}. Permitted.`);
-                }
-              }
-              
-              if (hasFailLabel && labelName !== 'releng-audit-override') {
-                core.setFailed("PR is currently in a failed audit state. Remove the releng-audit-fail label to re-run.");
               } else {
-                core.info(`[Router] Labeled event handled without triggering audit.`);
+                core.info(`[Router] Unlabeled event for '${removed}' requires no action.`);
               }
-              core.setOutput('run_audit', 'false');
-              return;
             }
 
             // --- OPENED / REOPENED ---
             if (eventName === 'pull_request_target' && (payload.action === 'opened' || payload.action === 'reopened')) {
-              core.info(`[Router] PR ${payload.action}. Triggering audit.`);
-              
+              core.info(`[Router] Processing PR ${payload.action} event...`);
               if (payload.action === 'reopened' && hasOverrideLabel) {
-                core.info('[Router] PR had override label. Removing it on reopen.');
-                try {
-                  await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'releng-audit-override' });
-                  await github.rest.issues.createComment({
-                    owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
-                    body: 'āš ļø **Audit Override Removed**\n\nThis PR was reopened, so the previous `releng-audit-override` has been removed.'
-                  });
-                } catch (e) {
-                  core.info(`[Router] Failed to remove override label: ${e.message}`);
-                }
+                core.info('[Router] PR reopened with previous override label. Removing stale override...');
+                await removeLabelSafely('releng-audit-override');
+                core.setOutput('has_override', 'false');
               }
-
-              core.setOutput('run_audit', 'true');
+              await triggerAuditRun('Running backport audit...');
               return;
             }
 
-            if (hasFailLabel && eventName === 'pull_request_target') {
-              core.setFailed("PR is currently in a failed audit state. Remove the releng-audit-fail label to re-run.");
-            } else {
-              core.info('[Router] Event did not match any trigger criteria. Skipping audit.');
-            }
+            core.info('[Router] Event did not match any active execution triggers. Skipping audit.');
             core.setOutput('run_audit', 'false');
 
       - name: Checkout Trusted Base Repository
@@ -361,23 +462,81 @@ jobs:
         if: steps.router.outputs.run_audit == 'true'
         run: pip install GitPython python-redmine requests
 
-      - name: Run PTL Audit
+      - id: ptl_audit
+        name: Run PTL Audit
         if: steps.router.outputs.run_audit == 'true'
         env:
           PTL_TOOL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           PTL_TOOL_REDMINE_API_KEY: ${{ secrets.REDMINE_API_KEY_BACKPORT_BOT }}
           PTL_TOOL_BASE_PROJECT: ${{ github.repository_owner }}
           PTL_TOOL_BASE_REPO: ${{ github.event.repository.name }}
+          PTL_TOOL_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
         run: |
-          PR_NUMBER="${{ github.event.pull_request.number || github.event.issue.number }}"
-          echo "Fetching latest labels for PR $PR_NUMBER to check for late-applied overrides..."
-          LABELS=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-            "https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/labels" | jq -r '.[].name')
-          
-          if echo "$LABELS" | grep -q "releng-audit-override"; then
-            echo "PR has releng-audit-override. Skipping audit execution to prevent reapplying releng-audit-fail."
-            exit 0
-          fi
-
-          python src/script/ptl-tool.py --debug --ci-mode --audit $PR_NUMBER
+          python src/script/ptl-tool.py --debug --ci-mode --audit
+
+      - name: Report Audit Status & Update State Labels
+        if: always() && steps.router.outputs.run_audit == 'true' && steps.router.outputs.pr_sha != ''
+        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+        with:
+          script: |
+            const statusContext = '${{ steps.router.outputs.status_context }}' || 'Backport Audit';
+            if (statusContext !== 'Backport Audit') {
+              core.info(`[Reporter] Non-standard audit run ('${statusContext}') ā€” skipping state labels and commit status reporting.`);
+              return;
+            }
+
+            const outcome = '${{ steps.ptl_audit.outcome }}';
+            const eventName = context.eventName;
+            core.info(`[Reporter] Evaluating final audit outcome: '${outcome}' for standard audit run (event: ${eventName})...`);
+
+            let state = 'failure';
+            let description = 'Backport audit failed. See review comments or workflow logs.';
 
+            if (outcome === 'success') {
+              state = 'success';
+              description = 'Backport audit completed successfully.';
+              core.info('[Reporter] Audit completed successfully. Applying releng-audit-pass label...');
+              try {
+                await github.rest.issues.addLabels({
+                  owner: context.repo.owner, repo: context.repo.repo,
+                  issue_number: context.issue.number || context.payload.pull_request.number,
+                  labels: ['releng-audit-pass']
+                });
+                core.info('[Reporter] Successfully applied releng-audit-pass label.');
+              } catch (e) {
+                core.error(`[Reporter] Failed to add pass label: ${e.message}`);
+              }
+            } else if (outcome === 'cancelled') {
+              state = 'error';
+              description = 'Backport audit execution was cancelled.';
+              core.warning('[Reporter] Audit execution was cancelled.');
+            } else {
+              // outcome === 'failure'
+              core.info('[Reporter] Audit detected failures. Applying releng-audit-fail label...');
+              try {
+                await github.rest.issues.addLabels({
+                  owner: context.repo.owner, repo: context.repo.repo,
+                  issue_number: context.issue.number || context.payload.pull_request.number,
+                  labels: ['releng-audit-fail']
+                });
+                core.info('[Reporter] Successfully applied releng-audit-fail label.');
+              } catch (e) {
+                core.error(`[Reporter] Failed to add fail label: ${e.message}`);
+              }
+            }
+
+            try {
+              core.info(`[Reporter] Creating final commit status '${state}' for context '${statusContext}' on SHA '${{ steps.router.outputs.pr_sha }}'...`);
+              await github.rest.repos.createCommitStatus({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                sha: '${{ steps.router.outputs.pr_sha }}',
+                state: state,
+                context: statusContext,
+                description: description,
+                target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
+              });
+              core.info(`[Reporter] Set ${state} commit status (${statusContext}) on SHA: ${{ steps.router.outputs.pr_sha }}`);
+            } catch (e) {
+              core.error(`[Reporter] Failed to set final commit status: ${e.message}`);
+            }
index ec72b3c1a0cf3c785757e68d0c37dd983f195fe5..e839bd0ee992224525eda39ccd2c58f030f39c92 100755 (executable)
@@ -30,6 +30,7 @@
 #  - PTL_TOOL_GITHUB_TOKEN (your github Personal access token, or what is stored in ~/.github_token)
 #  - PTL_TOOL_REDMINE_API_KEY (your redmine api key, or what is stored in ~/redmine_key)
 #  - PTL_TOOL_USER (your desired username embedded in test branch names)
+#  - PTL_TOOL_PR_NUMBER (the PR number to audit/merge when running in CI mode)
 
 import argparse
 from dataclasses import dataclass, field
@@ -162,12 +163,6 @@ NEW_CONTRIBUTORS = {}
 BZ_MATCH = re.compile("(.*https?://bugzilla.redhat.com/.*)")
 TRACKER_MATCH = re.compile("(.*https?://tracker.ceph.com/.*)")
 
-@dataclass
-class AuditLabels:
-    queue: str = None
-    passed: str = None
-    failed: str = None
-
 
 class SkipToMerge(Exception):
     """Raised to bypass remaining verification checks and proceed directly to merge."""
@@ -277,8 +272,9 @@ class AuditReport:
 
     def _hide_previous_bot_reviews(self, session: requests.Session, pr: int, dry_run: bool = False):
         """
-        Fetches previous reviews on the PR and hides older audit reports by dismissing
-        REQUEST_CHANGES reviews and minimizing comment threads as OUTDATED via GraphQL.
+        Fetches previous reviews on the PR and cleans up older automated audit reports.
+        To maintain clean review threads during iterative backport work, older reports are
+        dismissed via REST API and minimized as OUTDATED via GraphQL.
         """
         endpoint = f"https://api.github.com/repos/{BASE_PROJECT}/{BASE_REPO}/pulls/{pr}/reviews"
         try:
@@ -331,48 +327,35 @@ class AuditReport:
 
     def post_consolidated_review(self, session: requests.Session, pr: int, dry_run: bool = False, ci_mode: bool = False):
         """
-        Combines all collected md_text in self.issues into a single 
-        GitHub review payload and posts it via the API.
+        Combines all collected md_text in self.issues into a single GitHub review payload
+        and posts it via the API.
+
+        In CI mode, we post review findings as COMMENT events rather than REQUEST_CHANGES.
+        The GitHub commit status and state labels (managed by the workflow router)
+        serve as the formal gating mechanisms, avoiding redundant review blocking states.
         """
         consolidated_text = self.get_consolidated_text()
         if consolidated_text:
             if ci_mode:
                 footer = "\n\n---\n\nāš ļø **Note**: Automated audit checks will be suspended on future pushes to prevent comment spam while you work.\n\nWhen you are ready for a new audit, please **remove the `releng-audit-fail` label** or comment `/audit retest`."
-                
-                if os.getenv("GITHUB_ACTIONS") == "true":
-                    gh_server = os.getenv("GITHUB_SERVER_URL", "https://github.com")
-                    gh_repo = os.getenv("GITHUB_REPOSITORY", f"{BASE_PROJECT}/{BASE_REPO}")
-                    gh_run_id = os.getenv("GITHUB_RUN_ID", "nil")
-                    footer += f"\n\n**CI Run Log**: [View Workflow Details]({gh_server}/{gh_repo}/actions/runs/{gh_run_id})"
-
                 consolidated_text += footer
 
                 self._hide_previous_bot_reviews(session, pr, dry_run=dry_run)
 
+            consolidated_text = append_workflow_link(consolidated_text)
+
             if dry_run:
                 log.info(f"[DRY RUN] Would post consolidated review to PR #{pr}:\n{consolidated_text}")
             else:
                 payload = {'body': consolidated_text, 'event': 'REQUEST_CHANGES'}
                 if ci_mode:
-                    # The CI check failure is sufficient to block merge.
+                    # In CI mode, the GitHub commit status failure is sufficient to block branch merging.
+                    # Posting as COMMENT prevents stale REQUEST_CHANGES reviews from blocking after override.
                     payload['event'] = 'COMMENT'
 
                 endpoint = f"https://api.github.com/repos/{BASE_PROJECT}/{BASE_REPO}/pulls/{pr}/reviews"
                 session.post(endpoint, auth=GithubBearerAuth(), json=payload)
 
-
-def parse_audit_labels(value):
-    if not value:
-        return None
-    parts = [p.strip() for p in value.split(',')]
-    if len(parts) == 1:
-        return AuditLabels(queue=parts[0])
-    if len(parts) == 2:
-        return AuditLabels(passed=parts[0], failed=parts[1])
-    if len(parts) == 3:
-        return AuditLabels(queue=parts[0], passed=parts[1], failed=parts[2])
-    raise argparse.ArgumentTypeError("Audit labels must be 'queue', 'passed,failed', or 'queue,passed,failed'")
-
 class GithubBearerAuth(requests.auth.AuthBase):
     def __call__(self, r):
         if GITHUB_TOKEN:
@@ -380,6 +363,16 @@ class GithubBearerAuth(requests.auth.AuthBase):
         r.headers['Accept'] = 'application/vnd.github.v3+json'
         return r
 
+def append_workflow_link(text: str) -> str:
+    if os.getenv("GITHUB_ACTIONS") == "true":
+        gh_server = os.getenv("GITHUB_SERVER_URL", "https://github.com")
+        gh_repo = os.getenv("GITHUB_REPOSITORY", f"{BASE_PROJECT}/{BASE_REPO}")
+        gh_run_id = os.getenv("GITHUB_RUN_ID", "nil")
+        link_str = f"\n\n[View workflow run]({gh_server}/{gh_repo}/actions/runs/{gh_run_id})"
+        if link_str not in text and "[View workflow run]" not in text and "[View Workflow Details]" not in text:
+            return text + link_str
+    return text
+
 _PR_CACHE = {}
 def get_pr_info(session, pr):
     if pr not in _PR_CACHE:
@@ -1138,7 +1131,12 @@ class ConflictSimulationCheck(BaseAuditCheck):
             for commit in pr_commits:
                 if len(commit.parents) > 1:
                     log.error(f"Commit {commit.hexsha[:8]} is a merge commit. Not allowed.")
-                    sys.exit(1)
+                    if args.ci_mode:
+                        report.add("Invalid Commit Format", f"### Automated PR Review - Merge Commit Not Allowed\n\nCommit `{commit.hexsha[:8]}` is a merge commit. Merge commits are not allowed in backports.")
+                        report.record_failure()
+                        break
+                    else:
+                        sys.exit(1)
                 
                 m = cp_regex.search(commit.message)
                 is_cherry_pick = bool(m)
@@ -1633,6 +1631,28 @@ class MergeConflictCheck(BaseAuditCheck):
                     else:
                         print("Invalid choice. Please enter p, r, m, q, or o.")
 
+def write_ci_summary(pr: int, passed: bool, exit_code: int = 1):
+    if not passed:
+        print(f"::error title=Backport Audit Failed::PTL tool detected parity or conflict issues (exit code {exit_code}). See PR review comments or check this step's logs for details.", file=sys.stderr)
+
+    summary_file = os.getenv("GITHUB_STEP_SUMMARY")
+    if not summary_file:
+        return
+
+    try:
+        with open(summary_file, "a", encoding="utf-8") as f:
+            if passed:
+                f.write("### āœ… Backport Audit Passed\n")
+                f.write(f"All parity and conflict checks completed successfully for PR #{pr}.\n")
+            else:
+                f.write("### āŒ Backport Audit Failed\n")
+                f.write(f"The backport audit script detected issues with PR #{pr} (exit code `{exit_code}`).\n\n")
+                f.write("**How to proceed:**\n")
+                f.write(f"- šŸ’¬ **Review Comments:** Check the automated review feedback posted directly to PR #{pr}.\n")
+                f.write("- šŸ“œ **Detailed Logs:** Expand the **Run PTL Audit** step in the job logs below to view full debug output and parity visualization.\n")
+    except Exception as e:
+        log.warning(f"Failed to write to GITHUB_STEP_SUMMARY: {e}")
+
 def verify_pr_readiness(G, session, R, pr, pr_commits, tip, base, args):
     report = AuditReport()
     ctx = AuditContext(G, session, R, pr, pr_commits, tip, base, args, report)
@@ -1654,21 +1674,31 @@ def verify_pr_readiness(G, session, R, pr, pr_commits, tip, base, args):
             except SkipToMerge:
                 log.info(f"Skipping remaining checks for PR #{pr}.")
                 break
-    except SystemExit:
+    except SystemExit as e:
+        if args.ci_mode:
+            write_ci_summary(pr, passed=False, exit_code=e.code if isinstance(e.code, int) else 1)
         if not args.audit:
             raise
         return False
+    except Exception:
+        if args.ci_mode:
+            write_ci_summary(pr, passed=False, exit_code=1)
+        raise
 
     if report.has_errors():
         log.error(f"Audit failed for PR #{pr}.")
         if args.ci_mode:
             report.post_consolidated_review(session, pr, dry_run=args.dry_run, ci_mode=args.ci_mode)
+            write_ci_summary(pr, passed=False, exit_code=1)
         else:
             consolidated_text = report.get_consolidated_text()
             if consolidated_text:
                 post_draft_review(session, pr, consolidated_text, base=base)
         return False
-        
+
+    if args.ci_mode:
+        report._hide_previous_bot_reviews(session, pr, dry_run=args.dry_run)
+        write_ci_summary(pr, passed=True)
     return True
 
 def manage_qa_tracker(args, R, session, branch, prs, tag, qa_tracker_description, base, created_branch):
@@ -1808,7 +1838,7 @@ def manage_qa_tracker(args, R, session, branch, prs, tag, qa_tracker_description
             log.info(f"QA ticket {issue.url} is private. Skipping GitHub PR updates for added/removed PRs.")
         else:
             for pr in added_prs:
-                body = f"This PR has been added to [{issue.subject}]({issue_url})."
+                body = append_workflow_link(f"This PR has been added to [{issue.subject}]({issue_url}).")
                 if args.dry_run:
                     log.info(f"[DRY RUN] Would post comment to added PR #{pr}: {body}")
                 else:
@@ -1820,7 +1850,7 @@ def manage_qa_tracker(args, R, session, branch, prs, tag, qa_tracker_description
                         log.error(f"Failed to post comment: {r.status_code} {r.text}")
 
             for pr in removed_prs:
-                body = f"This PR has been removed from [{issue.subject}]({issue_url})."
+                body = append_workflow_link(f"This PR has been removed from [{issue.subject}]({issue_url}).")
                 if args.dry_run:
                     log.info(f"[DRY RUN] Would post comment to removed PR #{pr}: {body}")
                 else:
@@ -1851,7 +1881,7 @@ def manage_qa_tracker(args, R, session, branch, prs, tag, qa_tracker_description
             for pr in prs:
                 log.debug(f"Posting QA Run in comment for ={pr}")
                 subject = issue_kwargs['subject']
-                body = f"This PR has been added to [{subject}]({issue_url})."
+                body = append_workflow_link(f"This PR has been added to [{subject}]({issue_url}).")
                 if args.dry_run:
                     log.info(f"[DRY RUN] Would post comment to PR #{pr}: {body}")
                 else:
@@ -2074,29 +2104,6 @@ def build_branch(args):
 
         if args.audit:
             log.info(f"Audit of PR #{pr} {'passed' if audit_passed else 'failed'}. Skipping merge.")
-            audit = args.audit_label
-            if audit:
-                if audit.queue:
-                    if args.dry_run:
-                        log.info(f"[DRY RUN] Would remove label {audit.queue} from PR #{pr}")
-                    else:
-                        req = session.delete(f"https://api.github.com/repos/{BASE_PROJECT}/{BASE_REPO}/issues/{pr}/labels/{audit.queue}", auth=GithubBearerAuth())
-                        if req.status_code in (200, 204):
-                            log.info(f"Removed label {audit.queue} from PR #{pr}")
-                        else:
-                            log.warning(f"Failed to remove label {audit.queue} from PR #{pr}: {req.status_code}")
-
-                target_label = audit.passed if audit_passed else audit.failed
-                if target_label:
-                    if args.dry_run:
-                        log.info(f"[DRY RUN] Would add label {target_label} to PR #{pr}")
-                    else:
-                        req = session.post(f"https://api.github.com/repos/{BASE_PROJECT}/{BASE_REPO}/issues/{pr}/labels", data=json.dumps([target_label]), auth=GithubBearerAuth())
-                        if req.status_code == 200:
-                            log.info(f"Added label {target_label} to PR #{pr}")
-                        else:
-                            raise SystemExit(f"Failed to add label {target_label} to PR #{pr}: {req.status_code}")
-
             # Skip merge
             continue
 
@@ -2310,7 +2317,6 @@ def main():
 
     group = parser.add_argument_group('Backport Verification')
     group.add_argument('--audit', dest='audit', action='store_true', help='run parity and conflict simulations')
-    group.add_argument('--audit-label', dest='audit_label', type=parse_audit_labels, help='swap labels on success/failure. Format: "queue", "passed,failed", or "queue,passed,failed"')
     group.add_argument('--skip-conflict-check', dest='skip_conflict_check', action='store_true', help='skip conflict resolution simulation')
     group.add_argument('--ci-mode', dest='ci_mode', action='store_true', help='run non-interactively and post multiple separate reviews for failures')
 
@@ -2328,6 +2334,18 @@ def main():
 
     args = parser.parse_args(argv)
 
+    if not args.prs and (args.audit or args.ci_mode):
+        pr_env = os.getenv("PTL_TOOL_PR_NUMBER")
+        if pr_env:
+            try:
+                args.prs.append(int(pr_env))
+                log.info(f"Pulled PR #{args.prs[0]} from PTL_TOOL_PR_NUMBER environment variable.")
+            except ValueError:
+                log.error(f"Invalid PR number in PTL_TOOL_PR_NUMBER: {pr_env}")
+                sys.exit(1)
+        else:
+            parser.error("At least one PR number must be specified via CLI arguments or PTL_TOOL_PR_NUMBER when running with --audit or --ci-mode.")
+
     if args.qe_label:
         if args.ci_mode:
             log.error("--qe-label cannot be used with --ci-mode at this time.")
@@ -2335,10 +2353,6 @@ def main():
         args.integration = True
         args.pr_label = args.qe_label
 
-    # Make --audit-label redundant when --ci-mode is invoked
-    if args.ci_mode and not args.audit_label:
-        args.audit_label = parse_audit_labels("releng-audit-pass,releng-audit-fail")
-
     if args.examples:
         examples_text = textwrap.dedent("""
         Ceph PTL Tool - Advanced Examples
@@ -2371,12 +2385,6 @@ def main():
         print(examples_text.strip())
         sys.exit(0)
 
-    if args.audit_label and args.audit_label.queue:
-        if args.pr_label:
-            log.error("--audit-label with a queue label and --pr-label are mutually exclusive")
-            sys.exit(1)
-        args.pr_label = args.audit_label.queue
-
     if args.create_qa and args.update_qa:
         log.error("--create-qa and --update-qa are mutually exclusive switches")
         sys.exit(1)