Ctrl K
ComunidadGuías…

hack/close-manual-backport.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/sh
# Close a pull request opened by hand against a release branch by someone
# outside the team. Fixes reach the release lines through the backport batch
# (docs/release/backports.md), so a hand-made backport is work nobody needs. It
# acts only on an `opened` event: a maintainer keeps a fix that exists only on
# a release line by reopening it.
#
# Usage: close-manual-backport.sh [--dry-run] [EVENT_JSON]
#        close-manual-backport.sh [--dry-run] --pr NUMBER
#
# EVENT_JSON defaults to $GITHUB_EVENT_PATH and is a pull_request or
# pull_request_target payload. --pr reads the same REST pull request object
# with `gh api`, because `gh pr view --json` has no author association. Only
# JSON is read, that object and the author's repository permission: nothing
# from the pull request is checked out or run.
#
# The pull request is closed only when the event, if there is one, is
# `opened`, the pull request is open and targets release-*, its author's
# association is not OWNER, MEMBER or COLLABORATOR, its author is not a bot,
# and GitHub answers that the author's permission and role on the repository
# are both read or none. A missing field or a value this script does not know
# leaves it open and exits 0. Exits 1, closing nothing, when the input or
# GitHub's answer on the author cannot be read, 2 on a usage error, and with
# gh's status when the close fails.
set -eu

usage() {
  echo "usage: close-manual-backport.sh [--dry-run] [EVENT_JSON | --pr NUMBER]" >&2
  exit 2
}

leave() {
  echo "close-manual-backport: $*; leaving it open."
  exit 0
}

dry_run=false
pr_number=
event=
while [ "$#" -gt 0 ]; do
  case "$1" in
    --dry-run) dry_run=true ;;
    --pr)
      [ "$#" -ge 2 ] || usage
      pr_number=$2
      shift
      ;;
    -*) usage ;;
    *)
      [ -z "$event" ] || usage
      event=$1
      ;;
  esac
  shift
done

command -v jq >/dev/null 2>&1 || { echo "close-manual-backport: jq is required" >&2; exit 1; }

if [ -n "$pr_number" ]; then
  [ -z "$event" ] || usage
  case "$pr_number" in '' | *[!0-9]*) usage ;; esac
  if ! pr=$(gh api "repos/{owner}/{repo}/pulls/$pr_number"); then
    echo "close-manual-backport: cannot read PR #$pr_number with gh" >&2
    exit 1
  fi
else
  event=${event:-${GITHUB_EVENT_PATH:-}}
  [ -n "$event" ] || usage
  if ! pr=$(jq -c '.pull_request' "$event" 2>/dev/null); then
    echo "close-manual-backport: cannot read the event JSON at $event" >&2
    exit 1
  fi
  action=$(jq -r '.action | strings' "$event" 2>/dev/null || true)
  [ "$action" = opened ] || leave "the event is ${action:-missing}, not opened"
fi

# A filter that fails on an unexpected shape prints nothing, which every check
# below reads as a missing field.
json_field() {
  printf '%s' "$1" | jq -r "$2" 2>/dev/null || true
}

field() {
  json_field "$pr" "$1"
}

[ "$(field 'type')" = object ] || leave "the input carries no pull request"
number=$(field '.number | select(type == "number" and . > 0 and . == floor) | tostring')
[ -n "$number" ] || leave "the pull request carries no number"

base=$(field '.base.ref | strings')
case "$base" in
  release-*) ;;
  '') leave "PR #$number carries no base ref" ;;
  *) leave "PR #$number targets $base, not a release branch" ;;
esac

state=$(field '.state | strings')
[ "$state" = open ] || leave "PR #$number is not open (state: ${state:-missing})"

assoc=$(field '.author_association | strings')
case "$assoc" in
  OWNER | MEMBER | COLLABORATOR) leave "PR #$number is from a repository $assoc" ;;
  CONTRIBUTOR | FIRST_TIME_CONTRIBUTOR | FIRST_TIMER | NONE) ;;
  '') leave "PR #$number carries no author association" ;;
  *) leave "PR #$number has an author association this script does not know ($assoc)" ;;
esac

user_type=$(field '.user.type | strings')
case "$user_type" in
  User) ;;
  Bot) leave "PR #$number is from a bot" ;;
  '') leave "PR #$number carries no author type" ;;
  *) leave "PR #$number has an author type this script does not know ($user_type)" ;;
esac

repo=$(field '.base.repo.full_name | strings')
case "$repo" in
  */*/* | *[!A-Za-z0-9._/-]* | /* | */) leave "PR #$number names no usable base repository" ;;
  */*) ;;
  *) leave "PR #$number names no usable base repository" ;;
esac
login=$(field '.user.login | strings')
case "$login" in
  '' | *[!A-Za-z0-9-]*) leave "PR #$number carries no usable author login" ;;
esac

# author_association reports an organization member whose membership is
# private as a CONTRIBUTOR, so the author's own access to the repository is the
# check that decides. `permission` folds maintain into write and triage into
# read, which is why `role_name` has to agree. An answer that cannot be read
# fails the job: left open in silence, the pull request would look decided.
if ! access=$(gh api "repos/$repo/collaborators/$login/permission"); then
  echo "close-manual-backport: PR #$number: cannot read $login's permission on $repo; closing nothing" >&2
  exit 1
fi
permission=$(json_field "$access" '.permission | strings')
role=$(json_field "$access" '.role_name | strings')
if [ -z "$permission" ] || [ -z "$role" ]; then
  echo "close-manual-backport: PR #$number: GitHub's answer on $login's permission on $repo does not give both permission and role_name; closing nothing" >&2
  exit 1
fi
case "$permission:$role" in
  read:read | read:none | none:read | none:none) ;;
  *) leave "PR #$number is from $login, whose permission on $repo is $permission (role $role)" ;;
esac

policy="https://github.com/$repo/blob/main/docs/release/backports.md"
comment="Thank you for the contribution! Backports are automatic: fixes merged to \`main\` reach the release lines the batch runs for on their own, as the [backport policy]($policy) describes, so this pull request is closed. If the fix only makes sense on a release line and has no counterpart on \`main\`, a maintainer can reopen it."

if [ "$dry_run" = true ]; then
  printf 'close-manual-backport: would close PR #%s in %s with this comment:\n%s\n' "$number" "$repo" "$comment"
  exit 0
fi
gh pr close "$number" --repo "$repo" --comment "$comment"
echo "close-manual-backport: closed PR #$number in $repo."