diff --git a/.github/workflows/companion-pr-check.yml b/.github/workflows/companion-pr-check.yml index 430d7e9431..effdbc8714 100644 --- a/.github/workflows/companion-pr-check.yml +++ b/.github/workflows/companion-pr-check.yml @@ -13,16 +13,13 @@ name: companion-pr-check # surfaces the declared link but reports "couldn't verify". on: + # PR-driven only: runs on the one PR being opened/edited/synced — no periodic + # bulk scan. We assume companions are declared on BOTH sides, so the per-PR + # trigger keeps each side's status fresh; to refresh after a companion merges, + # re-edit the PR (or run this workflow manually via the Actions tab). pull_request: types: [opened, edited, reopened, synchronize] branches: [staging, main] - schedule: - # Refresh open staging/main PRs in case the companion merges AFTER this PR was - # opened. CAVEAT: GitHub runs scheduled workflows ONLY from the DEFAULT branch's - # copy of this file — so this auto-refresh activates once the workflow lands on - # the default branch (via the normal promotion), not before. The pull_request - # triggers below always work; re-editing the PR re-runs the check meanwhile. - - cron: '*/30 * * * *' workflow_dispatch: {} permissions: @@ -49,6 +46,12 @@ jobs: const TRAILER = /Companion:\s*(?:https?:\/\/github\.com\/)?([\w.-]+)\/([\w.-]+)(?:\/pull\/|#)(\d+)/gi; const REF = /(?:https?:\/\/github\.com\/)?([\w.-]+)\/([\w.-]+)(?:\/pull\/|#)(\d+)/g; const { owner, repo } = context.repo; + // Directional label: copilot/mothership PRs get "requires-sim-merge", + // sim PRs get "requires-mothership-merge". Applied whenever the PR + // declares a companion; removed when it declares none. + const otherSide = repo === 'sim' ? 'mothership/copilot' : 'sim'; + const LABEL = repo === 'sim' ? 'requires-mothership-merge' : 'requires-sim-merge'; + const LABEL_DESC = `Has a companion PR on the ${otherSide} side — merge in lockstep`; const crossToken = process.env.CROSS_REPO_TOKEN; // Read the OTHER repo's PR via a plain REST fetch with the PAT in the // header — keeps the PAT strictly READ-ONLY and avoids re-instantiating @@ -108,19 +111,16 @@ jobs: const ex = await findSticky(prNumber); if (ex) await github.rest.issues.deleteComment({ owner, repo, comment_id: ex.id }); // Drop the label too, so a PR edited to remove all companions doesn't - // keep a stale has-companion badge. 404 if not present → ignore. - try { await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: 'has-companion' }); } catch {} + // keep a stale badge. 404 (not present) is expected; surface anything else. + try { await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: LABEL }); } + catch (e) { if (e.status !== 404) core.warning(`companion: removeLabel ${LABEL} on #${prNumber} failed (${e.status || e.message})`); } } async function ensureLabel() { - try { await github.rest.issues.getLabel({ owner, repo, name: 'has-companion' }); } - catch { - try { - await github.rest.issues.createLabel({ - owner, repo, name: 'has-companion', color: '5319e7', - description: 'Has a cross-repo companion PR (see companion-pr-check)', - }); - } catch {} - } + try { await github.rest.issues.getLabel({ owner, repo, name: LABEL }); return; } + catch (e) { if (e.status && e.status !== 404) { core.warning(`companion: getLabel ${LABEL} failed (${e.status})`); return; } } + // 404 → label doesn't exist yet, create it. 422 = another run beat us (fine). + try { await github.rest.issues.createLabel({ owner, repo, name: LABEL, color: 'd93f0b', description: LABEL_DESC }); } + catch (e) { if (e.status !== 422) core.warning(`companion: createLabel ${LABEL} failed (${e.status || e.message})`); } } // staging PRs are a single feature → just this PR's body ("the one"). @@ -162,7 +162,8 @@ jobs: const companions = await collectCompanions(pr); if (companions.length === 0) { await clear(pr.number); return; } await ensureLabel(); - try { await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: ['has-companion'] }); } catch {} + try { await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [LABEL] }); } + catch (e) { core.warning(`companion: addLabels ${LABEL} on #${pr.number} failed (${e.status || e.message})`); } const base = pr.base.ref; const lines = []; @@ -200,6 +201,7 @@ jobs: if (context.eventName === 'pull_request') { await checkPR(context.payload.pull_request); } else { + // workflow_dispatch only: manual full re-scan of open staging/main PRs. for (const b of ['staging', 'main']) { const prs = await github.paginate(github.rest.pulls.list, { owner, repo, base: b, state: 'open', per_page: 100 }); for (const pr of prs) await checkPR(pr);