Skip to main content

Release Workflow Improvement Recommendations

Current State Analysis

The project has three main release-related scripts with overlapping functionality:

Issues Identified

1. Script Overlap and Confusion

Problem: Three scripts with unclear responsibilities

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

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

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)

  1. Create scripts/lib/ directory
  2. Extract version operations → lib/version.sh
  3. Extract validation → lib/validation.sh
  4. Add tests for each library

Phase 2: Simplify Commands (Week 2)

  1. Create new scripts/release using libraries
  2. Test extensively with --dry-run
  3. Keep old scripts as *.legacy.sh

Phase 3: Update Documentation (Week 3)

  1. Update CONTRIBUTING.md
  2. Update VS Code tasks
  3. Update GitHub Actions workflows
  4. Deprecate old scripts

Phase 4: Remove Legacy (Week 4)

  1. Remove gem-publish.sh
  2. Remove release.sh
  3. Remove build.sh
  4. Clean up documentation

Benefits of Refactoring

1. Clarity

2. Testability

3. Maintainability

4. Reusability

5. Simplicity

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

test/
├── test_release_workflow.sh
├── test_build_workflow.sh
└── test_dry_run.sh

Backward Compatibility

Transition Period (2 releases)

  1. Keep old scripts with deprecation warnings
  2. Redirect to new commands
  3. 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

Success Metrics

After refactoring, we should see:

Questions to Consider

  1. Should we keep the Rakefile approach instead?
    • Pro: Standard Ruby tooling
    • Con: Less flexible for shell-based automation
  2. Should we migrate to a Ruby-based CLI?
    • Pro: Better for gem ecosystem
    • Con: Adds dependency, reduces portability
  3. 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:

  1. Maintains zero external dependencies
  2. Keeps Docker-first approach
  3. Easier for contributors to understand
  4. Bash is universal across dev environments
  5. Custom workflow fits project needs

Next Steps: Review this proposal, get feedback, then start Phase 1 (library extraction) in a feature branch.