From fee4d70090ea307b5280c28f784260112de6cf9c Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:24:46 +0000 Subject: [PATCH 1/6] Add gh helper scripts for admin purpose --- git-migration/gh_manage_ss_labels | 116 ------------------------ sbin/gh_add_user | 145 ++++++++++++++++++++++++++++++ sbin/gh_manage_labels | 111 +++++++++++++++++++++++ 3 files changed, 256 insertions(+), 116 deletions(-) delete mode 100755 git-migration/gh_manage_ss_labels create mode 100755 sbin/gh_add_user create mode 100755 sbin/gh_manage_labels diff --git a/git-migration/gh_manage_ss_labels b/git-migration/gh_manage_ss_labels deleted file mode 100755 index 53b6e9eb..00000000 --- a/git-migration/gh_manage_ss_labels +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env bash - -# ---------------------------------------------------------------------------- -# (C) Crown copyright Met Office. All rights reserved. -# The file LICENCE, distributed with this code, contains details of the terms -# under which the code may be used. -# ---------------------------------------------------------------------------- - -# Script to create standard labels in multiple GitHub repositories -# Requires GitHub CLI: https://cli.github.com/ - -set -euo pipefail - -# -- Parse options -ADD_LABELS=true -DELETE_LABELS=false -usage() { - echo "Usage: $0 [--add|--delete] [--help|-h]" - echo "Manage labels in below repository - using this requires admin rights to the repositories" - echo " --add Add labels (default)" - echo " --delete Delete labels" - echo " --help,-h Show this help message" -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --add) - ADD_LABELS=true - DELETE_LABELS=false - shift - ;; - --delete) - ADD_LABELS=false - DELETE_LABELS=true - shift - ;; - --help|-h) - usage - exit 0 - ;; - *) - echo "Unknown option: $1" >&2 - usage - exit 1 - ;; - esac -done - -# -- Label Settings -# Format: "label_name|hex_color|description" -labels=( - "Linked UM|#50bfe6|This PR is linked to a MetOffice/um PR" - "Linked Jules|#66ff66|This PR is linked to a MetOffice/jules PR" - "Linked Apps|#ff6037|This PR is linked to a MetOffice/lfric_apps PR" - "Linked Core|#ffff66|This PR is linked to a MetOffice/lfric_core PR" - "Linked UKCA|#ff00cc|This PR is linked to a MetOffice/ukca PR" - "Linked Casim|#fd5b78|This PR is linked to a MetOffice/casim PR" - "Linked Socrates|#16d0cb|This PR is linked to a MetOffice/socrates PR" - "Linked Mule|#00ffff|This PR is linked to a MetOffice/mule PR" - "Linked Shumlib|#0080ff|This PR is linked to a MetOffice/shumlib PR" - "KGO|#9c27b0|This PR contains changes to KGO" - "macro|#aaf0d1|This PR contains a metadata upgrade macro" - "Accessibility|#1c96b9|Problems with the accessibility of the documentation" - "Discussion|#FBCA04|Issues that require some formal discussion" - "cla-required|#b60205|The CLA has not yet been signed by the author of this PR - added by GA" - "cla-signed|#0052cc|The CLA has been signed as part of this PR - added by GA" -) - -# -- Add labels in relevant repositories -repos=( - "MetOffice/um" - "MetOffice/jules" - "MetOffice/lfric_apps" - "MetOffice/lfric_core" - "MetOffice/ukca" - "MetOffice/casim" - "MetOffice/socrates" - "MetOffice/mule" - "MetOffice/shumlib" - "MetOffice/um_aux" - "MetOffice/um_doc" - "MetOffice/um_meta" - "MetOffice/socrates-spectral" - "MetOffice/moci" -) - -# -- Extract labels and colors - -for repo in "${repos[@]}"; do - echo "Processing labels in repository: $repo" - for label in "${labels[@]}"; do - name=$(echo "$label" | cut -d'|' -f1) - color=$(echo "$label" | cut -d'|' -f2) - description=$(echo "$label" | cut -d'|' -f3) - linked_repo=$(echo "$label" | grep -oP 'MetOffice/\w+' || true) - - [[ "$repo" == "$linked_repo" ]] && continue - - if [[ "$ADD_LABELS" == true ]]; then - gh label create "$name" \ - --color "$color" \ - --description "$description" \ - --force \ - --repo "$repo" - fi - if [[ "$DELETE_LABELS" == true ]]; then - gh label delete "$name" --yes --repo "$repo" || true - fi - done -done - -if [[ "$ADD_LABELS" == true ]]; then - echo "Labels added." -elif [[ "$DELETE_LABELS" == true ]]; then - echo "Labels deleted." -fi diff --git a/sbin/gh_add_user b/sbin/gh_add_user new file mode 100755 index 00000000..a81b43da --- /dev/null +++ b/sbin/gh_add_user @@ -0,0 +1,145 @@ +#!/usr/bin/env bash + +# ---------------------------------------------------------------------------- +# (C) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ---------------------------------------------------------------------------- + +# Add collaborator to a repository (requires admin privilege on the repo) +# gh api --method PUT /repos/:owner/:repo/collaborators/:username +# yaswant.pradhan + +# add a user to a repository via command line options +set -euo pipefail + +usage() { + cat < [-r ] [options] + -u GitHub username to add/remove + -r Repository name (if omitted, operate on all repos + from config.json) + -o Repository owner (default: MetOffice) + -p Permission (read, write, admin, maintain, triage; + default: read) + -d Remove user as collaborator instead of adding + -n, --dry-run Print actions without making changes + -h, --help Show this help message + +Examples: + # Add/Remove a user to/from all repositories defined in config.json + ${0##*/} -u [-p ] + ${0##*/} -u -d + + # Add/Remove a user to/from a specific repository + ${0##*/} -u -r [-o ] [-p ] + ${0##*/} -u -r -o -d + +EOF + exit 1 +} + +# -- Defaults +PERMISSION="read" +OWNER="MetOffice" +DELETE=0 +CONFIG_JSON="$(dirname "${BASH_SOURCE[0]}")/../git-migration/config.json" +REPOS=() +DRY_RUN=0 + +# -- Helper functions +remove_collaborator() { + local repo_name="$1" + local repo_api="/repos/${OWNER}/${repo_name}/collaborators/${USERNAME}" + + if [ "$DRY_RUN" -eq 1 ]; then + echo "[DRY-RUN] Would remove '${USERNAME}' from ${OWNER}/${repo_name}." + else + gh api --method DELETE "$repo_api" && \ + echo "Removed '${USERNAME}' from ${OWNER}/${repo_name}." || \ + echo "Failed to remove '${USERNAME}' from ${OWNER}/${repo_name}." >&2 + fi +} + +add_collaborator() { + local repo_name="$1" + local repo_api="/repos/${OWNER}/${repo_name}/collaborators/${USERNAME}" + + # Check if user is already a collaborator + if [ "$DRY_RUN" -eq 0 ] && gh api "$repo_api" --silent > /dev/null 2>&1; then + echo "User '${USERNAME}' already a collaborator on ${OWNER}/${repo_name}." + return 0 + fi + + if [ "$DRY_RUN" -eq 1 ]; then + echo "[DRY-RUN] Would add '${USERNAME}' to ${OWNER}/${repo_name} with" \ + "permission '$PERMISSION'." + else + if gh api --method PUT "$repo_api" -f permission="$PERMISSION"; then + echo "Added '${USERNAME}' to ${OWNER}/${repo_name} with permission" \ + "'$PERMISSION'." + else + echo "Failed to add '${USERNAME}' to ${OWNER}/${repo_name}." >&2 + fi + fi +} + +# -- Parse options +while getopts "u:r:o:p:dnh-:" opt; do + case $opt in + u) USERNAME="$OPTARG" ;; + r) REPO="$OPTARG" ;; + o) OWNER="$OPTARG" ;; + p) + case $OPTARG in + read|pull) PERMISSION="read" ;; + write|push) PERMISSION="write" ;; + admin|maintain|triage) PERMISSION="$OPTARG" ;; + *) echo "Invalid permission: $OPTARG"; usage ;; + esac + ;; + d) DELETE=1 ;; + n) DRY_RUN=1 ;; + h) usage ;; + -) [ "$OPTARG" = "help" ] && usage ;; + *) usage ;; + esac +done + +# -- Validate required args (username is mandatory) +if [ -z "${USERNAME:-}" ]; then + usage +fi + +# -- Populate REPOS array from config.json +# Only populate REPOS from config.json if no explicit -r repo provided +if [ -z "${REPO:-}" ]; then + if command -v jq >/dev/null 2>&1 && [ -f "$CONFIG_JSON" ]; then + mapfile -t REPOS < <(jq -r '.repo[].name' "$CONFIG_JSON") + else + REPOS=() + fi +fi + +# -- Determine target repos +TARGET_REPOS=() +if [ -n "${REPO:-}" ]; then + TARGET_REPOS=("$REPO") +else + # No repo provided: operate on all repos from config.json + if [ ${#REPOS[@]} -eq 0 ]; then + echo "No -r provided and repo list empty/unavailable." >&2 + echo "Install 'jq' and ensure config exists at: $CONFIG_JSON" >&2 + exit 1 + fi + TARGET_REPOS=("${REPOS[@]}") +fi + +# -- Process each target repo +for repo_name in "${TARGET_REPOS[@]}"; do + if [ "$DELETE" -eq 1 ]; then + remove_collaborator "$repo_name" + else + add_collaborator "$repo_name" + fi +done diff --git a/sbin/gh_manage_labels b/sbin/gh_manage_labels new file mode 100755 index 00000000..15d78b72 --- /dev/null +++ b/sbin/gh_manage_labels @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# ---------------------------------------------------------------------------- +# (C) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ---------------------------------------------------------------------------- + +# Script to create standard labels in multiple GitHub repositories +# Requires GitHub CLI: https://cli.github.com/ and Admin privileges to the repos + +set -euo pipefail + +# -- Parse options +ADD_LABELS=true +DELETE_LABELS=false +usage() { + echo "Usage: $0 [--add|--delete] [--help|-h]" + echo " --add Add labels (default)" + echo " --delete Delete labels" + echo " --help,-h Show this help message" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --add) + ADD_LABELS=true + DELETE_LABELS=false + shift ;; + --delete) + ADD_LABELS=false + DELETE_LABELS=true + shift ;; + --help|-h) + usage + exit 0 ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 ;; + esac +done + +# -- Label Settings +# Format: "label_name|hex_color|description" +labels=( + "Linked UM|#50bfe6|This PR is linked to a MetOffice/um PR" + "Linked JULES|#66ff66|This PR is linked to a MetOffice/jules PR" + "Linked Apps|#ff6037|This PR is linked to a MetOffice/lfric_apps PR" + "Linked Core|#ffff66|This PR is linked to a MetOffice/lfric_core PR" + "Linked UKCA|#ff00cc|This PR is linked to a MetOffice/ukca PR" + "Linked CASIM|#fd5b78|This PR is linked to a MetOffice/casim PR" + "Linked SOCRATES|#16d0cb|This PR is linked to a MetOffice/socrates PR" + "Linked Mule|#00ffff|This PR is linked to a MetOffice/mule PR" + "Linked Shumlib|#0080ff|This PR is linked to a MetOffice/shumlib PR" + "KGO|#9c27b0|This PR contains changes to KGO" + "macro|#aaf0d1|This PR contains a metadata upgrade macro" + "Accessibility|#1c96b9|Problems with the accessibility of the documentation" + "Discussion|#FBCA04|Issues that require some formal discussion" + "cla-required|#b60205|The CLA has not yet been signed by the author of this PR - added by GA" + "cla-signed|#0052cc|The CLA has been signed as part of this PR - added by GA" +) + +# -- Add labels in relevant repositories +repos=( + "MetOffice/um" + "MetOffice/jules" + "MetOffice/lfric_apps" + "MetOffice/lfric_core" + "MetOffice/ukca" + "MetOffice/casim" + "MetOffice/socrates" + "MetOffice/mule" + "MetOffice/shumlib" + "MetOffice/um_aux" + "MetOffice/um_doc" + "MetOffice/um_meta" + "MetOffice/socrates-spectral" + "MetOffice/moci" +) + +# -- Extract labels and colors + +for repo in "${repos[@]}"; do + echo "Processing labels in repository: $repo" + for label in "${labels[@]}"; do + name=$(echo "$label" | cut -d'|' -f1) + color=$(echo "$label" | cut -d'|' -f2) + description=$(echo "$label" | cut -d'|' -f3) + linked_repo=$(echo "$label" | grep -oP 'MetOffice/\w+' || true) + + [[ "$repo" == "$linked_repo" ]] && continue + + if [[ "$ADD_LABELS" == true ]]; then + gh label create "$name" \ + --color "$color" \ + --description "$description" \ + --force \ + --repo "$repo" + fi + if [[ "$DELETE_LABELS" == true ]]; then + gh label delete "$name" --yes --repo "$repo" || true + fi + done +done + +if [[ "$ADD_LABELS" == true ]]; then + echo "Labels added." +elif [[ "$DELETE_LABELS" == true ]]; then + echo "Labels deleted." +fi From 7f8ab335e645a995f6e7ac964a109a01db32e226 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:41:32 +0000 Subject: [PATCH 2/6] Reduce API calls --- sbin/gh_add_user | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sbin/gh_add_user b/sbin/gh_add_user index a81b43da..26a3818a 100755 --- a/sbin/gh_add_user +++ b/sbin/gh_add_user @@ -8,9 +8,11 @@ # Add collaborator to a repository (requires admin privilege on the repo) # gh api --method PUT /repos/:owner/:repo/collaborators/:username -# yaswant.pradhan +# Requires: +# GitHub CLI (gh): https://cli.github.com/ +# Commandline JSON processor (jq): https://jqlang.org/ +# Admin privileges to the repos -# add a user to a repository via command line options set -euo pipefail usage() { @@ -65,21 +67,19 @@ add_collaborator() { local repo_name="$1" local repo_api="/repos/${OWNER}/${repo_name}/collaborators/${USERNAME}" - # Check if user is already a collaborator - if [ "$DRY_RUN" -eq 0 ] && gh api "$repo_api" --silent > /dev/null 2>&1; then - echo "User '${USERNAME}' already a collaborator on ${OWNER}/${repo_name}." - return 0 - fi - if [ "$DRY_RUN" -eq 1 ]; then echo "[DRY-RUN] Would add '${USERNAME}' to ${OWNER}/${repo_name} with" \ "permission '$PERMISSION'." else - if gh api --method PUT "$repo_api" -f permission="$PERMISSION"; then - echo "Added '${USERNAME}' to ${OWNER}/${repo_name} with permission" \ - "'$PERMISSION'." + # PUT method is idempotent: adds user if new, updates permission if exists + # HTTP 201 = created (new), 204 = updated (existing) + if gh api --method PUT "$repo_api" -f permission="$PERMISSION" \ + --silent 2>/dev/null; then + echo "Added/Updated '${USERNAME}' on ${OWNER}/${repo_name} with" \ + "permission '$PERMISSION'." else echo "Failed to add '${USERNAME}' to ${OWNER}/${repo_name}." >&2 + return 1 fi fi } From 7e2dbd2bd414c4a2a6acaab7ed7cd2fdb1320c3b Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:53:19 +0000 Subject: [PATCH 3/6] Protect admin --- sbin/gh_add_user | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/sbin/gh_add_user b/sbin/gh_add_user index 26a3818a..9e306fd2 100755 --- a/sbin/gh_add_user +++ b/sbin/gh_add_user @@ -50,11 +50,28 @@ REPOS=() DRY_RUN=0 # -- Helper functions +check_admin_permission() { + local repo_name="$1" + local repo_api="/repos/${OWNER}/${repo_name}/collaborators/${USERNAME}" + local permission + + # Returns 0 if user is admin, 1 otherwise + permission=$(gh api "$repo_api" --jq '.role_name' 2>/dev/null) + [[ "$permission" == "admin" ]] +} + remove_collaborator() { local repo_name="$1" local repo_api="/repos/${OWNER}/${repo_name}/collaborators/${USERNAME}" - if [ "$DRY_RUN" -eq 1 ]; then + # Check current permission level + if (( !DRY_RUN )) && check_admin_permission "$repo_name"; then + echo "WARNING: '${USERNAME}' has admin role on ${OWNER}/${repo_name}." >&2 + echo "Skipping removal to prevent loss of admin access." >&2 + return 0 + fi + + if (( DRY_RUN )); then echo "[DRY-RUN] Would remove '${USERNAME}' from ${OWNER}/${repo_name}." else gh api --method DELETE "$repo_api" && \ @@ -67,7 +84,14 @@ add_collaborator() { local repo_name="$1" local repo_api="/repos/${OWNER}/${repo_name}/collaborators/${USERNAME}" - if [ "$DRY_RUN" -eq 1 ]; then + # Check current permission level to prevent admin role changes + if (( !DRY_RUN )) && check_admin_permission "$repo_name"; then + echo "WARNING: '${USERNAME}' has admin role on ${OWNER}/${repo_name}." >&2 + echo "Skipping to prevent modification of admin permissions." >&2 + return 0 + fi + + if (( DRY_RUN )); then echo "[DRY-RUN] Would add '${USERNAME}' to ${OWNER}/${repo_name} with" \ "permission '$PERMISSION'." else From e4374f77b91225ec3b0f330b791f546199f2813a Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 2 Dec 2025 18:43:15 +0000 Subject: [PATCH 4/6] Fix admin_permission helper function --- sbin/gh_add_user | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sbin/gh_add_user b/sbin/gh_add_user index 9e306fd2..b345c56a 100755 --- a/sbin/gh_add_user +++ b/sbin/gh_add_user @@ -13,6 +13,12 @@ # Commandline JSON processor (jq): https://jqlang.org/ # Admin privileges to the repos +# WARNINGS: +# - This script modifies repository permissions. Use with caution. +# - Always verify the current permissions before making changes. +# - Removing a user with admin privileges may lead to loss of access. +# - It also impacts API rate limits for the authenticated user. + set -euo pipefail usage() { @@ -20,12 +26,12 @@ usage() { Usage: ${0##*/} -u [-r ] [options] -u GitHub username to add/remove -r Repository name (if omitted, operate on all repos - from config.json) + from ../git-migration/config.json) -o Repository owner (default: MetOffice) -p Permission (read, write, admin, maintain, triage; default: read) -d Remove user as collaborator instead of adding - -n, --dry-run Print actions without making changes + -n Print actions without making changes -h, --help Show this help message Examples: @@ -56,7 +62,7 @@ check_admin_permission() { local permission # Returns 0 if user is admin, 1 otherwise - permission=$(gh api "$repo_api" --jq '.role_name' 2>/dev/null) + permission=$(gh api "${repo_api}/permission" --jq '.role_name' 2>/dev/null) [[ "$permission" == "admin" ]] } From f4249896005ae6d817be25eff3b632bb0c50a009 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:01:32 +0000 Subject: [PATCH 5/6] Refactor collaborator management to check admin permissions once before operations --- sbin/gh_add_user | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/sbin/gh_add_user b/sbin/gh_add_user index b345c56a..51514c91 100755 --- a/sbin/gh_add_user +++ b/sbin/gh_add_user @@ -70,13 +70,6 @@ remove_collaborator() { local repo_name="$1" local repo_api="/repos/${OWNER}/${repo_name}/collaborators/${USERNAME}" - # Check current permission level - if (( !DRY_RUN )) && check_admin_permission "$repo_name"; then - echo "WARNING: '${USERNAME}' has admin role on ${OWNER}/${repo_name}." >&2 - echo "Skipping removal to prevent loss of admin access." >&2 - return 0 - fi - if (( DRY_RUN )); then echo "[DRY-RUN] Would remove '${USERNAME}' from ${OWNER}/${repo_name}." else @@ -90,13 +83,6 @@ add_collaborator() { local repo_name="$1" local repo_api="/repos/${OWNER}/${repo_name}/collaborators/${USERNAME}" - # Check current permission level to prevent admin role changes - if (( !DRY_RUN )) && check_admin_permission "$repo_name"; then - echo "WARNING: '${USERNAME}' has admin role on ${OWNER}/${repo_name}." >&2 - echo "Skipping to prevent modification of admin permissions." >&2 - return 0 - fi - if (( DRY_RUN )); then echo "[DRY-RUN] Would add '${USERNAME}' to ${OWNER}/${repo_name} with" \ "permission '$PERMISSION'." @@ -159,7 +145,7 @@ else # No repo provided: operate on all repos from config.json if [ ${#REPOS[@]} -eq 0 ]; then echo "No -r provided and repo list empty/unavailable." >&2 - echo "Install 'jq' and ensure config exists at: $CONFIG_JSON" >&2 + echo "Make sure 'jq' is installed and config exists at: $CONFIG_JSON" >&2 exit 1 fi TARGET_REPOS=("${REPOS[@]}") @@ -167,7 +153,14 @@ fi # -- Process each target repo for repo_name in "${TARGET_REPOS[@]}"; do - if [ "$DELETE" -eq 1 ]; then + # Check admin permission once before any operation (skip in dry-run mode) + if (( !DRY_RUN )) && check_admin_permission "$repo_name"; then + echo "WARNING: '${USERNAME}' has admin role on ${OWNER}/${repo_name}." >&2 + echo "Skipping to prevent modification of admin permissions." >&2 + continue + fi + + if (( DELETE )); then remove_collaborator "$repo_name" else add_collaborator "$repo_name" From e9bce89aa066706bd1f8dd5b3a80e75cc1fa7bae Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:37:25 +0000 Subject: [PATCH 6/6] Prevent self-removal --- sbin/gh_add_user | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sbin/gh_add_user b/sbin/gh_add_user index 51514c91..d24d66a1 100755 --- a/sbin/gh_add_user +++ b/sbin/gh_add_user @@ -6,7 +6,7 @@ # under which the code may be used. # ---------------------------------------------------------------------------- -# Add collaborator to a repository (requires admin privilege on the repo) +# Add collaborator to a repository # gh api --method PUT /repos/:owner/:repo/collaborators/:username # Requires: # GitHub CLI (gh): https://cli.github.com/ @@ -16,7 +16,6 @@ # WARNINGS: # - This script modifies repository permissions. Use with caution. # - Always verify the current permissions before making changes. -# - Removing a user with admin privileges may lead to loss of access. # - It also impacts API rate limits for the authenticated user. set -euo pipefail @@ -151,8 +150,21 @@ else TARGET_REPOS=("${REPOS[@]}") fi +# -- Get current user once if needed for self-removal check +CURRENT_USER="" +if (( DELETE && !DRY_RUN )); then + CURRENT_USER=$(gh api user --jq '.login' 2>/dev/null) +fi + # -- Process each target repo for repo_name in "${TARGET_REPOS[@]}"; do + # Prevent users from removing themselves + if [[ -n "$CURRENT_USER" ]] && [[ "$CURRENT_USER" == "$USERNAME" ]]; then + echo "WARNING: Won't remove (${USERNAME}) from ${OWNER}/${repo_name}" >&2 + echo "Skipping self-removal to prevent loss of your own access." >&2 + continue + fi + # Check admin permission once before any operation (skip in dry-run mode) if (( !DRY_RUN )) && check_admin_permission "$repo_name"; then echo "WARNING: '${USERNAME}' has admin role on ${OWNER}/${repo_name}." >&2