Skip to content

Commit b16f89c

Browse files
committed
Add Tag manager script
1 parent 674ed15 commit b16f89c

1 file changed

Lines changed: 336 additions & 0 deletions

File tree

sbin/tagman

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
#!/usr/bin/env bash
2+
# -----------------------------------------------------------------------------
3+
# (C) Crown copyright Met Office. All rights reserved.
4+
# The file LICENCE, distributed with this code, contains details of the terms
5+
# under which the code may be used.
6+
# -----------------------------------------------------------------------------
7+
#
8+
# Script to manage Git tags (add/delete/list).
9+
# Requires:
10+
# GitHub CLI (gh): https://cli.github.com/
11+
# Git command-line tool: https://git-scm.com/
12+
# Warnings:
13+
# - This script modifies Git tags. Use with caution.
14+
# - Always verify the current tags before making changes.
15+
16+
set -euo pipefail
17+
# Colour codes for output
18+
GRN='\033[0;32m'
19+
RED='\033[0;31m'
20+
YLW='\033[0;33m'
21+
NC='\033[0m'
22+
23+
# Default values
24+
DEFAULT_REPO="MetOffice/git_playground"
25+
REPO="${REPO:-$DEFAULT_REPO}"
26+
# Variables set by parse_args()
27+
REF=""
28+
TAG=""
29+
MESSAGE=""
30+
DRY_RUN=false
31+
32+
usage() {
33+
cat <<EOF
34+
Usage:
35+
$(basename "$0") add <tag_name> <commit_ref> [options]
36+
$(basename "$0") delete|del <tag_name> [options]
37+
$(basename "$0") list|ls [options]
38+
39+
Actions:
40+
add Create and push a new tag
41+
delete Delete a tag from the repository (alias: del)
42+
list List all tags in the repository (alias: ls)
43+
44+
Arguments:
45+
<tag_name> Name of the tag to create or delete
46+
<commit_ref> Commit SHA, tag name, release name, or branch name
47+
48+
Options:
49+
--repo, -R REPO Repository in format owner/repo (default: $DEFAULT_REPO)
50+
--message MSG Tag annotation message (for add action)
51+
--dry-run, -n Show what would be done without making changes
52+
53+
Examples:
54+
# Create tag from commit SHA
55+
$(basename "$0") add v1.0.0 abc123def --repo MetOffice/git_playground
56+
57+
# Create tag from existing tag
58+
$(basename "$0") add Test vn1.5 --repo MetOffice/git_playground
59+
60+
# Create tag from release
61+
$(basename "$0") add Autumn2025 vn14.0 --repo MetOffice/um
62+
63+
# Create tag from branch
64+
$(basename "$0") add Spring2026 main --repo MetOffice/SimSys_Scripts
65+
66+
# Delete tag
67+
$(basename "$0") del 2025.12.0 --repo MetOffice/SimSys_Scripts
68+
69+
# List tags
70+
$(basename "$0") ls --repo MetOffice/SimSys_Scripts
71+
72+
Notes:
73+
- REPO can be set via environment variable (default: $DEFAULT_REPO)
74+
- All other parameters must be provided via command-line arguments
75+
- Use --dry-run to preview changes before executing
76+
EOF
77+
exit 1
78+
}
79+
80+
cleanup() {
81+
[[ -n "${WORK_TMP:-}" ]] && rm -rf "$WORK_TMP"
82+
}
83+
84+
confirm() {
85+
local message="$1"
86+
local response
87+
echo -en "${YLW}"
88+
read -rp "$message (y/n): " response
89+
echo -en "${NC}"
90+
91+
case "$response" in
92+
[yY][eE][sS] | [yY])
93+
return 0 ;;
94+
*)
95+
echo "Aborted..."
96+
return 1 ;;
97+
esac
98+
}
99+
100+
run() {
101+
local msg="$1"
102+
shift
103+
local timestamp
104+
timestamp=$(date "+%F %T")
105+
106+
if "$@"; then
107+
echo -e "[$timestamp] ${GRN}${NC} $msg succeeded."
108+
return 0
109+
else
110+
echo -e "[$timestamp] ${RED}${NC} $msg failed."
111+
return 1
112+
fi
113+
}
114+
115+
trap cleanup EXIT ERR SIGINT
116+
117+
verify_tag() {
118+
gh api "repos/${REPO}/git/refs/tags/${TAG}" >/dev/null 2>&1 && {
119+
echo -e "${YLW}Tag '$TAG' exists in repository '$REPO'.${NC}"
120+
return 0
121+
}
122+
return 1
123+
}
124+
125+
verify_ref() {
126+
local resolved_sha=""
127+
128+
# First, try to resolve as a commit SHA (handles both short and full)
129+
if resolved_sha=$(gh api "repos/${REPO}/commits/${REF}" --jq '.sha' 2>/dev/null); then
130+
REF="$resolved_sha"
131+
echo -e "${GRN}Using commit SHA: $REF${NC}"
132+
return 0
133+
fi
134+
135+
# Try to resolve as a tag
136+
if gh api "repos/${REPO}/git/refs/tags/${REF}" >/dev/null 2>&1; then
137+
echo -e "${YLW}Resolving tag '$REF' to commit SHA...${NC}"
138+
local tag_sha
139+
tag_sha=$(gh api "repos/${REPO}/git/refs/tags/${REF}" --jq '.object.sha')
140+
141+
# Try to get tag object to determine if it's annotated
142+
local tag_info
143+
if tag_info=$(gh api "repos/${REPO}/git/tags/${tag_sha}" 2>/dev/null); then
144+
# It's an annotated tag - get the commit SHA it points to
145+
local tag_type
146+
tag_type=$(echo "$tag_info" | jq -r '.object.type')
147+
148+
if [[ "$tag_type" == "commit" ]]; then
149+
resolved_sha=$(echo "$tag_info" | jq -r '.object.sha')
150+
else
151+
echo -e "${RED}** Tag points to unexpected object type: $tag_type${NC}"
152+
return 1
153+
fi
154+
else
155+
# It's a lightweight tag - the SHA is the commit SHA
156+
resolved_sha="$tag_sha"
157+
fi
158+
159+
# Verify it's a full SHA and a valid commit
160+
if resolved_sha=$(gh api "repos/${REPO}/commits/${resolved_sha}" --jq '.sha' 2>/dev/null); then
161+
REF="$resolved_sha"
162+
echo -e "${GRN}Resolved to commit: $REF${NC}"
163+
return 0
164+
else
165+
echo -e "${RED}** Failed to verify commit SHA from tag${NC}"
166+
return 1
167+
fi
168+
fi
169+
170+
# Try to resolve as a release
171+
if gh api "repos/${REPO}/releases/tags/${REF}" >/dev/null 2>&1; then
172+
echo -e "${YLW}Resolving release '$REF' to commit SHA...${NC}"
173+
local target_ref
174+
target_ref=$(gh api "repos/${REPO}/releases/tags/${REF}" --jq '.target_commitish')
175+
176+
# Resolve the target to full SHA
177+
if resolved_sha=$(gh api "repos/${REPO}/commits/${target_ref}" --jq '.sha' 2>/dev/null); then
178+
REF="$resolved_sha"
179+
echo -e "${GRN}Resolved to commit: $REF${NC}"
180+
return 0
181+
else
182+
echo -e "${RED}** Failed to resolve release target to commit SHA${NC}"
183+
return 1
184+
fi
185+
fi
186+
187+
# Try as a branch name
188+
if gh api "repos/${REPO}/git/refs/heads/${REF}" >/dev/null 2>&1; then
189+
echo -e "${YLW}Resolving branch '$REF' to commit SHA...${NC}"
190+
local branch_sha
191+
branch_sha=$(gh api "repos/${REPO}/git/refs/heads/${REF}" --jq '.object.sha')
192+
193+
# Verify it's a full SHA
194+
if resolved_sha=$(gh api "repos/${REPO}/commits/${branch_sha}" --jq '.sha' 2>/dev/null); then
195+
REF="$resolved_sha"
196+
echo -e "${GRN}Resolved to commit: $REF${NC}"
197+
return 0
198+
else
199+
echo -e "${RED}** Failed to verify commit SHA from branch${NC}"
200+
return 1
201+
fi
202+
fi
203+
204+
echo -e "${RED}** Reference '$REF' not found in repository '$REPO'.${NC}"
205+
echo -e "${RED}** Tried: commit SHA, tag, release, and branch name.${NC}"
206+
return 1
207+
}
208+
209+
add_tag() {
210+
[[ -z "$TAG" || -z "$REF" ]] && {
211+
echo -e "${RED}** TAG and REF are required for add action.${NC}"
212+
usage
213+
}
214+
215+
verify_tag && exit 1
216+
verify_ref || exit 1
217+
218+
local url="https://github.com/${REPO}.git"
219+
local msg="${MESSAGE:-"Tagging $TAG @ $REF"}"
220+
221+
if [[ "$DRY_RUN" == true ]]; then
222+
echo -e "${YLW}[DRY RUN] Would create tag with the following details:${NC}"
223+
echo -e " Repository: $REPO"
224+
echo -e " Tag name: $TAG"
225+
echo -e " Commit SHA: $REF"
226+
echo -e " Message: $msg"
227+
echo -e "${YLW}[DRY RUN] No changes made.${NC}"
228+
return 0
229+
fi
230+
231+
WORK_TMP=$(mktemp -d -t tagman-XXXX)
232+
233+
pushd "$WORK_TMP" >/dev/null
234+
run "Initialise temporary Git repository in $WORK_TMP" git init --bare --quiet
235+
run "Add remote repository $url" git remote add origin "$url"
236+
run "Fetch commit $REF" git fetch --quiet --depth 1 origin "$REF"
237+
run "Create and sign tag '$TAG' at commit $REF" \
238+
git tag --sign "$TAG" "$REF" --message "$msg"
239+
run "Push '$TAG' to remote '$REPO'" git push --quiet origin "$TAG"
240+
popd >/dev/null
241+
242+
echo -e "${GRN}Successfully created and pushed tag '$TAG' to '$REPO'.${NC}"
243+
}
244+
245+
delete_tag() {
246+
[[ -z "$TAG" ]] && {
247+
echo -e "${RED}** TAG is required for delete action.${NC}"
248+
usage
249+
}
250+
251+
verify_tag || {
252+
echo -e "${RED}** Tag '$TAG' does not exist in repository '$REPO'.${NC}"
253+
return 1
254+
}
255+
256+
if [[ "$DRY_RUN" == true ]]; then
257+
echo -e "${YLW}[DRY RUN] Would delete tag with the following details:${NC}"
258+
echo -e " Repository: $REPO"
259+
echo -e " Tag name: $TAG"
260+
echo -e "${YLW}[DRY RUN] No changes made.${NC}"
261+
return 0
262+
fi
263+
264+
local url="https://github.com/${REPO}.git"
265+
266+
WORK_TMP=$(mktemp -d -t tagman-XXXX)
267+
268+
pushd "$WORK_TMP" >/dev/null
269+
run "Initialise temporary Git repository in $WORK_TMP" git init --bare --quiet
270+
run "Add remote repository $url" git remote add origin "$url"
271+
272+
if confirm "Are you sure you want to delete the tag '$TAG' from '$REPO'?"; then
273+
run "Delete remote tag '$TAG' from '$REPO'" git push --quiet origin --delete "$TAG"
274+
echo -e "${GRN}Successfully deleted tag '$TAG' from '$REPO'.${NC}"
275+
fi
276+
popd >/dev/null
277+
}
278+
279+
list_tags() {
280+
local url="https://github.com/${REPO}.git"
281+
echo -e "${GRN}Listing tags from '$REPO':${NC}\n"
282+
git ls-remote --tags --sort="-version:refname" "$url"
283+
}
284+
285+
parse_args() {
286+
[[ $# -eq 0 ]] && usage
287+
288+
ACTION="$1"
289+
shift
290+
291+
case "$ACTION" in
292+
add)
293+
[[ $# -lt 2 ]] && usage
294+
TAG="$1"
295+
REF="$2"
296+
shift 2
297+
;;
298+
del|delete)
299+
[[ $# -lt 1 ]] && usage
300+
TAG="$1"
301+
shift
302+
;;
303+
ls|list)
304+
# No arguments required
305+
;;
306+
*)
307+
echo -e "${RED}Unknown action: $ACTION${NC}"
308+
usage
309+
;;
310+
esac
311+
312+
# Parse optional flags
313+
while [[ $# -gt 0 ]]; do
314+
case "$1" in
315+
-R|--repo) REPO="$2"; shift 2 ;;
316+
--message) MESSAGE="$2"; shift 2 ;;
317+
-n|--dry-run) DRY_RUN=true; shift ;;
318+
*)
319+
echo -e "${RED}Unknown option: $1${NC}"
320+
usage ;;
321+
esac
322+
done
323+
}
324+
325+
main() {
326+
parse_args "$@"
327+
328+
case "$ACTION" in
329+
add) add_tag ;;
330+
del|delete) delete_tag ;;
331+
ls|list) list_tags ;;
332+
*) echo -e "${RED}Invalid action: $ACTION${NC}" ;;
333+
esac
334+
}
335+
336+
main "$@"

0 commit comments

Comments
 (0)