Skip to content

Commit c8f5a19

Browse files
Milestone manager (#155)
* create new script * tidy up * fix lint issue * refactor checking for existing milestones * replace command runner * Add list and delete functions * capitalise repos * add error catch for mode --------- Co-authored-by: James Bruten <109733895+james-bruten-mo@users.noreply.github.com>
1 parent 59a1f94 commit c8f5a19

File tree

1 file changed

+228
-0
lines changed

1 file changed

+228
-0
lines changed

sbin/gh_manage_milestones

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/env bash
2+
3+
# ----------------------------------------------------------------------------
4+
# (C) Crown copyright Met Office. All rights reserved.
5+
# The file LICENCE, distributed with this code, contains details of the terms
6+
# under which the code may be used.
7+
# ----------------------------------------------------------------------------
8+
9+
# Script to create, update and close milestones in multiple GitHub repositories
10+
# Requires GitHub CLI: https://cli.github.com/ and Admin privileges to the repos
11+
12+
set -euo pipefail
13+
14+
# -- Modify milestones in relevant repositories
15+
REPOS=(
16+
"MetOffice/um"
17+
"MetOffice/jules"
18+
"MetOffice/lfric_apps"
19+
"MetOffice/lfric_core"
20+
"MetOffice/ukca"
21+
"MetOffice/casim"
22+
"MetOffice/socrates"
23+
"MetOffice/um_doc"
24+
"MetOffice/simulation-systems"
25+
"MetOffice/SimSys_Scripts"
26+
"MetOffice/git_playground"
27+
)
28+
29+
usage() {
30+
cat <<EOF
31+
Usage: ${0##*/} -t <title> [options]
32+
-t <title> Title of milestone to be created, updated, closed or deleted.
33+
-m <mode> Mode: update (default), list, close, delete. Update mode will
34+
create a milestone if it doesn't exist.
35+
-s <state> State to list. State can be: open (default), closed or all.
36+
-d <due on> Milestone due date. Format: YYYY-MM-DDTHH:MM:SSZ
37+
-p <description> Description of the milestone
38+
-n Dry Run, print actions without making changes
39+
-h, --help Show this help message
40+
41+
Examples:
42+
# Create a new milestone
43+
${0##*/} -t <title> [-d YYYY-MM-DDTHH:MM:SSZ] [-p <description>]
44+
45+
# Update all milestones with a new date
46+
${0##*/} -t <title> -d <YYYY-MM-DDTHH:MM:SSZ>
47+
48+
# List all open milestones
49+
${0##*/} -m list
50+
51+
# List all milestones (open and closed)
52+
${0##*/} -m list -s all
53+
54+
# Close a milestone
55+
${0##*/} -t <title> -m close
56+
57+
# Delete a milestone (permanent)
58+
${0##*/} -t <title> -m delete
59+
EOF
60+
exit 1
61+
}
62+
63+
# -- Defaults
64+
MODE="update"
65+
STATE="open"
66+
TITLE=""
67+
DUE=""
68+
DESC=""
69+
DRY_RUN=0
70+
71+
# -- Parse options
72+
while getopts "t:m:s:d:p:nh-:" opt; do
73+
case $opt in
74+
t) TITLE="$OPTARG" ;;
75+
m) MODE="$OPTARG" ;;
76+
s) STATE="$OPTARG" ;;
77+
d) DUE="$OPTARG" ;;
78+
p) DESC="$OPTARG" ;;
79+
n) DRY_RUN=1 ;;
80+
h) usage ;;
81+
-) [ "$OPTARG" = "help" ] && usage ;;
82+
*) usage ;;
83+
esac
84+
done
85+
86+
#
87+
# -- Helper functions
88+
get_milestone_number(){
89+
local repo_name="$1"
90+
gh api /repos/"${repo_name}"/milestones --jq ".[] | select(.title == \"${TITLE}\") | .number"
91+
}
92+
93+
run_gh_api(){
94+
local method="$1"
95+
local endpoint="$2"
96+
shift 2
97+
local -a api_args=("$@")
98+
99+
if (( DRY_RUN )); then
100+
echo "[DRY RUN] gh api --method ${method} ${endpoint} ${api_args[*]}"
101+
return 0
102+
fi
103+
104+
gh api --method "${method}" "${endpoint}" "${api_args[@]}" > /dev/null
105+
}
106+
107+
confirm_deletion() {
108+
if (( DRY_RUN )); then
109+
return 0
110+
fi
111+
112+
echo "WARNING: You are about to PERMANENTLY DELETE milestone '${TITLE}' from ${#REPOS[@]} repositories."
113+
echo "This action CANNOT be undone."
114+
echo ""
115+
read -p "Type 'yes' to confirm deletion: " -r confirmation
116+
echo ""
117+
118+
if [[ "$confirmation" != "yes" ]]; then
119+
echo "Deletion cancelled."
120+
exit 0
121+
fi
122+
123+
echo "Proceeding with deletion..."
124+
echo ""
125+
}
126+
127+
list_milestones() {
128+
local repo="$1"
129+
local state="$2"
130+
131+
local milestones
132+
milestones=$(gh api "/repos/${repo}/milestones?state=${state}&per_page=100" \
133+
--jq '.[] | " #\(.number) \(.title) [\(.state)] (\(.open_issues) open / \(.closed_issues) closed)" +
134+
(if .due_on then " - Due: \(.due_on)" else "" end)' 2>/dev/null)
135+
136+
if [[ -n "$milestones" ]]; then
137+
echo "$milestones"
138+
else
139+
echo " No milestones found"
140+
fi
141+
echo ""
142+
}
143+
144+
145+
#
146+
# ----
147+
#
148+
149+
150+
# Check deletion once for all repos
151+
if [[ "$MODE" == "delete" ]]; then
152+
confirm_deletion
153+
fi
154+
155+
156+
# -- Change milestone for each repository
157+
158+
for repo in "${REPOS[@]}"; do
159+
echo "$repo"
160+
161+
# -- List all milestones in requested state
162+
if [[ "$MODE" == "list" ]]; then
163+
164+
echo " → Listing milestones (state: ${STATE})"
165+
list_milestones "${repo}" "${STATE}"
166+
167+
else
168+
# -- All other modes act on specific milestones.
169+
# -- If milestone exists then fetch the number
170+
number=$(get_milestone_number "${repo}")
171+
if (( number )); then
172+
echo " → Found existing milestone ${TITLE} #${number}"
173+
else
174+
echo " → Milestone does not exist"
175+
fi
176+
177+
# -- Build GH command from optional arguments.
178+
gh_args=(-f "title=\"${TITLE}\"")
179+
[[ -n "$DUE" ]] && gh_args+=(-f "due_on=${DUE}")
180+
[[ -n "$DESC" ]] && gh_args+=(-f "description=\"${DESC}\"")
181+
182+
183+
# -- Create or update the milestone
184+
if [[ "$MODE" == "update" ]]; then
185+
186+
if (( number )); then
187+
echo " → Updating milestone #${number}"
188+
run_gh_api PATCH "/repos/${repo}/milestones/${number}" "${gh_args[@]}"
189+
else
190+
echo " → Creating new milestone"
191+
run_gh_api POST "/repos/${repo}/milestones" "${gh_args[@]}"
192+
fi
193+
194+
# -- Close the milestone
195+
elif [[ "$MODE" == "close" ]]; then
196+
197+
if (( number )); then
198+
echo " → Closing milestone #${number}"
199+
gh_args+=(-f "state=closed")
200+
run_gh_api PATCH "/repos/${repo}/milestones/${number}" "${gh_args[@]}"
201+
else
202+
echo " → Skipping"
203+
fi
204+
205+
# -- Delete the milestone
206+
elif [[ "$MODE" == "delete" ]]; then
207+
208+
if (( number )); then
209+
echo " → Deleting milestone #${number}"
210+
run_gh_api DELETE "/repos/${repo}/milestones/${number}"
211+
else
212+
echo " → Skipping"
213+
fi
214+
215+
else
216+
217+
echo "Mode ${MODE} not recognised"
218+
echo "Options: update, list, close or delete"
219+
exit 0
220+
221+
fi
222+
223+
echo ""
224+
225+
fi
226+
227+
228+
done

0 commit comments

Comments
 (0)