This is an automated email from the ASF dual-hosted git repository.

mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new f5c061de70 [MINOR] ci: Label issues with the fix version and assign 
the author on PR merge (#13501)
f5c061de70 is described below

commit f5c061de70e19d553ca4f73a8be37bd9f03ebfe3
Author: Jerry Shao <[email protected]>
AuthorDate: Fri Sep 25 21:57:37 2026 +0800

    [MINOR] ci: Label issues with the fix version and assign the author on PR 
merge (#13501)
    
    ### What changes were proposed in this pull request?
    
    Add a workflow `label-fix-version.yml` (logic in
    `dev/ci/label_fix_version.sh`) that labels issues with the release their
    fix ships in:
    
    - **On PR merge** (`pull_request_target`, any base branch): collect
    issues from `[#N]` in the title (`[#1][#2]`, `[#1, #2]` also work) and
    from closing keywords in the body (`Fix: #N`, `closes #N`, ...). PRs
    without an issue are skipped. The version comes from `gradle.properties`
    at the merge commit with `-SNAPSHOT` stripped, e.g. main -> `2.0.0`; if
    an RC of an earlier release already contains the merge commit, that
    release is used, so reruns are stable. The PR itself (even without a
    linked issue) and its unassigned issues are assigned to the PR author
    (bots skipped).
    - **On RC tag push** (`vX.Y.Z-rcN`, N ≥ 2): the release script bumps to
    the next SNAPSHOT right after tagging an RC, so issues fixed between
    `rc(N-1)` and `rcN` were labeled with the next version. They are moved
    to `X.Y.Z`, unless the issue was fixed again after the RC.
    
    Labels follow the existing `x.y.z` / `Release vx.y.z` convention and are
    created if missing. The merge job only checks out the default branch,
    never PR code, and passes PR-controlled values through `env`.
    
    Note: tag-push workflows run from the tagged commit, so the RC
    relabeling only works on release branches cut after this lands. It is
    intentionally not backported to branch-1.3.
    
    ### Why are the changes needed?
    
    Today the fix version of an issue is labeled manually, which is easy to
    miss, especially for fixes cherry-picked to release branches. With this,
    each issue carries one label per release it ships in (e.g. `2.0.0` and
    `1.3.1`).
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Ran the script locally with `DRY_RUN=true` against real PRs and tags:
    - #13398 (main) -> #13397 labeled `2.0.0`
    - #13485 (merged between v1.3.1-rc1 and rc2) and #13494 (between rc2 and
    rc3) -> `1.3.1`
    - #13497 (`[MINOR]`, no issue) -> labeling skipped; #13496 (`[MINOR]`,
    unassigned) -> PR assigned to its author
    - `v1.3.1-rc1` -> no-op; `v1.3.1-rc2` / `rc3` -> issues from the range
    moved to `1.3.1`; with a stubbed `1.3.2` label it is removed, and kept
    when a later commit fixes the issue again
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .github/workflows/label-fix-version.yml |  87 +++++++++++
 dev/ci/label_fix_version.sh             | 257 ++++++++++++++++++++++++++++++++
 2 files changed, 344 insertions(+)

diff --git a/.github/workflows/label-fix-version.yml 
b/.github/workflows/label-fix-version.yml
new file mode 100644
index 0000000000..94177a7960
--- /dev/null
+++ b/.github/workflows/label-fix-version.yml
@@ -0,0 +1,87 @@
+#
+# 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.
+#
+
+# Label issues with the release their fix ships in, see
+# dev/ci/label_fix_version.sh for details.
+#
+# - When a PR is merged, its issues get the version of the merge commit,
+#   e.g. main (2.0.0-SNAPSHOT) -> "2.0.0". The PR and its issues are assigned
+#   to the PR author if they have no assignee.
+# - The release script bumps to the next SNAPSHOT right after tagging an RC,
+#   so when a later RC is tagged, the issues fixed since the previous RC are
+#   moved to that release, e.g. v1.3.1-rc2 moves them from "1.3.2" to "1.3.1".
+#
+# The merge job uses pull_request_target so that PRs from forks get a token
+# that can write labels. It never checks out PR code, and all PR-controlled
+# values are passed through env to avoid script injection.
+
+name: Label issues with fix version
+
+on:
+  pull_request_target:
+    types: [closed]
+  push:
+    tags:
+      - 'v*-rc*'
+
+permissions:
+  contents: read
+  issues: write
+  pull-requests: write
+
+jobs:
+  label-on-merge:
+    if: github.event_name == 'pull_request_target' && 
github.event.pull_request.merged == true
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          # Only the trusted default branch, never the PR head.
+          ref: ${{ github.event.repository.default_branch }}
+          persist-credentials: false
+
+      - name: Label linked issues and assign the PR author
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          REPO: ${{ github.repository }}
+          PR_NUMBER: ${{ github.event.pull_request.number }}
+          PR_TITLE: ${{ github.event.pull_request.title }}
+          PR_BODY: ${{ github.event.pull_request.body }}
+          MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
+          PR_AUTHOR: ${{ github.event.pull_request.user.login }}
+          PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }}
+        run: dev/ci/label_fix_version.sh merge
+
+  label-on-rc:
+    if: github.event_name == 'push'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          persist-credentials: false
+
+      - name: Move issues fixed since the previous RC to this release
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          REPO: ${{ github.repository }}
+          TAG: ${{ github.ref_name }}
+        run: dev/ci/label_fix_version.sh rc
diff --git a/dev/ci/label_fix_version.sh b/dev/ci/label_fix_version.sh
new file mode 100755
index 0000000000..9298633c2b
--- /dev/null
+++ b/dev/ci/label_fix_version.sh
@@ -0,0 +1,257 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+# Labels issues with the release their fix ships in. Used by
+# .github/workflows/label-fix-version.yml.
+#
+#   label_fix_version.sh merge  A PR was merged. Label its issues with the
+#                               release of the merge commit, and assign the
+#                               PR and its issues to the PR author if they
+#                               have no assignee.
+#                               Env: REPO, PR_NUMBER, PR_TITLE, PR_BODY,
+#                               MERGE_SHA, PR_AUTHOR, PR_AUTHOR_TYPE.
+#   label_fix_version.sh rc     A release candidate tag was pushed. Move the
+#                               issues fixed since the previous RC to this
+#                               release. Needs full git history.
+#                               Env: REPO, TAG.
+#
+# Issues are taken from "[#N]" in the PR title or commit subject, e.g.
+# "[#1][#2]" or "[#1, #2]", and from closing keywords in the body, e.g.
+# "Fix: #1". Set DRY_RUN=true to print writes instead of running them.
+
+set -euo pipefail
+# Fail on errors inside command substitutions too (bash >= 4.4). The API calls
+# below also handle failures explicitly, so older bash still fails loudly.
+shopt -s inherit_errexit 2> /dev/null || true
+
+: "${REPO:?REPO must be set}"
+DRY_RUN=${DRY_RUN:-false}
+
+write() {
+  if [ "$DRY_RUN" = "true" ]; then
+    echo "[dry-run] $*"
+  else
+    "$@"
+  fi
+}
+
+# Prints the issue ids referenced by a subject and a body, one per line.
+extract_issues() {
+  local subject=$1 body=$2
+  {
+    printf '%s\n' "$subject" \
+      | grep -oE 
'\[[[:space:]]*#[0-9]+([[:space:]]*,?[[:space:]]*#[0-9]+)*[[:space:]]*\]' \
+      | grep -oE '#[0-9]+' | tr -d '#' || true
+    printf '%s\n' "$body" \
+      | grep -oiE 
'(^|[^[:alnum:]_])(close[sd]?|fix(e[sd])?|resolve[sd]?):?[[:space:]]+#[0-9]+' \
+      | grep -oE '[0-9]+$' || true
+  } | sort -un
+}
+
+# Reads gradle.properties from stdin and prints the version without -SNAPSHOT.
+parse_version() {
+  sed -n 's/^version[[:space:]]*=[[:space:]]*//p' | tr -d '[:space:]' | sed 
's/-SNAPSHOT$//'
+}
+
+is_version() {
+  printf '%s' "$1" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'
+}
+
+version_lt() {
+  [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -1)" = 
"$1" ]
+}
+
+# Creates the version label if missing, following the existing convention.
+ensure_label() {
+  local version=$1
+  if ! gh label list --repo "$REPO" --search "$version" --limit 100 --json 
name --jq '.[].name' \
+      | grep -qxF "$version"; then
+    write gh label create "$version" --repo "$REPO" \
+      --description "Release v$version" --color "$(openssl rand -hex 3)" \
+      || echo "Label $version may already exist."
+  fi
+}
+
+# Prints the release that contains the given merge commit.
+release_of_commit() {
+  local sha=$1 version tags tag status
+  version=$(gh api "repos/$REPO/contents/gradle.properties?ref=$sha" --jq 
'.content' \
+    | base64 -d | parse_version) || return 1
+  if ! is_version "$version"; then
+    echo "Unexpected version '$version' at $sha." >&2
+    return 1
+  fi
+
+  # The release script bumps to the next SNAPSHOT right after tagging an RC,
+  # so a commit is in an earlier release only if a later RC of that release
+  # was cut after it. Check the latest RC of each earlier release on this line.
+  tags=$(gh api --paginate 
"repos/$REPO/git/matching-refs/tags/v${version%.*}." \
+      --jq '.[].ref | sub("^refs/tags/"; "") | select(test("-rc[0-9]+$"))' \
+    | sort -V | awk -F'-rc' '!($1 in last) { order[++n] = $1 } { last[$1] = $0 
}
+        END { for (i = 1; i <= n; i++) print last[order[i]] }') || return 1
+  for tag in $tags; do
+    local tag_version=${tag#v}
+    tag_version=${tag_version%-rc*}
+    version_lt "$tag_version" "$version" || continue
+    status=$(gh api "repos/$REPO/compare/$tag...$sha" --jq '.status') || 
return 1
+    if [ "$status" = "behind" ] || [ "$status" = "identical" ]; then
+      echo "$tag_version"
+      return 0
+    fi
+  done
+  echo "$version"
+}
+
+on_merge() {
+  : "${PR_NUMBER:?}" "${PR_TITLE:?}" "${MERGE_SHA:?}" "${PR_AUTHOR:?}" 
"${PR_AUTHOR_TYPE:?}"
+  local issues version issue info is_pr assignee_count
+  assignee_count=$(gh api "repos/$REPO/issues/$PR_NUMBER" --jq '.assignees | 
length')
+  assign_author_if_unassigned "$PR_NUMBER" "$assignee_count"
+
+  issues=$(extract_issues "$PR_TITLE" "${PR_BODY:-}")
+  if [ -z "$issues" ]; then
+    echo "No linked issue found, skipping."
+    return 0
+  fi
+
+  version=$(release_of_commit "$MERGE_SHA")
+  echo "Merge commit $MERGE_SHA ships in $version."
+  ensure_label "$version"
+
+  for issue in $issues; do
+    info=$(gh api "repos/$REPO/issues/$issue" --jq '"\(has("pull_request")) 
\(.assignees | length)"')
+    read -r is_pr assignee_count <<< "$info"
+    if [ "$is_pr" != "false" ]; then
+      echo "#$issue is a pull request, skipping."
+      continue
+    fi
+    write gh issue edit "$issue" --repo "$REPO" --add-label "$version"
+    echo "Labeled issue #$issue with $version."
+    assign_author_if_unassigned "$issue" "$assignee_count"
+  done
+}
+
+# Assigns an issue or PR to the PR author if it has no assignee yet.
+assign_author_if_unassigned() {
+  local number=$1 assignee_count=$2 assigned
+  if [ "$assignee_count" != "0" ]; then
+    return 0
+  fi
+  if [ "$PR_AUTHOR_TYPE" = "Bot" ]; then
+    echo "PR author $PR_AUTHOR is a bot, not assigning #$number."
+    return 0
+  fi
+  if [ "$DRY_RUN" = "true" ]; then
+    echo "[dry-run] assign #$number to $PR_AUTHOR"
+    return 0
+  fi
+  # GitHub silently drops assignees it doesn't accept, so check the result.
+  assigned=$(gh api -X POST "repos/$REPO/issues/$number/assignees" -f 
"assignees[]=$PR_AUTHOR" \
+    --jq '[.assignees[].login] | index(env.PR_AUTHOR) != null')
+  if [ "$assigned" = "true" ]; then
+    echo "Assigned #$number to $PR_AUTHOR."
+  else
+    echo "Could not assign #$number to $PR_AUTHOR."
+  fi
+}
+
+# Prints the issues referenced by the commits in the given git log range.
+# Each commit is resolved to the PR that merged it, the same source the
+# merge job uses, since rebase-merged commits don't carry the PR title.
+# Commits pushed without a PR fall back to their own message.
+issues_in_range() {
+  local shas sha prs pr json
+  local seen_prs=" "
+  shas=$(git rev-list "$@") || return 1
+  # The loop runs in a pipeline subshell, so a failed API call exits it and
+  # pipefail fails the caller instead of falling back to the commit message.
+  for sha in $shas; do
+    json=$(gh api "repos/$REPO/commits/$sha/pulls" --jq '[.[] | 
select(.merged_at != null)]') \
+      || exit 1
+    prs=$(jq -r '.[].number' <<< "$json")
+    if [ -z "$prs" ]; then
+      extract_issues "$(git log -1 --format=%s "$sha")" "$(git log -1 
--format=%b "$sha")"
+      continue
+    fi
+    for pr in $prs; do
+      [[ "$seen_prs" == *" $pr "* ]] && continue
+      seen_prs="$seen_prs$pr "
+      extract_issues "$(jq -r --argjson n "$pr" '.[] | select(.number == $n) | 
.title' <<< "$json")" \
+        "$(jq -r --argjson n "$pr" '.[] | select(.number == $n) | .body // ""' 
<<< "$json")"
+    done
+  done | sort -un
+}
+
+on_rc() {
+  : "${TAG:?}"
+  if [[ ! "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)-rc([0-9]+)$ ]]; then
+    echo "$TAG is not a release candidate tag, skipping."
+    return 0
+  fi
+  local version=${BASH_REMATCH[1]} rc=${BASH_REMATCH[2]}
+  local prev="v$version-rc$((rc - 1))"
+  if [ "$rc" -lt 2 ] || ! git rev-parse -q --verify "refs/tags/$prev" > 
/dev/null; then
+    echo "No previous RC for $TAG, nothing to relabel."
+    return 0
+  fi
+
+  # Commits merged since the previous RC were labeled with the SNAPSHOT
+  # version the release script bumped to, but they ship in this release.
+  local next issues later_refs later_issues issue labels
+  next=$(git show "$TAG^:gradle.properties" | parse_version)
+  issues=$(issues_in_range "$prev..$TAG")
+  if [ -z "$issues" ]; then
+    echo "No issue fixed between $prev and $TAG."
+    return 0
+  fi
+  # Issues fixed again after this RC still ship in the next release.
+  later_refs=$(git for-each-ref --contains "$TAG" --format='%(refname)' 
refs/remotes)
+  later_issues=""
+  if [ -n "$later_refs" ]; then
+    # shellcheck disable=SC2086
+    later_issues=$(issues_in_range $later_refs --not "$TAG")
+  fi
+
+  echo "Issues fixed between $prev and $TAG ship in $version."
+  ensure_label "$version"
+  for issue in $issues; do
+    labels=$(gh api "repos/$REPO/issues/$issue" \
+      --jq 'if has("pull_request") then "PR" else ([.labels[].name] | 
join(",")) end')
+    if [ "$labels" = "PR" ]; then
+      echo "#$issue is a pull request, skipping."
+      continue
+    fi
+    write gh issue edit "$issue" --repo "$REPO" --add-label "$version"
+    echo "Labeled issue #$issue with $version."
+    if [ "$next" != "$version" ] && [[ ",$labels," == *",$next,"* ]] \
+        && ! grep -qxF "$issue" <<< "$later_issues"; then
+      write gh issue edit "$issue" --repo "$REPO" --remove-label "$next"
+      echo "Removed $next from issue #$issue."
+    fi
+  done
+}
+
+case "${1:-}" in
+  merge) on_merge ;;
+  rc) on_rc ;;
+  *)
+    echo "Usage: $0 merge|rc" >&2
+    exit 1
+    ;;
+esac

Reply via email to