Release Workflow Improvement Recommendations¶
Current State Analysis¶
The project has three main release-related scripts with overlapping functionality:
gem-publish.sh(700+ lines) - Comprehensive release automationrelease.sh(290+ lines) - Alternative release workflowbuild.sh(180+ lines) - Gem building utility
Issues Identified¶
1. Script Overlap and Confusion¶
Problem: Three scripts with unclear responsibilities
gem-publish.shdoes everything (version bump, changelog, build, publish, GitHub release)release.shduplicates most of this functionalitybuild.shis a subset of both
Impact: Contributors don't know which script to use
2. Excessive Complexity in gem-publish.sh¶
Problem: Single 700-line script trying to do too much
- 10+ command-line flags
- Complex argument parsing
- Difficult to test individual components
- Hard to maintain
Example: Changelog generation alone is ~200 lines embedded in the main script
3. Inconsistent Error Handling¶
Problem: Different validation patterns across scripts
# gem-publish.sh
if [[ ! -f "$file" ]]; then
error "Required file not found: $file"
fi
# release.sh
if [[ ! -f "jekyll-theme-zer0.gemspec" ]]; then
error "Must be run from the repository root directory"
fi
4. Redundant Functionality¶
Problem: Same operations implemented multiple times
- Version validation (3 places)
- Git operations (3 places)
- Gem building (3 places)
- GitHub release creation (2 places)
Recommended Refactoring¶
Strategy: Modular Library + Simple Commands¶
Replace the three scripts with a modular approach:
scripts/
βββ lib/
β βββ version.sh # Version operations library
β βββ changelog.sh # Changelog generation library
β βββ validation.sh # Environment validation library
β βββ git.sh # Git operations library
β βββ gem.sh # Gem build/publish library
βββ release # Main release command (simplified)
βββ build # Build-only command
βββ analyze-commits.sh # Keep as-is (already focused)
New Simplified Architecture¶
1. Library Functions (scripts/lib/)¶
version.sh - Version management
#!/bin/bash
# Single responsibility: version operations
get_current_version() { ... }
calculate_new_version() { ... }
update_version_files() { ... }
validate_version_format() { ... }
changelog.sh - Changelog generation
#!/bin/bash
# Single responsibility: changelog operations
generate_changelog() { ... }
extract_release_notes() { ... }
categorize_commit() { ... }
validation.sh - Environment validation
#!/bin/bash
# Single responsibility: validation checks
validate_git_repo() { ... }
validate_clean_working_dir() { ... }
validate_dependencies() { ... }
validate_rubygems_auth() { ... }
git.sh - Git operations
#!/bin/bash
# Single responsibility: git operations
get_last_version_tag() { ... }
commit_release() { ... }
create_tag() { ... }
push_changes() { ... }
gem.sh - Gem operations
#!/bin/bash
# Single responsibility: gem build/publish
build_gem() { ... }
validate_gemspec() { ... }
publish_to_rubygems() { ... }
create_github_release() { ... }
2. Simple Commands (scripts/)¶
release - Main release command (100 lines max)
#!/bin/bash
# Main release orchestrator
source "$(dirname "$0")/lib/validation.sh"
source "$(dirname "$0")/lib/version.sh"
source "$(dirname "$0")/lib/changelog.sh"
source "$(dirname "$0")/lib/gem.sh"
source "$(dirname "$0")/lib/git.sh"
main() {
# Parse simple arguments
local version_type="${1:-patch}"
local dry_run="${2:-false}"
# Validate
validate_environment
# Execute workflow
local current_version=$(get_current_version)
local new_version=$(calculate_new_version "$current_version" "$version_type")
generate_changelog "$new_version"
update_version_files "$new_version"
build_gem "$new_version"
commit_and_tag "$new_version"
publish_gem "$new_version"
create_github_release "$new_version"
push_changes
}
main "$@"
build - Build-only command (50 lines max)
#!/bin/bash
# Simple gem builder
source "$(dirname "$0")/lib/validation.sh"
source "$(dirname "$0")/lib/gem.sh"
main() {
validate_gemspec
build_gem "$(get_current_version)"
}
main "$@"
Migration Plan¶
Phase 1: Extract Libraries (Week 1)¶
- Create
scripts/lib/directory - Extract version operations β
lib/version.sh - Extract validation β
lib/validation.sh - Add tests for each library
Phase 2: Simplify Commands (Week 2)¶
- Create new
scripts/releaseusing libraries - Test extensively with
--dry-run - Keep old scripts as
*.legacy.sh
Phase 3: Update Documentation (Week 3)¶
- Update CONTRIBUTING.md
- Update VS Code tasks
- Update GitHub Actions workflows
- Deprecate old scripts
Phase 4: Remove Legacy (Week 4)¶
- Remove
gem-publish.sh - Remove
release.sh - Remove
build.sh - Clean up documentation
Benefits of Refactoring¶
1. Clarity¶
- Each file has ONE responsibility
- Easy to understand what each component does
- Clear entry points for common tasks
2. Testability¶
- Small, focused functions easy to test
- Can unit test libraries independently
- Easier to mock dependencies
3. Maintainability¶
- Changes isolated to specific files
- Less duplication means fewer places to update
- Easier for new contributors
4. Reusability¶
- Libraries can be used by other scripts
- GitHub Actions can source specific libraries
- No need to copy-paste code
5. Simplicity¶
- Main commands under 100 lines
- Fewer command-line flags
- Sensible defaults
Example: Simplified Release Command¶
Before (gem-publish.sh):
./scripts/gem-publish.sh patch --dry-run --skip-tests --skip-changelog \
--skip-publish --no-github-release --non-interactive \
--automated-release --auto-commit-range=HEAD~5..HEAD
After (release):
# Most common case (patch release)
./scripts/release
# With options
./scripts/release minor --dry-run
# Advanced (still supported through env vars)
SKIP_TESTS=true ./scripts/release patch
Testing Strategy¶
Unit Tests for Libraries¶
scripts/test/lib/
βββ test_version.sh
βββ test_changelog.sh
βββ test_validation.sh
βββ test_git.sh
βββ test_gem.sh
Integration Tests¶
Backward Compatibility¶
Transition Period (2 releases)¶
- Keep old scripts with deprecation warnings
- Redirect to new commands
- Log usage to track adoption
Example deprecation wrapper:
#!/bin/bash
# gem-publish.sh (deprecated)
echo "β οΈ WARNING: gem-publish.sh is deprecated"
echo " Use: ./scripts/release $*"
echo ""
echo " This wrapper will be removed in v0.3.0"
echo ""
exec "$(dirname "$0")/release" "$@"
Implementation Checklist¶
- Create
scripts/lib/directory structure - Extract and test
version.sh - Extract and test
changelog.sh - Extract and test
validation.sh - Extract and test
git.sh - Extract and test
gem.sh - Create new
releasecommand - Create new
buildcommand - Add deprecation wrappers for old scripts
- Update all documentation
- Update VS Code tasks
- Update GitHub Actions
- Test on real release
- Remove old scripts after 2 releases
Success Metrics¶
After refactoring, we should see:
- 70% reduction in total lines of code
- 100% test coverage for library functions
- 50% reduction in maintenance time
- Zero confusion about which script to use
- Faster onboarding for new contributors
Questions to Consider¶
- Should we keep the
Rakefileapproach instead? - Pro: Standard Ruby tooling
-
Con: Less flexible for shell-based automation
-
Should we migrate to a Ruby-based CLI?
- Pro: Better for gem ecosystem
-
Con: Adds dependency, reduces portability
-
Should we use existing tools like
semantic-release? - Pro: Battle-tested, feature-rich
- Con: Node.js dependency, less customizable
Recommendation¶
Proceed with shell script refactoring because:
- Maintains zero external dependencies
- Keeps Docker-first approach
- Easier for contributors to understand
- Bash is universal across dev environments
- Custom workflow fits project needs
Next Steps: Review this proposal, get feedback, then start Phase 1 (library extraction) in a feature branch.