From b16f89cd68f514cabecb8275cb059152bd647b13 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 9 Feb 2026 21:46:07 +0000 Subject: [PATCH 1/6] Add Tag manager script --- sbin/tagman | 336 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100755 sbin/tagman diff --git a/sbin/tagman b/sbin/tagman new file mode 100755 index 00000000..2aec0574 --- /dev/null +++ b/sbin/tagman @@ -0,0 +1,336 @@ +#!/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 manage Git tags (add/delete/list). +# Requires: +# GitHub CLI (gh): https://cli.github.com/ +# Git command-line tool: https://git-scm.com/ +# Warnings: +# - This script modifies Git tags. Use with caution. +# - Always verify the current tags before making changes. + +set -euo pipefail +# Colour codes for output +GRN='\033[0;32m' +RED='\033[0;31m' +YLW='\033[0;33m' +NC='\033[0m' + +# Default values +DEFAULT_REPO="MetOffice/git_playground" +REPO="${REPO:-$DEFAULT_REPO}" +# Variables set by parse_args() +REF="" +TAG="" +MESSAGE="" +DRY_RUN=false + +usage() { + cat < [options] + $(basename "$0") delete|del [options] + $(basename "$0") list|ls [options] + +Actions: + add Create and push a new tag + delete Delete a tag from the repository (alias: del) + list List all tags in the repository (alias: ls) + +Arguments: + Name of the tag to create or delete + Commit SHA, tag name, release name, or branch name + +Options: + --repo, -R REPO Repository in format owner/repo (default: $DEFAULT_REPO) + --message MSG Tag annotation message (for add action) + --dry-run, -n Show what would be done without making changes + +Examples: + # Create tag from commit SHA + $(basename "$0") add v1.0.0 abc123def --repo MetOffice/git_playground + + # Create tag from existing tag + $(basename "$0") add Test vn1.5 --repo MetOffice/git_playground + + # Create tag from release + $(basename "$0") add Autumn2025 vn14.0 --repo MetOffice/um + + # Create tag from branch + $(basename "$0") add Spring2026 main --repo MetOffice/SimSys_Scripts + + # Delete tag + $(basename "$0") del 2025.12.0 --repo MetOffice/SimSys_Scripts + + # List tags + $(basename "$0") ls --repo MetOffice/SimSys_Scripts + +Notes: + - REPO can be set via environment variable (default: $DEFAULT_REPO) + - All other parameters must be provided via command-line arguments + - Use --dry-run to preview changes before executing +EOF + exit 1 +} + +cleanup() { + [[ -n "${WORK_TMP:-}" ]] && rm -rf "$WORK_TMP" +} + +confirm() { + local message="$1" + local response + echo -en "${YLW}" + read -rp "$message (y/n): " response + echo -en "${NC}" + + case "$response" in + [yY][eE][sS] | [yY]) + return 0 ;; + *) + echo "Aborted..." + return 1 ;; + esac +} + +run() { + local msg="$1" + shift + local timestamp + timestamp=$(date "+%F %T") + + if "$@"; then + echo -e "[$timestamp] ${GRN}✓${NC} $msg succeeded." + return 0 + else + echo -e "[$timestamp] ${RED}✗${NC} $msg failed." + return 1 + fi +} + +trap cleanup EXIT ERR SIGINT + +verify_tag() { + gh api "repos/${REPO}/git/refs/tags/${TAG}" >/dev/null 2>&1 && { + echo -e "${YLW}Tag '$TAG' exists in repository '$REPO'.${NC}" + return 0 + } + return 1 +} + +verify_ref() { + local resolved_sha="" + + # First, try to resolve as a commit SHA (handles both short and full) + if resolved_sha=$(gh api "repos/${REPO}/commits/${REF}" --jq '.sha' 2>/dev/null); then + REF="$resolved_sha" + echo -e "${GRN}Using commit SHA: $REF${NC}" + return 0 + fi + + # Try to resolve as a tag + if gh api "repos/${REPO}/git/refs/tags/${REF}" >/dev/null 2>&1; then + echo -e "${YLW}Resolving tag '$REF' to commit SHA...${NC}" + local tag_sha + tag_sha=$(gh api "repos/${REPO}/git/refs/tags/${REF}" --jq '.object.sha') + + # Try to get tag object to determine if it's annotated + local tag_info + if tag_info=$(gh api "repos/${REPO}/git/tags/${tag_sha}" 2>/dev/null); then + # It's an annotated tag - get the commit SHA it points to + local tag_type + tag_type=$(echo "$tag_info" | jq -r '.object.type') + + if [[ "$tag_type" == "commit" ]]; then + resolved_sha=$(echo "$tag_info" | jq -r '.object.sha') + else + echo -e "${RED}** Tag points to unexpected object type: $tag_type${NC}" + return 1 + fi + else + # It's a lightweight tag - the SHA is the commit SHA + resolved_sha="$tag_sha" + fi + + # Verify it's a full SHA and a valid commit + if resolved_sha=$(gh api "repos/${REPO}/commits/${resolved_sha}" --jq '.sha' 2>/dev/null); then + REF="$resolved_sha" + echo -e "${GRN}Resolved to commit: $REF${NC}" + return 0 + else + echo -e "${RED}** Failed to verify commit SHA from tag${NC}" + return 1 + fi + fi + + # Try to resolve as a release + if gh api "repos/${REPO}/releases/tags/${REF}" >/dev/null 2>&1; then + echo -e "${YLW}Resolving release '$REF' to commit SHA...${NC}" + local target_ref + target_ref=$(gh api "repos/${REPO}/releases/tags/${REF}" --jq '.target_commitish') + + # Resolve the target to full SHA + if resolved_sha=$(gh api "repos/${REPO}/commits/${target_ref}" --jq '.sha' 2>/dev/null); then + REF="$resolved_sha" + echo -e "${GRN}Resolved to commit: $REF${NC}" + return 0 + else + echo -e "${RED}** Failed to resolve release target to commit SHA${NC}" + return 1 + fi + fi + + # Try as a branch name + if gh api "repos/${REPO}/git/refs/heads/${REF}" >/dev/null 2>&1; then + echo -e "${YLW}Resolving branch '$REF' to commit SHA...${NC}" + local branch_sha + branch_sha=$(gh api "repos/${REPO}/git/refs/heads/${REF}" --jq '.object.sha') + + # Verify it's a full SHA + if resolved_sha=$(gh api "repos/${REPO}/commits/${branch_sha}" --jq '.sha' 2>/dev/null); then + REF="$resolved_sha" + echo -e "${GRN}Resolved to commit: $REF${NC}" + return 0 + else + echo -e "${RED}** Failed to verify commit SHA from branch${NC}" + return 1 + fi + fi + + echo -e "${RED}** Reference '$REF' not found in repository '$REPO'.${NC}" + echo -e "${RED}** Tried: commit SHA, tag, release, and branch name.${NC}" + return 1 +} + +add_tag() { + [[ -z "$TAG" || -z "$REF" ]] && { + echo -e "${RED}** TAG and REF are required for add action.${NC}" + usage + } + + verify_tag && exit 1 + verify_ref || exit 1 + + local url="https://github.com/${REPO}.git" + local msg="${MESSAGE:-"Tagging $TAG @ $REF"}" + + if [[ "$DRY_RUN" == true ]]; then + echo -e "${YLW}[DRY RUN] Would create tag with the following details:${NC}" + echo -e " Repository: $REPO" + echo -e " Tag name: $TAG" + echo -e " Commit SHA: $REF" + echo -e " Message: $msg" + echo -e "${YLW}[DRY RUN] No changes made.${NC}" + return 0 + fi + + WORK_TMP=$(mktemp -d -t tagman-XXXX) + + pushd "$WORK_TMP" >/dev/null + run "Initialise temporary Git repository in $WORK_TMP" git init --bare --quiet + run "Add remote repository $url" git remote add origin "$url" + run "Fetch commit $REF" git fetch --quiet --depth 1 origin "$REF" + run "Create and sign tag '$TAG' at commit $REF" \ + git tag --sign "$TAG" "$REF" --message "$msg" + run "Push '$TAG' to remote '$REPO'" git push --quiet origin "$TAG" + popd >/dev/null + + echo -e "${GRN}Successfully created and pushed tag '$TAG' to '$REPO'.${NC}" +} + +delete_tag() { + [[ -z "$TAG" ]] && { + echo -e "${RED}** TAG is required for delete action.${NC}" + usage + } + + verify_tag || { + echo -e "${RED}** Tag '$TAG' does not exist in repository '$REPO'.${NC}" + return 1 + } + + if [[ "$DRY_RUN" == true ]]; then + echo -e "${YLW}[DRY RUN] Would delete tag with the following details:${NC}" + echo -e " Repository: $REPO" + echo -e " Tag name: $TAG" + echo -e "${YLW}[DRY RUN] No changes made.${NC}" + return 0 + fi + + local url="https://github.com/${REPO}.git" + + WORK_TMP=$(mktemp -d -t tagman-XXXX) + + pushd "$WORK_TMP" >/dev/null + run "Initialise temporary Git repository in $WORK_TMP" git init --bare --quiet + run "Add remote repository $url" git remote add origin "$url" + + if confirm "Are you sure you want to delete the tag '$TAG' from '$REPO'?"; then + run "Delete remote tag '$TAG' from '$REPO'" git push --quiet origin --delete "$TAG" + echo -e "${GRN}Successfully deleted tag '$TAG' from '$REPO'.${NC}" + fi + popd >/dev/null +} + +list_tags() { + local url="https://github.com/${REPO}.git" + echo -e "${GRN}Listing tags from '$REPO':${NC}\n" + git ls-remote --tags --sort="-version:refname" "$url" +} + +parse_args() { + [[ $# -eq 0 ]] && usage + + ACTION="$1" + shift + + case "$ACTION" in + add) + [[ $# -lt 2 ]] && usage + TAG="$1" + REF="$2" + shift 2 + ;; + del|delete) + [[ $# -lt 1 ]] && usage + TAG="$1" + shift + ;; + ls|list) + # No arguments required + ;; + *) + echo -e "${RED}Unknown action: $ACTION${NC}" + usage + ;; + esac + + # Parse optional flags + while [[ $# -gt 0 ]]; do + case "$1" in + -R|--repo) REPO="$2"; shift 2 ;; + --message) MESSAGE="$2"; shift 2 ;; + -n|--dry-run) DRY_RUN=true; shift ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + usage ;; + esac + done +} + +main() { + parse_args "$@" + + case "$ACTION" in + add) add_tag ;; + del|delete) delete_tag ;; + ls|list) list_tags ;; + *) echo -e "${RED}Invalid action: $ACTION${NC}" ;; + esac +} + +main "$@" From e6c4c9836d007894420ac7af0aa7bf1df261849c Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:15:05 +0000 Subject: [PATCH 2/6] Add jq requirement and improve color code comments --- sbin/tagman | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sbin/tagman b/sbin/tagman index 2aec0574..179878f5 100755 --- a/sbin/tagman +++ b/sbin/tagman @@ -9,16 +9,17 @@ # Requires: # GitHub CLI (gh): https://cli.github.com/ # Git command-line tool: https://git-scm.com/ +# jq for JSON parsing: https://stedolan.github.io/jq/ # Warnings: # - This script modifies Git tags. Use with caution. # - Always verify the current tags before making changes. set -euo pipefail # Colour codes for output -GRN='\033[0;32m' -RED='\033[0;31m' -YLW='\033[0;33m' -NC='\033[0m' +GRN='\033[0;32m' # Green +RED='\033[0;31m' # Red +YLW='\033[0;33m' # Yellow +NC='\033[0m' # No Color # Default values DEFAULT_REPO="MetOffice/git_playground" From 1fb5fc3b6a8d94f62bb7d688dfede89da5c38c80 Mon Sep 17 00:00:00 2001 From: R Sharp Date: Fri, 13 Feb 2026 09:24:01 +0000 Subject: [PATCH 3/6] Update sbin/tagman --- sbin/tagman | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbin/tagman b/sbin/tagman index 179878f5..c4f6e738 100755 --- a/sbin/tagman +++ b/sbin/tagman @@ -116,7 +116,7 @@ run() { trap cleanup EXIT ERR SIGINT verify_tag() { - gh api "repos/${REPO}/git/refs/tags/${TAG}" >/dev/null 2>&1 && { + if gh api "repos/${REPO}/git/refs/tags/${TAG}" >/dev/null 2>&1 ; then echo -e "${YLW}Tag '$TAG' exists in repository '$REPO'.${NC}" return 0 } From 56133c64a2f3181e3509f18f54f3230523d8d38c Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:43:04 +0000 Subject: [PATCH 4/6] Refactor conditionals for clarity and consistency --- sbin/tagman | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/sbin/tagman b/sbin/tagman index 179878f5..b943edc9 100755 --- a/sbin/tagman +++ b/sbin/tagman @@ -79,7 +79,9 @@ EOF } cleanup() { - [[ -n "${WORK_TMP:-}" ]] && rm -rf "$WORK_TMP" + if [[ -n "${WORK_TMP:-}" ]]; then + rm -rf "$WORK_TMP" + fi } confirm() { @@ -89,8 +91,8 @@ confirm() { read -rp "$message (y/n): " response echo -en "${NC}" - case "$response" in - [yY][eE][sS] | [yY]) + case "${response^^}" in # Note: requires bash 4+ for ^^ operator + YES | Y) return 0 ;; *) echo "Aborted..." @@ -116,10 +118,10 @@ run() { trap cleanup EXIT ERR SIGINT verify_tag() { - gh api "repos/${REPO}/git/refs/tags/${TAG}" >/dev/null 2>&1 && { + if gh api "repos/${REPO}/git/refs/tags/${TAG}" >/dev/null 2>&1; then echo -e "${YLW}Tag '$TAG' exists in repository '$REPO'.${NC}" return 0 - } + fi return 1 } @@ -208,13 +210,13 @@ verify_ref() { } add_tag() { - [[ -z "$TAG" || -z "$REF" ]] && { + if [[ -z "$TAG" || -z "$REF" ]]; then echo -e "${RED}** TAG and REF are required for add action.${NC}" usage - } + fi - verify_tag && exit 1 - verify_ref || exit 1 + verify_tag && exit 1 # Tag already exists, exit with error + verify_ref || exit 1 # Reference resolution failed, exit with error local url="https://github.com/${REPO}.git" local msg="${MESSAGE:-"Tagging $TAG @ $REF"}" @@ -244,15 +246,15 @@ add_tag() { } delete_tag() { - [[ -z "$TAG" ]] && { + if [[ -z "$TAG" ]]; then echo -e "${RED}** TAG is required for delete action.${NC}" usage - } + fi - verify_tag || { + if ! verify_tag; then echo -e "${RED}** Tag '$TAG' does not exist in repository '$REPO'.${NC}" return 1 - } + fi if [[ "$DRY_RUN" == true ]]; then echo -e "${YLW}[DRY RUN] Would delete tag with the following details:${NC}" @@ -284,20 +286,26 @@ list_tags() { } parse_args() { - [[ $# -eq 0 ]] && usage + if [[ $# -eq 0 ]]; then + usage + fi ACTION="$1" shift case "$ACTION" in add) - [[ $# -lt 2 ]] && usage + if [[ $# -lt 2 ]]; then + usage + fi TAG="$1" REF="$2" shift 2 ;; del|delete) - [[ $# -lt 1 ]] && usage + if [[ $# -lt 1 ]]; then + usage + fi TAG="$1" shift ;; From b2d9710cbde4cdebcda46c5105d486f888758b4f Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:53:30 +0000 Subject: [PATCH 5/6] Reduce example calls in usage --- sbin/tagman | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/sbin/tagman b/sbin/tagman index b943edc9..f9b97205 100755 --- a/sbin/tagman +++ b/sbin/tagman @@ -52,24 +52,12 @@ Options: --dry-run, -n Show what would be done without making changes Examples: - # Create tag from commit SHA - $(basename "$0") add v1.0.0 abc123def --repo MetOffice/git_playground - - # Create tag from existing tag - $(basename "$0") add Test vn1.5 --repo MetOffice/git_playground - - # Create tag from release - $(basename "$0") add Autumn2025 vn14.0 --repo MetOffice/um - - # Create tag from branch - $(basename "$0") add Spring2026 main --repo MetOffice/SimSys_Scripts + # Create tag from commit SHA or existing tag or release or branch + $(basename "$0") add Test abc123def|vn1.5|main --repo MetOffice/git_playground # Delete tag $(basename "$0") del 2025.12.0 --repo MetOffice/SimSys_Scripts - # List tags - $(basename "$0") ls --repo MetOffice/SimSys_Scripts - Notes: - REPO can be set via environment variable (default: $DEFAULT_REPO) - All other parameters must be provided via command-line arguments From 549b7a50f41fe06d4c851e3a6a98ee36f0d5be8b Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:06:55 +0000 Subject: [PATCH 6/6] Update warnings in tagman script to emphasise responsibility --- sbin/tagman | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sbin/tagman b/sbin/tagman index f9b97205..984c6fd5 100755 --- a/sbin/tagman +++ b/sbin/tagman @@ -13,6 +13,9 @@ # Warnings: # - This script modifies Git tags. Use with caution. # - Always verify the current tags before making changes. +# - Anyone with push/write access to the repository can create or delete tags. +# Ensure you have the necessary permissions and understand the implications +# of modifying tags in a shared repository. set -euo pipefail # Colour codes for output