initial commit
This commit is contained in:
570
run-epic.sh
Executable file
570
run-epic.sh
Executable file
@@ -0,0 +1,570 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# run-epic.sh — Automated Epic Development Pipeline
|
||||
# ==============================================================================
|
||||
# Orchestrates BMAD workflows (create-story → dev-story → code-review)
|
||||
# for each story in an epic. Each step runs in a clean Claude context.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/run-epic.sh --epic <epic-path> [options]
|
||||
# ./scripts/run-epic.sh --stories <story1> <story2> ... [options]
|
||||
#
|
||||
# Examples:
|
||||
# # Process entire epic (creates stories first, then develops them)
|
||||
# ./scripts/run-epic.sh --epic docs/03-epics/04-banking/EPIC-001-transfers-payments/epic.md
|
||||
#
|
||||
# # Process specific stories only (skip story creation)
|
||||
# ./scripts/run-epic.sh --stories \
|
||||
# docs/03-epics/04-banking/EPIC-001-transfers-payments/features/FEAT-001-1-p2p-transfers/stories/story-001.md \
|
||||
# docs/03-epics/04-banking/EPIC-001-transfers-payments/features/FEAT-001-2-wire-transfers/stories/story-001.md
|
||||
#
|
||||
# # Dry run (show what would be executed)
|
||||
# ./scripts/run-epic.sh --epic docs/03-epics/04-banking/EPIC-001-transfers-payments/epic.md --dry-run
|
||||
#
|
||||
# # Skip already-completed stories
|
||||
# ./scripts/run-epic.sh --epic docs/03-epics/04-banking/EPIC-001-transfers-payments/epic.md --skip-completed
|
||||
#
|
||||
# # Start from a specific phase (useful for resuming)
|
||||
# ./scripts/run-epic.sh --stories story1.md --phase dev
|
||||
#
|
||||
# Options:
|
||||
# --epic <path> Path to epic.md file (will discover/create stories)
|
||||
# --stories <paths...> Explicit story paths (skip story creation phase)
|
||||
# --phase <phase> Start from phase: create | dev | review (default: create)
|
||||
# --skip-completed Skip stories with status: done/completed in their YAML frontmatter
|
||||
# --cooldown <seconds> Pause between stories (default: 120)
|
||||
# --dry-run Show execution plan without running
|
||||
# --no-review Skip code-review phase
|
||||
# --log-dir <path> Directory for logs (default: .logs/epic-pipeline/)
|
||||
# --help Show this help message
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Portable Helper Functions ─────────────────────────────────────────────────
|
||||
# macOS-compatible replacement for `realpath --relative-to`
|
||||
relpath() {
|
||||
python3 -c "import os; print(os.path.relpath('$1', '${2:-$PWD}'))"
|
||||
}
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$SCRIPT_DIR"
|
||||
LOG_DIR="${PROJECT_ROOT}/.logs/epic-pipeline"
|
||||
COOLDOWN=10
|
||||
DRY_RUN=false
|
||||
SKIP_COMPLETED=false
|
||||
NO_REVIEW=false
|
||||
START_PHASE="create"
|
||||
EPIC_PATH=""
|
||||
STORIES=()
|
||||
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
# ── Colors ─────────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# ── Functions ──────────────────────────────────────────────────────────────────
|
||||
|
||||
usage() {
|
||||
head -n 38 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//'
|
||||
exit 0
|
||||
}
|
||||
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local msg="$*"
|
||||
local ts
|
||||
ts=$(date +"%H:%M:%S")
|
||||
|
||||
case "$level" in
|
||||
INFO) echo -e "${BLUE}[$ts]${NC} ${BOLD}INFO${NC} $msg" ;;
|
||||
OK) echo -e "${GREEN}[$ts]${NC} ${GREEN}${BOLD}OK${NC} $msg" ;;
|
||||
WARN) echo -e "${YELLOW}[$ts]${NC} ${YELLOW}${BOLD}WARN${NC} $msg" ;;
|
||||
ERROR) echo -e "${RED}[$ts]${NC} ${RED}${BOLD}ERROR${NC} $msg" ;;
|
||||
STEP) echo -e "${CYAN}[$ts]${NC} ${CYAN}${BOLD}STEP${NC} $msg" ;;
|
||||
esac
|
||||
|
||||
# Also append to log file
|
||||
echo "[$ts] $level $msg" >> "${LOG_DIR}/run-${TIMESTAMP}.log" 2>/dev/null || true
|
||||
}
|
||||
|
||||
separator() {
|
||||
echo ""
|
||||
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD} $1${NC}"
|
||||
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
check_prerequisites() {
|
||||
if ! command -v claude &> /dev/null; then
|
||||
log ERROR "claude CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "${PROJECT_ROOT}/_bmad" ]; then
|
||||
log ERROR "Not in a valid BMAD project root (_bmad/ not found)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if a story is completed by reading its YAML frontmatter
|
||||
is_story_completed() {
|
||||
local story_path="$1"
|
||||
if [ ! -f "$story_path" ]; then
|
||||
return 1 # Not completed (doesn't exist yet)
|
||||
fi
|
||||
# Check for status: done, completed, or delivered in frontmatter
|
||||
if head -50 "$story_path" | grep -qiE '^status:\s*(done|completed|delivered)'; then
|
||||
return 0 # Completed
|
||||
fi
|
||||
return 1 # Not completed
|
||||
}
|
||||
|
||||
# Extract story titles from epic.md
|
||||
# Parses headers like: ### Story 1.1: Lexer & Tokenizer
|
||||
extract_features_from_epic() {
|
||||
local epic_file="$1"
|
||||
grep -E '^### Story [0-9]+\.[0-9]+:' "$epic_file" \
|
||||
| sed 's/^### Story [0-9]*\.[0-9]*: //' || true
|
||||
}
|
||||
|
||||
# Convert feature name to story path convention
|
||||
feature_to_slug() {
|
||||
echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//'
|
||||
}
|
||||
|
||||
# Discover stories from _bmad-output/implementation-artifacts/
|
||||
STORY_DIR="${PROJECT_ROOT}/_bmad-output/implementation-artifacts"
|
||||
|
||||
discover_stories() {
|
||||
log INFO "Discovering stories in: $STORY_DIR"
|
||||
|
||||
# Find story markdown files matching pattern like 1-1-lexer-tokenizer.md
|
||||
local found_stories=()
|
||||
while IFS= read -r -d '' story; do
|
||||
found_stories+=("$story")
|
||||
done < <(find "$STORY_DIR" -maxdepth 1 -name "[0-9]*-[0-9]*-*.md" -print0 2>/dev/null | sort -z)
|
||||
|
||||
if [ ${#found_stories[@]} -eq 0 ]; then
|
||||
log WARN "No stories found. Stories will be created in the create phase."
|
||||
return 0
|
||||
fi
|
||||
|
||||
log OK "Found ${#found_stories[@]} stories"
|
||||
for s in "${found_stories[@]}"; do
|
||||
local rel_path
|
||||
rel_path=$(relpath "$s" "$PROJECT_ROOT")
|
||||
echo " → $rel_path"
|
||||
STORIES+=("$rel_path")
|
||||
done
|
||||
}
|
||||
|
||||
# Generate simulated stories for dry-run mode (when stories don't exist yet)
|
||||
simulate_stories_from_epic() {
|
||||
local epic_rel
|
||||
epic_rel=$(relpath "$EPIC_PATH" "$PROJECT_ROOT")
|
||||
|
||||
log INFO "Simulating stories from epic features..."
|
||||
|
||||
local feature_num=0
|
||||
while IFS= read -r feature_name; do
|
||||
[ -z "$feature_name" ] && continue
|
||||
((feature_num++)) || true
|
||||
local slug
|
||||
slug=$(feature_to_slug "$feature_name")
|
||||
local simulated_path="_bmad-output/implementation-artifacts/${feature_num}-1-${slug}.md"
|
||||
STORIES+=("$simulated_path")
|
||||
log INFO " [simulated] $simulated_path"
|
||||
done < <(extract_features_from_epic "$EPIC_PATH")
|
||||
|
||||
if [ ${#STORIES[@]} -eq 0 ]; then
|
||||
log WARN "Could not extract features from epic for simulation"
|
||||
else
|
||||
log OK "Simulated ${#STORIES[@]} stories from ${feature_num} features"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run a single Claude command with clean context
|
||||
run_claude() {
|
||||
local description="$1"
|
||||
local prompt="$2"
|
||||
local log_file="$3"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log INFO "[DRY RUN] Would execute: claude -p \"${prompt:0:80}...\""
|
||||
return 0
|
||||
fi
|
||||
|
||||
log STEP "$description"
|
||||
log INFO "Log: $log_file"
|
||||
|
||||
# Each invocation = fresh context (no --continue)
|
||||
# Unset CLAUDECODE to allow launching from within a Claude Code session
|
||||
# Redirect stdin from /dev/null to prevent consuming the caller's stdin (e.g., while-read loops)
|
||||
if env -u CLAUDECODE claude --dangerously-skip-permissions -p "$prompt" < /dev/null > "$log_file" 2>&1; then
|
||||
log OK "$description — completed successfully"
|
||||
return 0
|
||||
else
|
||||
local exit_code=$?
|
||||
log ERROR "$description — failed (exit code: $exit_code)"
|
||||
log ERROR "Check log: $log_file"
|
||||
return $exit_code
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Phase: Create Stories ──────────────────────────────────────────────────────
|
||||
phase_create_stories() {
|
||||
if [ -z "$EPIC_PATH" ]; then
|
||||
log WARN "No epic path provided, skipping story creation"
|
||||
return 0
|
||||
fi
|
||||
|
||||
separator "PHASE: Create Stories from Epic"
|
||||
|
||||
local epic_rel
|
||||
epic_rel=$(relpath "$EPIC_PATH" "$PROJECT_ROOT")
|
||||
|
||||
log INFO "Epic: $epic_rel"
|
||||
|
||||
# Create stories one per feature using the BMAD create-story workflow
|
||||
# Extract story titles and create a story for each
|
||||
local feature_num=0
|
||||
while IFS= read -r feature_name; do
|
||||
[ -z "$feature_name" ] && continue
|
||||
((feature_num++)) || true
|
||||
|
||||
local create_log="${LOG_DIR}/${TIMESTAMP}-create-story-${feature_num}.log"
|
||||
run_claude \
|
||||
"Creating story for feature ${feature_num}: $feature_name" \
|
||||
"/bmad-create-story -- Create the next story from the epic at '${epic_rel}'. Focus on Feature ${feature_num}: ${feature_name}. Read the epic file first, then create the story following BMAD structure." \
|
||||
"$create_log"
|
||||
done < <(extract_features_from_epic "$EPIC_PATH")
|
||||
|
||||
if [ $feature_num -eq 0 ]; then
|
||||
log ERROR "No features found in epic file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log OK "Requested creation of $feature_num stories"
|
||||
|
||||
# Re-discover stories after creation (or simulate for dry-run)
|
||||
STORIES=()
|
||||
discover_stories
|
||||
|
||||
# Also check BMAD default output locations
|
||||
if [ ${#STORIES[@]} -eq 0 ]; then
|
||||
log INFO "Checking alternative story locations..."
|
||||
|
||||
# Check _bmad-output/implementation-artifacts/ (BMAD default)
|
||||
for search_dir in \
|
||||
"$PROJECT_ROOT/_bmad-output/implementation-artifacts" \
|
||||
"$PROJECT_ROOT/.docs/sprint-artifacts/stories" \
|
||||
"$PROJECT_ROOT/docs/stories"; do
|
||||
|
||||
if [ -d "$search_dir" ]; then
|
||||
while IFS= read -r -d '' story; do
|
||||
local rel_path
|
||||
rel_path=$(relpath "$story" "$PROJECT_ROOT")
|
||||
STORIES+=("$rel_path")
|
||||
done < <(find "$search_dir" -name "*.md" -print0 2>/dev/null | sort -z)
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#STORIES[@]} -gt 0 ]; then
|
||||
log OK "Found ${#STORIES[@]} stories in alternative locations"
|
||||
for s in "${STORIES[@]}"; do
|
||||
echo " → $s"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# In dry-run mode, stories won't actually exist — simulate from epic features
|
||||
if [ "$DRY_RUN" = true ] && [ ${#STORIES[@]} -eq 0 ]; then
|
||||
simulate_stories_from_epic
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Phase: Develop Story ───────────────────────────────────────────────────────
|
||||
phase_dev_story() {
|
||||
local story_path="$1"
|
||||
local story_name
|
||||
story_name=$(basename "$story_path" .md)
|
||||
local story_dir
|
||||
story_dir=$(dirname "$story_path")
|
||||
|
||||
separator "DEV: $story_name"
|
||||
|
||||
if [ "$SKIP_COMPLETED" = true ] && is_story_completed "${PROJECT_ROOT}/$story_path"; then
|
||||
log WARN "Skipping (already completed): $story_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Step 1: Plan the implementation
|
||||
local plan_log="${LOG_DIR}/${TIMESTAMP}-plan-${story_name}.log"
|
||||
run_claude \
|
||||
"Planning: $story_name" \
|
||||
"Read the story at '$story_path' and all related documents (epic, feature). Enter plan mode and create a detailed implementation plan. Consider the existing codebase architecture, DDD boundaries, and enterprise standards. Output the plan then exit." \
|
||||
"$plan_log" || return 1
|
||||
|
||||
# Step 2: Develop the story
|
||||
local dev_log="${LOG_DIR}/${TIMESTAMP}-dev-${story_name}.log"
|
||||
run_claude \
|
||||
"Developing: $story_name" \
|
||||
"/bmad-dev-story $story_path" \
|
||||
"$dev_log" || return 1
|
||||
|
||||
log OK "Development complete: $story_name"
|
||||
}
|
||||
|
||||
# ── Phase: Code Review ─────────────────────────────────────────────────────────
|
||||
phase_review_story() {
|
||||
local story_path="$1"
|
||||
local story_name
|
||||
story_name=$(basename "$story_path" .md)
|
||||
|
||||
separator "REVIEW: $story_name"
|
||||
|
||||
local review_log="${LOG_DIR}/${TIMESTAMP}-review-${story_name}.log"
|
||||
run_claude \
|
||||
"Code review: $story_name" \
|
||||
"/bmad-code-review $story_path" \
|
||||
"$review_log" || return 1
|
||||
|
||||
log OK "Code review complete: $story_name"
|
||||
}
|
||||
|
||||
# ── Phase: Git Commit ──────────────────────────────────────────────────────────
|
||||
phase_commit_story() {
|
||||
local story_path="$1"
|
||||
local story_name
|
||||
story_name=$(basename "$story_path" .md)
|
||||
|
||||
separator "COMMIT: $story_name"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log INFO "[DRY RUN] Would commit changes for: $story_name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Check if there are any changes to commit
|
||||
if git diff --quiet && git diff --staged --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
|
||||
log WARN "No changes to commit for: $story_name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Extract story title from file (e.g. "# Story B002.1: Fee Configuration")
|
||||
local story_title
|
||||
story_title=$(grep -m1 '^# Story' "${PROJECT_ROOT}/${story_path}" 2>/dev/null \
|
||||
| sed 's/^# Story [A-Z0-9.]*: //' \
|
||||
|| echo "$story_name")
|
||||
|
||||
# Determine epic number from story path for commit message prefix
|
||||
local epic_ref
|
||||
epic_ref=$(echo "$story_path" | grep -oE 'EPIC-[0-9]+' | head -1 || echo "EPIC")
|
||||
|
||||
# Stage all tracked and untracked changes (excluding gitignored)
|
||||
git add -A
|
||||
|
||||
# Commit with story reference — no co-author per project preference
|
||||
git commit -m "$(cat <<EOF
|
||||
feat(${epic_ref,,}): implement $story_title
|
||||
|
||||
Story: $story_path
|
||||
EOF
|
||||
)"
|
||||
log OK "Committed: $story_name"
|
||||
}
|
||||
|
||||
# ── Parse Arguments ────────────────────────────────────────────────────────────
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--epic)
|
||||
EPIC_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--stories)
|
||||
shift
|
||||
while [[ $# -gt 0 && ! "$1" =~ ^-- ]]; do
|
||||
STORIES+=("$1")
|
||||
shift
|
||||
done
|
||||
;;
|
||||
--phase)
|
||||
START_PHASE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-completed)
|
||||
SKIP_COMPLETED=true
|
||||
shift
|
||||
;;
|
||||
--cooldown)
|
||||
COOLDOWN="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--no-review)
|
||||
NO_REVIEW=true
|
||||
shift
|
||||
;;
|
||||
--log-dir)
|
||||
LOG_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
log ERROR "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate
|
||||
if [ -z "$EPIC_PATH" ] && [ ${#STORIES[@]} -eq 0 ]; then
|
||||
log ERROR "Must provide --epic or --stories"
|
||||
usage
|
||||
fi
|
||||
|
||||
# Resolve epic path to absolute if needed
|
||||
if [ -n "$EPIC_PATH" ] && [[ ! "$EPIC_PATH" = /* ]]; then
|
||||
EPIC_PATH="${PROJECT_ROOT}/${EPIC_PATH}"
|
||||
fi
|
||||
|
||||
if [ -n "$EPIC_PATH" ] && [ ! -f "$EPIC_PATH" ]; then
|
||||
log ERROR "Epic file not found: $EPIC_PATH"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
# Setup
|
||||
mkdir -p "$LOG_DIR"
|
||||
check_prerequisites
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
separator "Epic Development Pipeline"
|
||||
log INFO "Timestamp: $TIMESTAMP"
|
||||
log INFO "Project Root: $PROJECT_ROOT"
|
||||
log INFO "Log Directory: $LOG_DIR"
|
||||
log INFO "Start Phase: $START_PHASE"
|
||||
log INFO "Cooldown: ${COOLDOWN}s"
|
||||
log INFO "Dry Run: $DRY_RUN"
|
||||
log INFO "Skip Completed: $SKIP_COMPLETED"
|
||||
log INFO "No Review: $NO_REVIEW"
|
||||
|
||||
if [ -n "$EPIC_PATH" ]; then
|
||||
local epic_rel
|
||||
epic_rel=$(relpath "$EPIC_PATH" "$PROJECT_ROOT")
|
||||
log INFO "Epic: $epic_rel"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Phase 1: Create stories (if starting from create phase)
|
||||
if [ "$START_PHASE" = "create" ] && [ -n "$EPIC_PATH" ]; then
|
||||
phase_create_stories
|
||||
|
||||
if [ ${#STORIES[@]} -eq 0 ]; then
|
||||
log ERROR "No stories found after creation phase. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$START_PHASE" = "create" ] && [ ${#STORIES[@]} -gt 0 ]; then
|
||||
log INFO "Stories provided explicitly, skipping creation phase"
|
||||
fi
|
||||
|
||||
# Discover stories if epic provided but starting from dev/review
|
||||
if [ ${#STORIES[@]} -eq 0 ] && [ -n "$EPIC_PATH" ]; then
|
||||
discover_stories
|
||||
if [ ${#STORIES[@]} -eq 0 ]; then
|
||||
log ERROR "No stories found to process"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Summary
|
||||
separator "Execution Plan: ${#STORIES[@]} stories"
|
||||
for i in "${!STORIES[@]}"; do
|
||||
local num=$((i + 1))
|
||||
echo -e " ${BOLD}${num}.${NC} ${STORIES[$i]}"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Track results
|
||||
local total=${#STORIES[@]}
|
||||
local succeeded=0
|
||||
local failed=0
|
||||
local skipped=0
|
||||
|
||||
# Phase 2 & 3: Dev + Review each story
|
||||
for i in "${!STORIES[@]}"; do
|
||||
local story="${STORIES[$i]}"
|
||||
local num=$((i + 1))
|
||||
|
||||
separator "Story $num/$total: $(basename "$story" .md)"
|
||||
|
||||
# Dev phase
|
||||
if [[ "$START_PHASE" = "create" || "$START_PHASE" = "dev" ]]; then
|
||||
if phase_dev_story "$story"; then
|
||||
: # continue to review
|
||||
else
|
||||
log ERROR "Dev failed for: $story"
|
||||
((failed++)) || true
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Review phase
|
||||
if [ "$NO_REVIEW" = false ] && [[ "$START_PHASE" != "review" || true ]]; then
|
||||
if phase_review_story "$story"; then
|
||||
((succeeded++)) || true
|
||||
else
|
||||
log WARN "Review found issues for: $story (counted as succeeded, review logged)"
|
||||
((succeeded++)) || true
|
||||
fi
|
||||
else
|
||||
((succeeded++)) || true
|
||||
fi
|
||||
|
||||
# Commit phase — commit everything produced by dev + review
|
||||
phase_commit_story "$story" || log WARN "Commit failed for: $story (continuing)"
|
||||
|
||||
# Cooldown between stories (skip after last)
|
||||
if [ $((i + 1)) -lt $total ] && [ "$DRY_RUN" = false ]; then
|
||||
log INFO "Cooldown: ${COOLDOWN}s before next story..."
|
||||
sleep "$COOLDOWN"
|
||||
fi
|
||||
done
|
||||
|
||||
# Final summary
|
||||
separator "Pipeline Complete"
|
||||
echo -e " ${GREEN}Succeeded:${NC} $succeeded"
|
||||
echo -e " ${RED}Failed:${NC} $failed"
|
||||
echo -e " ${YELLOW}Skipped:${NC} $skipped"
|
||||
echo -e " ${BOLD}Total:${NC} $total"
|
||||
echo ""
|
||||
log INFO "Full logs: $LOG_DIR/"
|
||||
|
||||
if [ $failed -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user