From a86e533c3d7d6b330fe8adbc4c6e81b1dc86f42a Mon Sep 17 00:00:00 2001 From: jennyhickson <61183013+jennyhickson@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:19:44 +0000 Subject: [PATCH 1/4] create new script --- sbin/gh_manage_milestones | 140 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100755 sbin/gh_manage_milestones diff --git a/sbin/gh_manage_milestones b/sbin/gh_manage_milestones new file mode 100755 index 0000000..33b8ffb --- /dev/null +++ b/sbin/gh_manage_milestones @@ -0,0 +1,140 @@ +#!/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, update and close milestones in multiple GitHub repositories +# Requires GitHub CLI: https://cli.github.com/ and Admin privileges to the repos + +set -euo pipefail + +usage() { + cat < [options] + -t Title of milestone to be created, updated or closed. + -c Close milestone. Otherwise will create a new milestone or + update an existing milestone. + -d <due on> Milestone due date. Format: YYYY-MM-DDTHH:MM:SSZ + -p <description> Description of the milestone + -n Dry Run, print actions without making changes + -h, --help Show this help message + +Examples: + # Create a new milestone + ${0##*/} -t <title> [-d YYYY-MM-DDTHH:MM:SSZ] [-p <description>] + + # Update all milestones with a new date + ${0##*/} -t <title> -d <YYYY-MM-DDTHH:MM:SSZ> + + # Close a milestone + ${0##*/} -t <title> -c +EOF + exit 1 +} + +# -- Defaults +STATE="open" +TITLE="" +DUE="" +DESC="" +DRY_RUN=0 + +# -- Parse options +while getopts "t:d:p:cnh-:" opt; do + case $opt in + t) TITLE="$OPTARG" ;; + c) STATE="closed" ;; + d) DUE="$OPTARG" ;; + p) DESC="$OPTARG" ;; + n) DRY_RUN=1 ;; + h) usage ;; + -) [ "$OPTARG" = "help" ] && usage ;; + *) usage ;; + esac +done + +# -- Modify milestones in relevant repositories +repos=( + "MetOffice/um" + "MetOffice/jules" + "MetOffice/lfric_apps" + "MetOffice/lfric_core" + "MetOffice/ukca" + "MetOffice/casim" + "MetOffice/socrates" + "MetOffice/um_doc" + "MetOffice/simulation-systems" + "MetOffice/SimSys_Scripts" + "MetOffice/git_playground" +) +# +# -- Helper functions +get_milestone_number(){ + local repo_name="$1" + + gh api /repos/${repo_name}/milestones --jq ".[] | select(.title == \"${TITLE}\") | .number" +} + +run_command(){ + local gh_command="$1" + + if (( DRY_RUN )); then + echo "[DRY RUN] $gh_command" + else + eval "$gh_command" + fi +} + +# -- Change milestone for each repository + +for repo in "${repos[@]}"; do + echo "Processing milestone in repository: $repo" + + + # -- If milestone exists then fetch the number + NUMBER=0 + if [[ $(gh api /repos/${repo}/milestones --jq ".[] | select(.title == \"${TITLE}\")") ]]; then + NUMBER=$(get_milestone_number "${repo}") + echo "${TITLE} in ${repo} is milestone ${NUMBER}" + else + echo "Milestone does not exist in ${repo}" + fi + + # -- Build GH command from optional arguments. + GH_COMMAND="gh api -f \"title=${TITLE}\"" + if [[ ${DUE} ]]; then + GH_COMMAND=$GH_COMMAND" -f \"due_on=${DUE}\"" + fi + if [[ ${DESC} ]]; then + GH_COMMAND=$GH_COMMAND" -f \"description=${DESC}\"" + fi + + if [[ "$STATE" == "open" ]]; then + + if (( NUMBER )); then + echo "Updating milestone" + GH_COMMAND=$GH_COMMAND" --method PATCH" + GH_COMMAND=$GH_COMMAND" /repos/${repo}/milestones/${NUMBER}" + run_command "${GH_COMMAND}" + else + echo "Creating new milestone" + GH_COMMAND=$GH_COMMAND" --method POST" + GH_COMMAND=$GH_COMMAND" /repos/${repo}/milestones" + run_command "${GH_COMMAND}" + fi + + else + + if (( NUMBER )); then + echo "Closing milestone" + GH_COMMAND=$GH_COMMAND" --method PATCH" + GH_COMMAND=$GH_COMMAND" /repos/${repo}/milestones/${NUMBER}" + GH_COMMAND=$GH_COMMAND" -f \"state=closed\"" + run_command "${GH_COMMAND}" + fi + fi + +done From 611c4bf8b289f46638927bb5ddd74dee17ac4431 Mon Sep 17 00:00:00 2001 From: jennyhickson <61183013+jennyhickson@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:34:37 +0000 Subject: [PATCH 2/4] tidy up --- sbin/gh_manage_milestones | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sbin/gh_manage_milestones b/sbin/gh_manage_milestones index 33b8ffb..7d25161 100755 --- a/sbin/gh_manage_milestones +++ b/sbin/gh_manage_milestones @@ -74,10 +74,14 @@ repos=( # -- Helper functions get_milestone_number(){ local repo_name="$1" - gh api /repos/${repo_name}/milestones --jq ".[] | select(.title == \"${TITLE}\") | .number" } +check_exists(){ + local repo_name="$1" + gh api /repos/${repo_name}/milestones --jq ".[] | select(.title == \"${TITLE}\")" +} + run_command(){ local gh_command="$1" @@ -88,6 +92,7 @@ run_command(){ fi } + # -- Change milestone for each repository for repo in "${repos[@]}"; do @@ -96,7 +101,7 @@ for repo in "${repos[@]}"; do # -- If milestone exists then fetch the number NUMBER=0 - if [[ $(gh api /repos/${repo}/milestones --jq ".[] | select(.title == \"${TITLE}\")") ]]; then + if [[ $(check_exists "${repo}") ]]; then NUMBER=$(get_milestone_number "${repo}") echo "${TITLE} in ${repo} is milestone ${NUMBER}" else @@ -112,6 +117,7 @@ for repo in "${repos[@]}"; do GH_COMMAND=$GH_COMMAND" -f \"description=${DESC}\"" fi + # -- Create or update the milestone if [[ "$STATE" == "open" ]]; then if (( NUMBER )); then @@ -125,7 +131,8 @@ for repo in "${repos[@]}"; do GH_COMMAND=$GH_COMMAND" /repos/${repo}/milestones" run_command "${GH_COMMAND}" fi - + + # -- Close the milestone else if (( NUMBER )); then From 6634e90c6ff26475972b8ddce5d55b2084d6408f Mon Sep 17 00:00:00 2001 From: jennyhickson <61183013+jennyhickson@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:45:12 +0000 Subject: [PATCH 3/4] fix lint issue --- sbin/gh_manage_milestones | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sbin/gh_manage_milestones b/sbin/gh_manage_milestones index 7d25161..a5c647b 100755 --- a/sbin/gh_manage_milestones +++ b/sbin/gh_manage_milestones @@ -74,12 +74,12 @@ repos=( # -- Helper functions get_milestone_number(){ local repo_name="$1" - gh api /repos/${repo_name}/milestones --jq ".[] | select(.title == \"${TITLE}\") | .number" + gh api /repos/"${repo_name}"/milestones --jq ".[] | select(.title == \"${TITLE}\") | .number" } check_exists(){ local repo_name="$1" - gh api /repos/${repo_name}/milestones --jq ".[] | select(.title == \"${TITLE}\")" + gh api /repos/"${repo_name}"/milestones --jq ".[] | select(.title == \"${TITLE}\")" } run_command(){ From 569fd86e609e0056186b033998e6c84c533de6c4 Mon Sep 17 00:00:00 2001 From: jennyhickson <61183013+jennyhickson@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:06:06 +0000 Subject: [PATCH 4/4] refactor checking for existing milestones --- sbin/gh_manage_milestones | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sbin/gh_manage_milestones b/sbin/gh_manage_milestones index a5c647b..6a0a709 100755 --- a/sbin/gh_manage_milestones +++ b/sbin/gh_manage_milestones @@ -77,11 +77,6 @@ get_milestone_number(){ gh api /repos/"${repo_name}"/milestones --jq ".[] | select(.title == \"${TITLE}\") | .number" } -check_exists(){ - local repo_name="$1" - gh api /repos/"${repo_name}"/milestones --jq ".[] | select(.title == \"${TITLE}\")" -} - run_command(){ local gh_command="$1" @@ -100,9 +95,8 @@ for repo in "${repos[@]}"; do # -- If milestone exists then fetch the number - NUMBER=0 - if [[ $(check_exists "${repo}") ]]; then - NUMBER=$(get_milestone_number "${repo}") + NUMBER=$(get_milestone_number "${repo}") + if (( NUMBER )); then echo "${TITLE} in ${repo} is milestone ${NUMBER}" else echo "Milestone does not exist in ${repo}"