From 7cbdb80f6ba17e1a85f61ecc408d61799b5c9273 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:26:19 -0700 Subject: [PATCH] ci(label-actions): composit actions to add and delete labels in bulk Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .github/actions/add-labels/action.yml | 238 +++++++++++++++++++++ .github/actions/delete-labels/action.yml | 99 +++++++++ .github/workflows/manual-add-label.yml | 44 ++++ .github/workflows/manual-delete-labels.yml | 29 +++ 4 files changed, 410 insertions(+) create mode 100644 .github/actions/add-labels/action.yml create mode 100644 .github/actions/delete-labels/action.yml create mode 100644 .github/workflows/manual-add-label.yml create mode 100644 .github/workflows/manual-delete-labels.yml diff --git a/.github/actions/add-labels/action.yml b/.github/actions/add-labels/action.yml new file mode 100644 index 000000000..fdc8395b2 --- /dev/null +++ b/.github/actions/add-labels/action.yml @@ -0,0 +1,238 @@ +name: Add Label(s) +description: adds label(s) to labelable +inputs: + gh_token: + description: gh api access token to use + default: ${{ secrets.GITHUB_TOKEN }} + repository: + description: the OWNER/REPOSITORY to operate on + default: ${{ github.repository }} + issues: + description: a single or comma separated list of issue numbers + required: true + labels: + description: a single or comma separated list of labels to apply to all listed issues + required: true + colors: + description: | + A single or comma separated list of colors to create the labels with if needed. + the list order is the same as `labels`. Missing or blank values (e.g. `FFFFFF,,FFFFFF`) use the `default-color` + default-color: + description: default color to create labels with + default: "#D4C5F9" + +runs: + using: "composite" + steps: + - name: Collect Repo Labels + id: collect_labels + shell: bash + env: + GH_TOKEN: ${{ inputs.gh_token }} + REPOSITORY: ${{ inputs.repository }} + LABELS: ${{ inputs.labels }} + COLORS: ${{ inputs.colors }} + DEFAULT_COLOR: ${{ inputs.default-color }} + run: | + owner=$(echo "$REPOSITORY" | cut -d '/' -f 1) + repo=$(echo "$REPOSITORY" | cut -d '/' -f 2) + query=$( + jq -nr \ + --arg labels "$LABELS" \ + ' + (reduce ($labels | split (", *"; null) | .[]) as $i ([]; . + [$i])) as $labels + | "query ($owner: String!, $repo: String!) {\n" + + " repository(owner: $owner, name: $repo) {\n" + + " id\n" + + ( + reduce $labels[] as $i ([0, ""]; [ + .[0] + 1, + .[1] + ( + " _" + (.[0] | tostring) + + ": label(name: \"" + $i + "\") {\n" + + " id\n" + + " name\n"+ + " }\n" + ) + ]) + | .[1] + )+ + " }\n" + + "}" + ' + ) + data=$( + gh api graphql \ + -f owner="$owner" \ + -f repo="$repo" \ + -f query="$query" \ + | jq -c \ + --arg labels "$LABELS" \ + --arg colors "$COLORS" \ + --arg defaultColor "$DEFAULT_COLOR" \ + ' + . as $in + | ($labels | split(", *"; null)) as $labels + | ($colors | split(", *"; null)) as $colors + | ( + reduce ( [$labels, $colors] | transpose)[] as $i ( + {}; + .[$i[0]] = ( + if ($i[1] and $i[1] != "") then + $i[1] + else + $defaultColor + end + | if startswith("#") then .[1:] else . end + ) + ) + ) as $colorMap + | ( + reduce ( + $in.data.repository[] + | select( objects | .name as $name | any($labels[]; . == $name ) ) + ) as $i ({}; .[$i.name] = {"id": $i.id}) + ) as $found + | ( + reduce ( + $labels[] + | select( . as $name | $found | has($name) | not ) + ) as $i ({}; .[$i] = {"color": $colorMap[$i]}) + ) as $missing + | { + "repoId": $in.data.repository.id, + "found": $found, + "missing": $missing, + } + ' + ) + echo "found=$(jq -c '.found' <<< "$data")" >> "$GITHUB_OUTPUT" + echo "missing=$(jq -c '.missing' <<< "$data")" >> "$GITHUB_OUTPUT" + echo "repo_id=$(jq -r '.repoId' <<< "$data")" >> "$GITHUB_OUTPUT" + + - name: Collect Item Node IDs + id: collect_ids + shell: bash + env: + GH_TOKEN: ${{ inputs.gh_token }} + REPOSITORY: ${{ inputs.repository }} + ISSUES: ${{ inputs.labels }} + run: | + owner=$(echo "$REPOSITORY" | cut -d '/' -f 1) + repo=$(echo "$REPOSITORY" | cut -d '/' -f 2) + query=$( + jq -nr \ + --arg issues "$ISSUES" \ + ' + (reduce ($issues | split (", *"; null) | .[]) as $i ([]; . + [$i])) as $issues + | + "query ($owner: String!, $repo: String!) {\n" + + " repository(owner: $owner, name: $repo) {\n" + + ( + reduce $issues[] as $i ([0, ""]; [ + .[0] + 1, + .[1] + ( + " _" + (.[0] | tostring) + + ": issueOrPullRequest(number: " + ($i | tostring) +") {\n" + + " ... on Issue {\n" + + " id\n" + + " }\n" + + " ... on PullRequest {\n"+ + " id\n"+ + " }\n" + + " }\n" + ) + ]) + | .[1] + )+ + " }\n" + + "}" + ' + ) + data=$( + gh api graphql \ + -f owner="$owner" \ + -f repo="$repo" \ + -f query="$query" \ + | jq -c 'reduce .data.repository[].id as $i ([]; . + [$i])' + ) + echo "issue_ids=$data" >> "$GITHUB_OUTPUT" + + - name: Create Missing Labels + id: create_missing + shell: bash + env: + GH_TOKEN: ${{ inputs.gh_token }} + REPO_ID: ${{ steps.collect_labels.outputs.repo_id }} + LABELS: ${{ steps.collect_labels.outputs.labels }} + MISSING: ${{ steps.collect_labels.outputs.missing }} + run: | + query=$( + jq -nr \ + --argjson labels "$MISSING" \ + --arg repo "$REPO_ID" \ + ' + "mutation {\n" + ( + reduce ($labels | keys | .[] | [., $labels[.]]) as $i ([0, ""]; [ + .[0] + 1, + .[1] + ( + " _" + (.[0] | tostring) + + ": createLabel(input: {repositoryId: \"" + $repo + + "\", name: \"" + $i[0] + + "\", color: \"" + $i[1].color + + "\"}) {\n" + + " clientMutationId\n" + + " label {\n" + + " id\n" + + " name\n" + + " color\n" + + " }\n" + + " }\n" + ) + ]) + | .[1] + ) + + "}" + ' + ) + data=$( + gh api graphql -f query="$query" \ + | jq --argjson existing "$LABELS" \ + ' + reduce .data[].label as $i ({}; .[$i.name] = {"id": $i.id, "color": $i.color }) + | . + $existing + ' + ) + lable_ids=$(jq -c '[.[].id]' <<< "$data") + echo "label_ids=$lable_ids" >> "$GITHUB_OUTPUT" + + - name: Apply Labels + id: apply_labels + shell: bash + env: + GH_TOKEN: ${{ inputs.gh_token }} + ISSUES: ${{ steps.collect_ids.outputs.issue_ids }} + LABELS: ${{ steps.create_missing.outputs.label_ids }} + run: | + query=$( + jq -nr \ + --argjson labels "$LABELS" \ + --argjson issues "$ISSUES" \ + ' + "mutation {\n" + ( + reduce $issues[] as $i ([0, ""]; [ + .[0] + 1, + .[1] + ( + " _" + (.[0] | tostring) + + ": addLabelsToLabelable(input: {labelableId: \"" + + $i + "\", labelIds: " + ($labels | tojson) + "}) {\n" + + " clientMutationId\n" + + " }\n" + ) + ]) + | .[1] + ) + + "}" + ' + ) + gh api graphql -f query="$query" diff --git a/.github/actions/delete-labels/action.yml b/.github/actions/delete-labels/action.yml new file mode 100644 index 000000000..9ca921283 --- /dev/null +++ b/.github/actions/delete-labels/action.yml @@ -0,0 +1,99 @@ +name: Delete Label(s) +description: delete Label(s) +inputs: + gh_token: + description: gh api access token to use + default: ${{ secrets.GITHUB_TOKEN }} + repository: + description: the OWNER/REPOSITORY to operate on + default: ${{ github.repository }} + labels: + description: a single or comma separated list of labels to delete + required: true + +runs: + using: "composite" + steps: + - name: Collect Repo Labels + id: collect_labels + shell: bash + env: + GH_TOKEN: ${{ inputs.gh_token }} + REPOSITORY: ${{ inputs.repository }} + LABELS: ${{ inputs.labels }} + run: | + owner=$(echo "$REPOSITORY" | cut -d '/' -f 1) + repo=$(echo "$REPOSITORY" | cut -d '/' -f 2) + query=$( + jq -nr \ + --arg labels "$LABELS" \ + ' + (reduce ($labels | split (", *"; null) | .[]) as $i ([]; . + [$i])) as $labels + | "query ($owner: String!, $repo: String!) {\n" + + " repository(owner: $owner, name: $repo) {\n" + + ( + reduce $labels[] as $i ([0, ""]; [ + .[0] + 1, + .[1] + ( + " _" + (.[0] | tostring) + + ": label(name: \"" + $i + "\") {\n" + + " id\n" + + " name\n"+ + " }\n" + ) + ]) + | .[1] + )+ + " }\n" + + "}" + ' + ) + data=$( + gh api graphql \ + -f owner="$owner" \ + -f repo="$repo" \ + -f query="$query" \ + | jq -c \ + --arg labels "$LABELS" \ + --arg colors "$COLORS" \ + --arg defaultColor "$DEFAULT_COLOR" \ + ' + . as $in + | ($labels | split(", *"; null)) as $labels + | ( + reduce ( + $in.data.repository[] + | select( objects | .name as $name | any($labels[]; . == $name ) ) + ) as $i ({}; .[$i.name] = {"id": $i.id}) + ) as $found + | [.[].id] + ' + ) + echo "label_ids=$data" >>> "$GITHUB_OUTPUT" + + - name: Delete Labels + id: delete_labels + shell: bash + env: + GH_TOKEN: ${{ inputs.gh_token }} + LABELS: ${{ steps.collect_labels.outputs.label_ids }} + run: | + query=$(jq -r ' + . as $in + | ( + "mutation {\n" + ( + reduce $in[] as $id ([0, ""]; [ + .[0] + 1 , + .[1] + ( + " _" + (.[0] | tostring) + ": deleteLabel(input: {id: \"" + $id + "\"}) {\n" + + " clientMutationId\n" + + " }\n" + ) + ]) + | .[1] + ) + + "}" + ) + ' <<< "$LABELS" + ) + gh api graphql -f query="$query" diff --git a/.github/workflows/manual-add-label.yml b/.github/workflows/manual-add-label.yml new file mode 100644 index 000000000..e6c768a7d --- /dev/null +++ b/.github/workflows/manual-add-label.yml @@ -0,0 +1,44 @@ +name: Manual workflow to apply labels in bulk + +on: + workflow_dispatch: + inputs: + issues: + description: a single or comma separated list of issue numbers + required: true + type: string + labels: + description: a single or comma separated list of labels to apply to all listed issues + required: true + type: string + colors: + description: | + A single or comma separated list of colors to create the labels with if needed. + the list order is the same as `labels`. Missing or blank values (e.g. `FFFFFF,,FFFFFF`) use the `default_color` + type: string + default-color: + description: default color to create labels with + default: "#D4C5F9" + type: string + +jobs: + apply-labels: + name: Apply Labels + runs-on: ubuntu-latest + + permissions: + issues: write + pull-requests: write + + steps: + - name: Checkout Default Branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + - name: Run Label Action + uses: ./.github/actions/add-labels + with: + issues: ${{ inputs.issues }} + labels: ${{ inputs.labels }} + colors: ${{ inputs.colors }} + default-color: ${{ inputs.default-color }} diff --git a/.github/workflows/manual-delete-labels.yml b/.github/workflows/manual-delete-labels.yml new file mode 100644 index 000000000..b575d59d8 --- /dev/null +++ b/.github/workflows/manual-delete-labels.yml @@ -0,0 +1,29 @@ + +name: Manual workflow to delete labels in bulk + +on: + workflow_dispatch: + inputs: + labels: + description: a single or comma separated list of labels to delete + required: true + type: string + +jobs: + delete-labels: + name: Delete Labels + runs-on: ubuntu-latest + + permissions: + issues: write + pull-requests: write + + steps: + - name: Checkout Default Branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + - name: Run Label Action + uses: ./.github/actions/delete-labels + with: + labels: ${{ inputs.labels }}