-
-
Notifications
You must be signed in to change notification settings - Fork 36.4k
tools: add more automation for labeling #65059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
avivkeller
wants to merge
2
commits into
nodejs:main
Choose a base branch
from
avivkeller:automate-labels
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| name: Author ready | ||
|
|
||
| # Adds `author ready` to pull requests that qualify | ||
| on: | ||
| schedule: | ||
| - cron: 25,55 * * * * | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| author-ready: | ||
| if: github.repository == 'nodejs/node' | ||
| runs-on: ubuntu-slim | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| steps: | ||
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| persist-credentials: false | ||
| sparse-checkout: | | ||
| /tools/actions/author-ready.sh | ||
| /tools/actions/pr-state.sh | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Collect pull requests to evaluate | ||
| id: collect | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| # Pull requests that might have become ready, plus the ones already | ||
| # labelled so the label comes back off when it stops holding. | ||
| candidates=$(gh pr list \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --json 'number' \ | ||
| --search 'review:approved draft:false -label:blocked -label:wip -label:"author ready"' \ | ||
| -t '{{ range . }}{{ .number }} {{ end }}' \ | ||
| --limit 200) | ||
| labelled=$(gh pr list \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --label 'author ready' \ | ||
| --json 'number' \ | ||
| -t '{{ range . }}{{ .number }} {{ end }}' \ | ||
| --limit 200) | ||
| numbers=$(printf '%s %s' "$candidates" "$labelled" | | ||
| tr ' ' '\n' | grep -v '^$' | sort -un | tr '\n' ' ') | ||
| echo "numbers=$numbers" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Add the label where it applies | ||
| if: steps.collect.outputs.numbers != '' | ||
| run: ./tools/actions/author-ready.sh ${{ steps.collect.outputs.numbers }} | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| GH_REPO: ${{ github.repository }} |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| name: Auto Commit Queue | ||
|
|
||
| # `author ready` says a pull request is just about ready to land. | ||
| # This runs a bit more aggressively than adding the label, and if | ||
| # a PR passes, adds it to the queue. | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: 10,40 * * * * | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| queue-ready-prs: | ||
| if: github.repository == 'nodejs/node' | ||
| runs-on: ubuntu-slim | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| steps: | ||
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| persist-credentials: false | ||
| sparse-checkout: | | ||
| /README.md | ||
| /tools/actions/auto-commit-queue.sh | ||
| /tools/actions/pr-state.sh | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Queue pull requests that are ready to land | ||
| run: ./tools/actions/auto-commit-queue.sh | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| GH_REPO: ${{ github.repository }} |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| #!/bin/sh | ||
|
|
||
| # Keeps the `author ready` label in sync with the definition in | ||
| # doc/contributing/collaborator-guide.md. A pull request is author ready when: | ||
| # | ||
| # * there is a CI run in progress or completed, | ||
| # * there is at least one collaborator approval, | ||
| # * there are no outstanding review comments. | ||
|
|
||
| set -e | ||
|
|
||
| # shellcheck source=tools/actions/pr-state.sh | ||
| . "$(dirname "$0")/pr-state.sh" | ||
|
|
||
| AUTHOR_READY_LABEL="author ready" | ||
|
|
||
| # shellcheck disable=SC2154 | ||
| OWNER="${GH_REPO%%/*}" | ||
| REPO="${GH_REPO#*/}" | ||
|
|
||
| # shellcheck disable=SC2016 # `$owner` and friends are GraphQL variables | ||
| QUERY=' | ||
| query($owner: String!, $repo: String!, $number: Int!) { | ||
| repository(owner: $owner, name: $repo) { | ||
| pullRequest(number: $number) {'"${PR_STATE_FRAGMENT}"'} | ||
| } | ||
| }' | ||
|
|
||
| # shellcheck disable=SC2016 # `$ci_pattern` and friends are jq variables | ||
| ELIGIBLE="${PR_STATE_JQ}"' | ||
| .data.repository.pullRequest | ||
| | { | ||
| missing: [ | ||
| if .state == "OPEN" then empty else "not open" end, | ||
| if .isDraft then "draft" else empty end, | ||
| if .mergeable == "CONFLICTING" then "merge conflict" else empty end, | ||
| (labels[] | select(. == "blocked" or . == "wip") | "labelled \(.)"), | ||
| (labels[] | select(. == "request-ci" or . == "request-ci-failed") | "CI not started yet"), | ||
| if changes_requested then "changes requested" else empty end, | ||
| if unresolved_threads then "unresolved review threads" else empty end, | ||
| if (.latestOpinionatedReviews.nodes | any(.state == "APPROVED")) | ||
| then empty else "not approved" end, | ||
|
|
||
| # "A CI run in progress or completed": for a pull request that needs | ||
| # Jenkins, only a Jenkins run counts; otherwise any check does. | ||
| if (if jenkins_required then jenkins_ran($ci_pattern) else ci_started end) | ||
| then empty else "no CI run" end, | ||
| if ci_failing then "CI is failing" else empty end | ||
| ], | ||
| is_author_ready: has_label($author_ready_label) | ||
| }' | ||
|
|
||
| for pr in "$@"; do | ||
| state="$( | ||
| gh api graphql -f query="${QUERY}" \ | ||
| -f owner="${OWNER}" -f repo="${REPO}" -F number="${pr}" | | ||
| jq -c --arg ci_pattern "${CI_CONTEXT_PATTERN}" \ | ||
| --arg author_ready_label "${AUTHOR_READY_LABEL}" \ | ||
| "${ELIGIBLE}" | ||
| )" | ||
|
|
||
| missing="$(echo "${state}" | jq -r '.missing | join(", ")')" | ||
| is_author_ready="$(echo "${state}" | jq -r '.is_author_ready')" | ||
|
|
||
| if [ -z "${missing}" ] && [ "${is_author_ready}" = "false" ]; then | ||
| echo "#${pr}: author ready, adding label" | ||
| gh pr edit "${pr}" --add-label "${AUTHOR_READY_LABEL}" | ||
| elif [ -n "${missing}" ] && [ "${is_author_ready}" = "true" ]; then | ||
| echo "#${pr}: no longer author ready (${missing}), removing label" | ||
| gh pr edit "${pr}" --remove-label "${AUTHOR_READY_LABEL}" | ||
| elif [ -n "${missing}" ]; then | ||
| echo "#${pr}: not author ready (${missing})" | ||
| else | ||
| echo "#${pr}: author ready, already labelled" | ||
| fi | ||
| done | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| #!/bin/sh | ||
|
|
||
| # Adds `commit-queue` to pull requests that are ready to land, so nobody has to | ||
| # come back once the waiting period is over. | ||
| # | ||
| # * open, not a draft, targets `main`, and `mergeable` is not CONFLICTING | ||
| # * at least one approval given against the commit that would land | ||
| # * semver-major needs two of those approvals to be from TSC voting members | ||
| # * two approvals and 48 hours since it was opened (unless fast-track) | ||
| # * every GitHub check passing | ||
| # * a green Jenkins run when the pull request is labelled `needs-ci` | ||
| # * that CI having run in the last five days, so `main` has not moved on | ||
|
|
||
| set -e | ||
|
|
||
| # shellcheck source=tools/actions/pr-state.sh | ||
| . "$(dirname "$0")/pr-state.sh" | ||
|
|
||
| COMMIT_QUEUE_LABEL="commit-queue" | ||
|
|
||
| TSC_MEMBERS="$( | ||
| sed -n '/^#### TSC voting members$/,/^#### TSC regular members$/ s/^\* \[\([^]]*\)\].*/\1/p' \ | ||
| "$(dirname "$0")/../../README.md" | | ||
| tr '[:upper:]' '[:lower:]' | | ||
| sort -u | | ||
| jq -Rsc 'split("\n") | map(select(length > 0))' | ||
| )" | ||
|
|
||
| # shellcheck disable=SC2154 | ||
| SEARCH="repo:${GH_REPO} is:pr is:open \ | ||
| label:\"author ready\" \ | ||
| -label:blocked -label:wip \ | ||
| -label:${COMMIT_QUEUE_LABEL} -label:${COMMIT_QUEUE_LABEL}-failed \ | ||
| -label:request-ci -label:request-ci-failed" | ||
|
|
||
| # shellcheck disable=SC2016 # `$q` is a GraphQL variable | ||
| QUERY=' | ||
| query($q: String!) { | ||
| search(query: $q, type: ISSUE, first: 50) { | ||
| nodes { | ||
| ... on PullRequest {'"${PR_STATE_FRAGMENT}"'} | ||
| } | ||
| } | ||
| }' | ||
|
|
||
| # shellcheck disable=SC2016 # `$tsc` and friends are jq variables | ||
| READY="${PR_STATE_JQ}"' | ||
| [ .data.search.nodes[] | ||
| | select(. != null) | ||
| | (current_approvals) as $approvals | ||
| | ([$approvals[].author.login | ascii_downcase] | map(select(IN($tsc[]))) | length) as $tsc_approvals | ||
| | (if has_label("semver-major") and ($tsc | length) > 0 then $tsc_approvals >= 2 | ||
| elif has_label("semver-major") then ($approvals | length) >= 2 | ||
| else ($approvals | length) >= 1 end) as $approved | ||
| | (if has_label("fast-track") then ($approvals | length) >= 2 | ||
| elif wait_waived then true | ||
| else (($approvals | length) >= 2 and age_seconds >= $wait_multi) | ||
| or age_seconds >= $wait_single | ||
| end) as $waited | ||
| | select( | ||
| $approved and $waited | ||
| and .state == "OPEN" | ||
| and (.isDraft | not) | ||
| and .baseRefName == "main" | ||
| and .mergeable != "CONFLICTING" | ||
| and (changes_requested | not) | ||
| and (unresolved_threads | not) | ||
| and github_checks_passing | ||
| and ((jenkins_required | not) or jenkins_passing($ci_pattern)) | ||
| and ci_recent($ci_pattern; $ci_max_age) | ||
| ) | ||
| | .number | ||
| ]' | ||
|
|
||
| numbers="$( | ||
| gh api graphql -f query="${QUERY}" -f q="${SEARCH}" | | ||
| jq -r --arg ci_pattern "${CI_CONTEXT_PATTERN}" \ | ||
| --argjson tsc "${TSC_MEMBERS:-[]}" \ | ||
| --argjson wait_multi "${WAIT_MULTI_APPROVAL}" \ | ||
| --argjson wait_single "${WAIT_SINGLE_APPROVAL}" \ | ||
| --argjson ci_max_age "${CI_MAX_AGE}" \ | ||
| "${READY} | .[]" | ||
| )" | ||
|
|
||
| if [ -z "${numbers}" ]; then | ||
| echo "No pull requests are ready for the commit queue." | ||
| exit 0 | ||
| fi | ||
|
|
||
| for pr in ${numbers}; do | ||
| echo "#${pr}: ready to land, adding ${COMMIT_QUEUE_LABEL}" | ||
| gh pr edit "${pr}" --add-label "${COMMIT_QUEUE_LABEL}" | ||
| done |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| #!/bin/sh | ||
| # shellcheck disable=SC2034 # everything here is read by the scripts that source it | ||
|
|
||
| CI_CONTEXT_PATTERN="${CI_CONTEXT_PATTERN:-node-test-pull-request}" | ||
| WAIT_MULTI_APPROVAL="${WAIT_MULTI_APPROVAL:-172800}" | ||
| WAIT_SINGLE_APPROVAL="${WAIT_SINGLE_APPROVAL:-604800}" | ||
| CI_MAX_AGE="${CI_MAX_AGE:-432000}" | ||
|
|
||
| PR_STATE_FRAGMENT=' | ||
| number | ||
| state | ||
| isDraft | ||
| mergeable | ||
| createdAt | ||
| baseRefName | ||
| labels(first: 100) { nodes { name } } | ||
| latestOpinionatedReviews(first: 100, writersOnly: true) { | ||
| nodes { state author { login } commit { oid } } | ||
| } | ||
| reviewThreads(first: 100) { | ||
| nodes { isResolved isOutdated } | ||
| } | ||
| commits(last: 1) { | ||
| nodes { | ||
| commit { | ||
| oid | ||
| statusCheckRollup { | ||
| contexts(first: 100) { | ||
| nodes { | ||
| ... on StatusContext { context state createdAt } | ||
| ... on CheckRun { name conclusion completedAt } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ' | ||
|
|
||
| # shellcheck disable=SC2016 # `$name` and friends are jq parameters | ||
| PR_STATE_JQ=' | ||
| def labels: .labels.nodes | map(.name); | ||
| def has_label($name): labels | index($name) != null; | ||
| def changes_requested: .latestOpinionatedReviews.nodes | any(.state == "CHANGES_REQUESTED"); | ||
| def unresolved_threads: .reviewThreads.nodes | any((.isResolved | not) and (.isOutdated | not)); | ||
| def head_commit: .commits.nodes[0].commit; | ||
|
|
||
| # Only approvals given against the commit that would actually land count | ||
| def current_approvals: | ||
| # Bound before the map, where `.` is still the pull request rather than a review. | ||
| head_commit.oid as $oid | ||
| | .latestOpinionatedReviews.nodes | ||
| | map(select(.state == "APPROVED" and .commit.oid == $oid)); | ||
|
|
||
| def check_results: (head_commit.statusCheckRollup.contexts.nodes // []) | map(.conclusion // .state); | ||
| def ci_started: (check_results | length) > 0; | ||
| def ci_failing: | ||
| check_results | ||
| | any(IN("FAILURE", "ERROR", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STARTUP_FAILURE")); | ||
| # PRChecker treats neutral and skipped checks as passing. | ||
| def github_checks_passing: ci_started and (check_results | all(IN("SUCCESS", "NEUTRAL", "SKIPPED"))); | ||
|
|
||
| def jenkins_contexts($pattern): | ||
| (head_commit.statusCheckRollup.contexts.nodes // []) | ||
| | map(select((.context // .name) | test($pattern; "i"))); | ||
| def jenkins_ran($pattern): (jenkins_contexts($pattern) | length) > 0; | ||
| def jenkins_passing($pattern): | ||
| jenkins_ran($pattern) and (jenkins_contexts($pattern) | all((.conclusion // .state) == "SUCCESS")); | ||
| def jenkins_required: has_label("needs-ci"); | ||
|
|
||
| def relevant_check_times($pattern): | ||
| (if jenkins_required | ||
| then jenkins_contexts($pattern) | ||
| else (head_commit.statusCheckRollup.contexts.nodes // []) end) | ||
| | map(.completedAt // .createdAt) | ||
| | map(select(. != null) | fromdateiso8601); | ||
| def ci_recent($pattern; $max_age): | ||
| relevant_check_times($pattern) | (length > 0) and ((now - max) <= $max_age); | ||
|
|
||
| def age_seconds: now - (.createdAt | fromdateiso8601); | ||
| def wait_waived: has_label("fast-track"); | ||
| ' |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review comments can be plain comments. This automation only counts GitHub inline code review comments (only these comments have a "resolve" button).