Skip to content

feat(tag_release): add script for automatically tagging and releasing modules #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 23, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 275 additions & 0 deletions .github/scripts/tag_release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
#!/bin/bash

# Tag Release Script
# Automatically detects modules that need tagging and creates release tags
# Usage: ./tag_release.sh
# Operates on the current checked-out commit

set -euo pipefail

MODULES_TO_TAG=()

usage() {
echo "Usage: $0"
echo ""
echo "This script will:"
echo " 1. Scan all modules in the registry"
echo " 2. Check which modules need new release tags"
echo " 3. Extract version information from README files"
echo " 4. Generate a report for confirmation"
echo " 5. Create and push release tags after confirmation"
echo ""
echo "The script operates on the current checked-out commit."
echo "Make sure you have checked out the commit you want to tag before running."
exit 1
}

validate_version() {
local version="$1"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Invalid version format: '$version'. Expected X.Y.Z format." >&2
return 1
fi
return 0
}

extract_version_from_readme() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this logic in multiple scripts now. Thoughts on extracting it to a lib.sh file we can source where needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly this is a much better idea. We do use this code in a lot of places.

local readme_path="$1"
local namespace="$2"
local module_name="$3"

[ ! -f "$readme_path" ] && return 1

local version_line
version_line=$(grep -E "source\s*=\s*\"registry\.coder\.com/${namespace}/${module_name}" "$readme_path" | head -1 || echo "")

if [ -n "$version_line" ]; then
local version
version=$(echo "$version_line" | sed -n 's/.*version\s*=\s*"\([^"]*\)".*/\1/p')
if [ -n "$version" ]; then
echo "$version"
return 0
fi
fi

local fallback_version
fallback_version=$(grep -E 'version\s*=\s*"[0-9]+\.[0-9]+\.[0-9]+"' "$readme_path" | head -1 | sed 's/.*version\s*=\s*"\([^"]*\)".*/\1/' || echo "")

if [ -n "$fallback_version" ]; then
echo "$fallback_version"
return 0
fi

return 1
}

check_module_needs_tagging() {
local namespace="$1"
local module_name="$2"
local readme_version="$3"

local tag_name="release/${namespace}/${module_name}/v${readme_version}"

if git rev-parse --verify "$tag_name" > /dev/null 2>&1; then
return 1
else
return 0
fi
}

detect_modules_needing_tags() {
MODULES_TO_TAG=()

echo "πŸ” Scanning all modules for missing release tags..."
echo ""

local all_modules
all_modules=$(find registry -mindepth 3 -maxdepth 3 -type d -path "*/modules/*" | sort -u || echo "")

[ -z "$all_modules" ] && {
echo "❌ No modules found to check"
return 1
}

local total_checked=0
local needs_tagging=0

while IFS= read -r module_path; do
if [ -z "$module_path" ]; then continue; fi

local namespace
namespace=$(echo "$module_path" | cut -d'/' -f2)
local module_name
module_name=$(echo "$module_path" | cut -d'/' -f4)

total_checked=$((total_checked + 1))

local readme_path="$module_path/README.md"
local readme_version

if ! readme_version=$(extract_version_from_readme "$readme_path" "$namespace" "$module_name"); then
echo "⚠️ $namespace/$module_name: No version found in README, skipping"
continue
fi

if ! validate_version "$readme_version"; then
echo "⚠️ $namespace/$module_name: Invalid version format '$readme_version', skipping"
continue
fi

if check_module_needs_tagging "$namespace" "$module_name" "$readme_version"; then
echo "πŸ“¦ $namespace/$module_name: v$readme_version (needs tag)"
MODULES_TO_TAG+=("$module_path:$namespace:$module_name:$readme_version")
needs_tagging=$((needs_tagging + 1))
else
echo "βœ… $namespace/$module_name: v$readme_version (already tagged)"
fi

done <<< "$all_modules"

echo ""
echo "πŸ“Š Summary: $needs_tagging of $total_checked modules need tagging"
echo ""

[ $needs_tagging -eq 0 ] && {
echo "πŸŽ‰ All modules are up to date! No tags needed."
return 0
}

echo "## Tags to be created:"
for module_info in "${MODULES_TO_TAG[@]}"; do
IFS=':' read -r module_path namespace module_name version <<< "$module_info"
echo "- \`release/$namespace/$module_name/v$version\`"
done
echo ""

return 0
}

create_and_push_tags() {
[ ${#MODULES_TO_TAG[@]} -eq 0 ] && {
echo "❌ No modules to tag found"
return 1
}

local current_commit
current_commit=$(git rev-parse HEAD)

echo "🏷️ Creating release tags for commit: $current_commit"
echo ""

local created_tags=0
local failed_tags=0

for module_info in "${MODULES_TO_TAG[@]}"; do
IFS=':' read -r module_path namespace module_name version <<< "$module_info"

local tag_name="release/$namespace/$module_name/v$version"
local tag_message="Release $namespace/$module_name v$version"

echo "Creating tag: $tag_name"

if git tag -a "$tag_name" -m "$tag_message" "$current_commit"; then
echo "βœ… Created: $tag_name"
created_tags=$((created_tags + 1))
else
echo "❌ Failed to create: $tag_name"
failed_tags=$((failed_tags + 1))
fi
done

echo ""
echo "πŸ“Š Tag creation summary:"
echo " Created: $created_tags"
echo " Failed: $failed_tags"
echo ""

[ $created_tags -eq 0 ] && {
echo "❌ No tags were created successfully"
return 1
}

echo "πŸš€ Pushing tags to origin..."

local tags_to_push=()
for module_info in "${MODULES_TO_TAG[@]}"; do
IFS=':' read -r module_path namespace module_name version <<< "$module_info"
local tag_name="release/$namespace/$module_name/v$version"

if git rev-parse --verify "$tag_name" > /dev/null 2>&1; then
tags_to_push+=("$tag_name")
fi
done

local pushed_tags=0
local failed_pushes=0

if [ ${#tags_to_push[@]} -eq 0 ]; then
echo "❌ No valid tags found to push"
else
if git push --atomic origin "${tags_to_push[@]}"; then
echo "βœ… Successfully pushed all ${#tags_to_push[@]} tags"
pushed_tags=${#tags_to_push[@]}
else
echo "❌ Failed to push tags"
failed_pushes=${#tags_to_push[@]}
fi
fi

echo ""
echo "πŸ“Š Push summary:"
echo " Pushed: $pushed_tags"
echo " Failed: $failed_pushes"
echo ""

if [ $pushed_tags -gt 0 ]; then
echo "πŸŽ‰ Successfully created and pushed $pushed_tags release tags!"
echo ""
echo "πŸ“ Next steps:"
echo " - Tags will be automatically published to registry.coder.com"
echo " - Monitor the registry website for updates"
echo " - Check GitHub releases for any issues"
fi

return 0
}

main() {
[ $# -gt 0 ] && usage

echo "πŸš€ Coder Registry Tag Release Script"
echo "Operating on commit: $(git rev-parse HEAD)"
echo ""

if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "❌ Not in a git repository"
exit 1
fi

detect_modules_needing_tags || exit 1

[ ${#MODULES_TO_TAG[@]} -eq 0 ] && {
echo "✨ No modules need tagging. All done!"
exit 0
}

echo ""
echo "❓ Do you want to proceed with creating and pushing these release tags?"
echo " This will create git tags and push them to the remote repository."
echo ""
read -p "Continue? [y/N]: " -r response

case "$response" in
[yY] | [yY][eE][sS])
echo ""
create_and_push_tags
;;
*)
echo ""
echo "🚫 Operation cancelled by user"
exit 0
;;
esac
}

main "$@"