rdev/cookbooks/scripts/lib/checkpoint.sh
jordan 56e3f83955 feat: add auth scopes, OpenAPI docs, SDLC guides, and code quality improvements
- Add auth.RequireScope() to all handler routes for proper authorization
- Add SDLC OpenAPI endpoint documentation (state, features, tasks, branches, merge, archive, orchestrator)
- Add SDLC documentation guides (getting-started, cli-reference, api-reference, command-catalog)
- Add artifact_test.go for SDLC artifact coverage
- Add CLAUDE.md rules: auth scopes requirement, error wrapping with %w
- Fix error wrapping to use %w instead of %v throughout codebase
- Improve CLI merge command with conflict detection and resolution
- Fix handler tests to include auth middleware for RequireScope
- Add cookbook tree runner scripts for automated testing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 13:55:50 -07:00

338 lines
9.2 KiB
Bash
Executable File

#!/bin/bash
# Checkpoint utilities for tree-based cookbook execution
#
# Usage:
# source "$(dirname "${BASH_SOURCE[0]}")/checkpoint.sh"
#
# Provides:
# - checkpoint_init() - Initialize a new checkpoint
# - checkpoint_load() - Load existing checkpoint
# - checkpoint_save() - Persist checkpoint to disk
# - checkpoint_step_start() - Mark step as started
# - checkpoint_step_complete() - Mark step as completed with output
# - checkpoint_step_fail() - Mark step as failed with error
# - checkpoint_get_output() - Get output from a completed step
# - checkpoint_status() - Get overall checkpoint status
# - checkpoint_list() - List all checkpoints
# - checkpoint_delete() - Delete a checkpoint
# Checkpoint directory
CHECKPOINT_DIR="${CHECKPOINT_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../.checkpoints}"
# Ensure checkpoint directory exists
_checkpoint_ensure_dir() {
mkdir -p "$CHECKPOINT_DIR"
}
# Get checkpoint file path for a tree
_checkpoint_path() {
local tree_name="$1"
echo "$CHECKPOINT_DIR/${tree_name}.json"
}
# Initialize a new checkpoint
# Arguments: tree_name vars_json
# Returns: run_id
# Example: run_id=$(checkpoint_init "landing-page" '{"project_name": "test"}')
checkpoint_init() {
local tree_name="$1"
local vars_json="${2:-{}}"
_checkpoint_ensure_dir
local run_id="${tree_name}-$(date +%s)"
local now
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local checkpoint
checkpoint=$(jq -n \
--arg tree "$tree_name" \
--arg run_id "$run_id" \
--arg started "$now" \
--argjson vars "$vars_json" \
'{
tree: $tree,
run_id: $run_id,
status: "pending",
vars: $vars,
steps: {},
started_at: $started,
last_completed_step: null
}')
echo "$checkpoint" > "$(_checkpoint_path "$tree_name")"
echo "$run_id"
}
# Load existing checkpoint
# Arguments: tree_name
# Returns: checkpoint JSON on stdout, exit 1 if not found
# Example: checkpoint=$(checkpoint_load "landing-page")
checkpoint_load() {
local tree_name="$1"
local path
path="$(_checkpoint_path "$tree_name")"
if [[ ! -f "$path" ]]; then
return 1
fi
cat "$path"
}
# Save checkpoint to disk
# Arguments: tree_name checkpoint_json
# Example: checkpoint_save "landing-page" "$checkpoint"
checkpoint_save() {
local tree_name="$1"
local checkpoint="$2"
_checkpoint_ensure_dir
echo "$checkpoint" > "$(_checkpoint_path "$tree_name")"
}
# Mark step as started
# Arguments: tree_name step_name
# Example: checkpoint_step_start "landing-page" "create-project"
checkpoint_step_start() {
local tree_name="$1"
local step_name="$2"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || return 1
local now
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
checkpoint=$(echo "$checkpoint" | jq \
--arg step "$step_name" \
--arg started "$now" \
'.steps[$step] = {status: "running", started_at: $started} | .status = "partial"')
checkpoint_save "$tree_name" "$checkpoint"
}
# Mark step as completed with output
# Arguments: tree_name step_name output_json
# Example: checkpoint_step_complete "landing-page" "create-project" '{"project_id": "test"}'
checkpoint_step_complete() {
local tree_name="$1"
local step_name="$2"
local output_json="${3:-{}}"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || return 1
local now
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
checkpoint=$(echo "$checkpoint" | jq \
--arg step "$step_name" \
--arg completed "$now" \
--argjson output "$output_json" \
'.steps[$step].status = "completed" |
.steps[$step].completed_at = $completed |
.steps[$step].output = $output |
.last_completed_step = $step')
checkpoint_save "$tree_name" "$checkpoint"
}
# Mark step as failed with error
# Arguments: tree_name step_name error_message
# Example: checkpoint_step_fail "landing-page" "wait-pipeline" "Pipeline failed"
checkpoint_step_fail() {
local tree_name="$1"
local step_name="$2"
local error_msg="$3"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || return 1
local now
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
checkpoint=$(echo "$checkpoint" | jq \
--arg step "$step_name" \
--arg completed "$now" \
--arg error "$error_msg" \
'.steps[$step].status = "failed" |
.steps[$step].completed_at = $completed |
.steps[$step].error = $error |
.status = "failed"')
checkpoint_save "$tree_name" "$checkpoint"
}
# Get output from a completed step
# Arguments: tree_name step_name [output_key]
# Returns: output value or full output JSON
# Example: project_id=$(checkpoint_get_output "landing-page" "create-project" "project_id")
checkpoint_get_output() {
local tree_name="$1"
local step_name="$2"
local output_key="${3:-}"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || return 1
if [[ -n "$output_key" ]]; then
echo "$checkpoint" | jq -r ".steps[\"$step_name\"].output[\"$output_key\"] // empty"
else
echo "$checkpoint" | jq ".steps[\"$step_name\"].output // {}"
fi
}
# Get variable from checkpoint
# Arguments: tree_name var_name
# Returns: variable value
# Example: project_name=$(checkpoint_get_var "landing-page" "project_name")
checkpoint_get_var() {
local tree_name="$1"
local var_name="$2"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || return 1
echo "$checkpoint" | jq -r ".vars[\"$var_name\"] // empty"
}
# Get overall checkpoint status
# Arguments: tree_name
# Returns: status string (pending, partial, completed, failed)
# Example: status=$(checkpoint_status "landing-page")
checkpoint_status() {
local tree_name="$1"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || {
echo "none"
return 0
}
echo "$checkpoint" | jq -r '.status // "unknown"'
}
# Get detailed checkpoint status (for display)
# Arguments: tree_name
# Returns: formatted status output
checkpoint_status_detail() {
local tree_name="$1"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || {
echo "No checkpoint found for tree: $tree_name"
return 1
}
echo "$checkpoint" | jq -r '
"Tree: \(.tree)",
"Run ID: \(.run_id)",
"Status: \(.status)",
"Started: \(.started_at)",
"",
"Variables:",
(.vars | to_entries | .[] | " \(.key): \(.value)"),
"",
"Steps:",
(.steps | to_entries | sort_by(.value.started_at // "") | .[] |
if .value.status == "completed" then
" \u001b[32m\u2713\u001b[0m \(.key): completed"
elif .value.status == "failed" then
" \u001b[31m\u2717\u001b[0m \(.key): failed - \(.value.error)"
elif .value.status == "running" then
" \u001b[33m\u25d0\u001b[0m \(.key): running..."
else
" \u25cb \(.key): \(.value.status)"
end
),
"",
"Last completed: \(.last_completed_step // "none")"
'
}
# Get list of completed steps
# Arguments: tree_name
# Returns: newline-separated list of completed step names
checkpoint_completed_steps() {
local tree_name="$1"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || return 1
echo "$checkpoint" | jq -r '.steps | to_entries | .[] | select(.value.status == "completed") | .key'
}
# Get list of failed steps
# Arguments: tree_name
# Returns: newline-separated list of failed step names
checkpoint_failed_steps() {
local tree_name="$1"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || return 1
echo "$checkpoint" | jq -r '.steps | to_entries | .[] | select(.value.status == "failed") | .key'
}
# List all checkpoints
# Returns: newline-separated list of tree names with checkpoints
# Example: trees=$(checkpoint_list)
checkpoint_list() {
_checkpoint_ensure_dir
for f in "$CHECKPOINT_DIR"/*.json; do
[[ -e "$f" ]] || continue
basename "$f" .json
done
}
# Delete a checkpoint
# Arguments: tree_name
# Example: checkpoint_delete "landing-page"
checkpoint_delete() {
local tree_name="$1"
local path
path="$(_checkpoint_path "$tree_name")"
if [[ -f "$path" ]]; then
rm "$path"
return 0
fi
return 1
}
# Mark entire tree as completed
# Arguments: tree_name
checkpoint_mark_completed() {
local tree_name="$1"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || return 1
local now
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
checkpoint=$(echo "$checkpoint" | jq \
--arg completed "$now" \
'.status = "completed" | .completed_at = $completed')
checkpoint_save "$tree_name" "$checkpoint"
}
# Update vars in checkpoint (for resume with different vars)
# Arguments: tree_name vars_json
checkpoint_update_vars() {
local tree_name="$1"
local vars_json="$2"
local checkpoint
checkpoint=$(checkpoint_load "$tree_name") || return 1
checkpoint=$(echo "$checkpoint" | jq \
--argjson new_vars "$vars_json" \
'.vars = (.vars + $new_vars)')
checkpoint_save "$tree_name" "$checkpoint"
}