fix(auth): make email-verification signup completable #8401
Workflow file for this run
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: Review State | |
| # Event-driven interpreter that maintains four mutually-exclusive review/* | |
| # labels on open PRs so maintainers can see review status at a glance. | |
| # Pure actions/github-script: no checkout, no secrets, no agent. | |
| on: | |
| pull_request_target: | |
| types: [opened, reopened, synchronize, ready_for_review] | |
| pull_request_review: | |
| types: [submitted, dismissed] | |
| schedule: | |
| # Backstop sweep so state stays correct even if an event was missed. | |
| - cron: "15 */6 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| review-state: | |
| name: Update review state labels | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: review-state-${{ github.event.pull_request.number || 'sweep' }} | |
| cancel-in-progress: false | |
| steps: | |
| - name: Apply review state labels | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| // The four mutually-exclusive review states. | |
| const stateLabels = { | |
| 'review/needs-review': { | |
| color: 'fbca04', | |
| description: 'No maintainer or bot review yet', | |
| }, | |
| 'review/awaiting-author': { | |
| color: 'c5def5', | |
| description: 'Reviewed; waiting on the author to respond', | |
| }, | |
| 'review/needs-rereview': { | |
| color: 'd93f0b', | |
| description: 'New commits since the last review', | |
| }, | |
| 'review/approved': { | |
| color: '0e8a16', | |
| description: 'Approved; no new commits since', | |
| }, | |
| }; | |
| const stateNames = Object.keys(stateLabels); | |
| // --- Ensure the four labels exist (mirrors pr-sweep.yml) --- | |
| const existingLabels = new Set(); | |
| for await (const response of github.paginate.iterator( | |
| github.rest.issues.listLabelsForRepo, | |
| { owner, repo, per_page: 100 } | |
| )) { | |
| for (const label of response.data) { | |
| existingLabels.add(label.name); | |
| } | |
| } | |
| for (const [name, meta] of Object.entries(stateLabels)) { | |
| if (!existingLabels.has(name)) { | |
| await github.rest.issues.createLabel({ | |
| owner, | |
| repo, | |
| name, | |
| color: meta.color, | |
| description: meta.description, | |
| }); | |
| } | |
| } | |
| // Remove a label, ignoring 404 if it's already gone (mirrors pr-sweep.yml). | |
| async function safeRemoveLabel(prNumber, name) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| name, | |
| }); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| } | |
| // --- Determine the set of PRs to process --- | |
| let prs; | |
| if ( | |
| context.eventName === 'pull_request_target' || | |
| context.eventName === 'pull_request_review' | |
| ) { | |
| prs = [context.payload.pull_request]; | |
| } else { | |
| // schedule / workflow_dispatch: sweep every open PR. | |
| prs = await github.paginate(github.rest.pulls.list, { | |
| owner, | |
| repo, | |
| state: 'open', | |
| per_page: 100, | |
| }); | |
| } | |
| core.info(`Processing ${prs.length} PR(s) for review state`); | |
| for (const pr of prs) { | |
| if (!pr) continue; | |
| try { | |
| const currentLabels = new Set((pr.labels || []).map(l => l.name)); | |
| // Drafts get no review state: strip any review/* labels and move on. | |
| if (pr.draft === true) { | |
| for (const name of stateNames) { | |
| if (currentLabels.has(name)) { | |
| await safeRemoveLabel(pr.number, name); | |
| } | |
| } | |
| core.info(`#${pr.number}: draft, cleared review state`); | |
| continue; | |
| } | |
| // Skip bot-authored PRs entirely (no labeling). | |
| if (pr.user && pr.user.type === 'Bot') { | |
| core.info(`#${pr.number}: bot author, skipping`); | |
| continue; | |
| } | |
| // --- Qualifying reviews --- | |
| const reviews = await github.paginate(github.rest.pulls.listReviews, { | |
| owner, | |
| repo, | |
| pull_number: pr.number, | |
| per_page: 100, | |
| }); | |
| const qualifying = reviews.filter(review => | |
| review.user && | |
| review.user.login !== pr.user.login && | |
| ( | |
| review.user.type === 'Bot' || | |
| ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(review.author_association) | |
| ) && | |
| ['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED'].includes(review.state) | |
| ); | |
| let lastReview = null; | |
| for (const review of qualifying) { | |
| if ( | |
| !lastReview || | |
| new Date(review.submitted_at) > new Date(lastReview.submitted_at) | |
| ) { | |
| lastReview = review; | |
| } | |
| } | |
| // --- Last non-merge commit --- | |
| // Only NON-merge commits count as new work. Merge commits (parents > 1) | |
| // are "Update branch" / `git merge main`, so syncing a branch with main | |
| // does not flip a reviewed PR back to needs-rereview. | |
| const commits = await github.paginate(github.rest.pulls.listCommits, { | |
| owner, | |
| repo, | |
| pull_number: pr.number, | |
| per_page: 100, | |
| }); | |
| let lastCommitAt; | |
| for (const commit of commits) { | |
| if (commit.parents && commit.parents.length > 1) continue; | |
| const committed = commit.commit?.committer?.date; | |
| if (!committed) continue; | |
| if (!lastCommitAt || new Date(committed) > new Date(lastCommitAt)) { | |
| lastCommitAt = committed; | |
| } | |
| } | |
| // --- Decide the state --- | |
| // lastReview may be a COMMENTED review; approvals should still win unless a later | |
| // decision review (APPROVED/CHANGES_REQUESTED) supersedes them. | |
| let lastDecisionReview = null; | |
| for (const review of qualifying) { | |
| if (review.state === 'COMMENTED') continue; | |
| if ( | |
| !lastDecisionReview || | |
| new Date(review.submitted_at) > new Date(lastDecisionReview.submitted_at) | |
| ) { | |
| lastDecisionReview = review; | |
| } | |
| } | |
| let desired; | |
| if (!lastReview) { | |
| desired = 'review/needs-review'; | |
| } else if ( | |
| lastCommitAt && | |
| new Date(lastCommitAt) > new Date(lastReview.submitted_at) | |
| ) { | |
| desired = 'review/needs-rereview'; | |
| } else if (lastDecisionReview?.state === 'APPROVED') { | |
| desired = 'review/approved'; | |
| } else { | |
| desired = 'review/awaiting-author'; | |
| } | |
| // --- Apply: add the chosen label, remove the other three --- | |
| if (!currentLabels.has(desired)) { | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: pr.number, | |
| labels: [desired], | |
| }); | |
| } | |
| for (const name of stateNames) { | |
| if (name !== desired && currentLabels.has(name)) { | |
| await safeRemoveLabel(pr.number, name); | |
| } | |
| } | |
| core.info(`#${pr.number}: ${desired}`); | |
| } catch (e) { | |
| core.warning(`#${pr.number}: failed to update review state: ${e.message}`); | |
| } | |
| } |