| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| name: PR effort label | |
| # Labels a pull request when it first asks for review, then re-checks the whole | |
| # open set once a day. A push changes the diff and so can change the tier, but | |
| # subscribing to every push would run this hundreds of times a day to correct a | |
| # label nobody is misled by in the meantime; the daily sweep absorbs that drift | |
| # and doubles as the recovery path for any event this misses. | |
| on: | |
| pull_request_target: | |
| types: [opened, reopened, ready_for_review] | |
| branches: [main] | |
| schedule: | |
| - cron: "23 4 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: Log the tier each open pull request would get without writing labels. | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: pr-effort-label-${{ github.event.pull_request.number || github.workflow }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request_target' }} | |
| jobs: | |
| label: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| steps: | |
| # pull_request_target runs with a writable token, so the ref is pinned to | |
| # the trusted default-branch commit rather than left to the event's | |
| # default: pull_request_review and friends resolve to refs/pull/N/merge, | |
| # and this job imports the checked-out script. | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.sha }} | |
| persist-credentials: false | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| DRY_RUN: ${{ inputs.dry_run || false }} | |
| with: | |
| script: | | |
| const path = require("node:path") | |
| const { pathToFileURL } = require("node:url") | |
| const { planLabels } = await import( | |
| pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, "scripts/pr-effort.mjs")).href | |
| ) | |
| const { owner, repo } = context.repo | |
| // Reviewing a tier boundary or an exclusion means seeing it against | |
| // real pull requests, which is this same sweep minus the writes. | |
| const dryRun = process.env.DRY_RUN === "true" | |
| const targets = context.payload.pull_request | |
| ? [context.payload.pull_request.number] | |
| : ( | |
| await github.paginate(github.rest.pulls.list, { | |
| owner, | |
| repo, | |
| state: "open", | |
| per_page: 100, | |
| }) | |
| ).map((pull) => pull.number) | |
| for (const pull_number of targets) { | |
| const [files, current] = await Promise.all([ | |
| github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }), | |
| github.paginate(github.rest.issues.listLabelsOnIssue, { | |
| owner, | |
| repo, | |
| issue_number: pull_number, | |
| per_page: 100, | |
| }), | |
| ]) | |
| const plan = planLabels( | |
| files, | |
| current.map((label) => label.name), | |
| ) | |
| if (dryRun) { | |
| core.info( | |
| `#${pull_number}: ${plan.label} (${plan.lines} readable lines)` + | |
| ` +[${plan.addLabels.join(", ")}] -[${plan.removeLabels.join(", ")}]`, | |
| ) | |
| continue | |
| } | |
| if (plan.addLabels.length > 0) { | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: pull_number, | |
| labels: plan.addLabels, | |
| }) | |
| } | |
| for (const name of plan.removeLabels) { | |
| try { | |
| await github.rest.issues.removeLabel({ owner, repo, issue_number: pull_number, name }) | |
| } catch (error) { | |
| // Another run may have removed it first; anything else is real. | |
| if (error.status !== 404) throw error | |
| } | |
| } | |
| core.info(`#${pull_number}: ${plan.label} (${plan.lines} readable lines)`) | |
| } |