- 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>
401 lines
11 KiB
Bash
Executable File
401 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
# Tree parser utilities for YAML cookbook definitions
|
|
#
|
|
# Usage:
|
|
# source "$(dirname "${BASH_SOURCE[0]}")/tree-parser.sh"
|
|
#
|
|
# Provides:
|
|
# - tree_parse() - Parse a tree YAML file
|
|
# - tree_get_step() - Get a specific step definition
|
|
# - tree_get_deps() - Get dependencies for a step
|
|
# - tree_execution_order() - Get topological sort of steps
|
|
# - tree_step_ready() - Check if step's dependencies are satisfied
|
|
# - tree_expand_template() - Expand Go template variables
|
|
# - tree_list() - List all available trees
|
|
# - tree_get_teardown() - Get teardown steps
|
|
|
|
# Trees directory
|
|
TREES_DIR="${TREES_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../trees}"
|
|
|
|
# Check for yq
|
|
_tree_check_yq() {
|
|
if ! command -v yq &> /dev/null; then
|
|
echo "Error: yq is required but not installed" >&2
|
|
echo "Install with: brew install yq" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Get tree file path
|
|
_tree_path() {
|
|
local tree_name="$1"
|
|
echo "$TREES_DIR/${tree_name}.yaml"
|
|
}
|
|
|
|
# Parse a tree YAML file
|
|
# Arguments: tree_name
|
|
# Returns: tree JSON on stdout
|
|
# Example: tree_json=$(tree_parse "landing-page")
|
|
tree_parse() {
|
|
local tree_name="$1"
|
|
local path
|
|
path="$(_tree_path "$tree_name")"
|
|
|
|
_tree_check_yq || return 1
|
|
|
|
if [[ ! -f "$path" ]]; then
|
|
echo "Error: Tree '$tree_name' not found at $path" >&2
|
|
return 1
|
|
fi
|
|
|
|
yq -o=json "$path"
|
|
}
|
|
|
|
# Get tree metadata
|
|
# Arguments: tree_name
|
|
# Returns: JSON with name, description, version
|
|
tree_get_meta() {
|
|
local tree_name="$1"
|
|
|
|
local tree
|
|
tree=$(tree_parse "$tree_name") || return 1
|
|
|
|
echo "$tree" | jq '{name: .name, description: .description, version: .version}'
|
|
}
|
|
|
|
# Get default vars from tree
|
|
# Arguments: tree_name
|
|
# Returns: JSON object of default vars
|
|
tree_get_default_vars() {
|
|
local tree_name="$1"
|
|
|
|
local tree
|
|
tree=$(tree_parse "$tree_name") || return 1
|
|
|
|
echo "$tree" | jq '.vars // {}'
|
|
}
|
|
|
|
# Get a specific step definition
|
|
# Arguments: tree_name step_name
|
|
# Returns: step JSON on stdout
|
|
# Example: step=$(tree_get_step "landing-page" "create-project")
|
|
tree_get_step() {
|
|
local tree_name="$1"
|
|
local step_name="$2"
|
|
|
|
local tree
|
|
tree=$(tree_parse "$tree_name") || return 1
|
|
|
|
local step
|
|
step=$(echo "$tree" | jq --arg step "$step_name" '.steps[$step] // null')
|
|
|
|
if [[ "$step" == "null" ]]; then
|
|
echo "Error: Step '$step_name' not found in tree '$tree_name'" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Add step name to the JSON for convenience
|
|
echo "$step" | jq --arg name "$step_name" '. + {name: $name}'
|
|
}
|
|
|
|
# Get all step names
|
|
# Arguments: tree_name
|
|
# Returns: newline-separated list of step names
|
|
tree_get_steps() {
|
|
local tree_name="$1"
|
|
|
|
local tree
|
|
tree=$(tree_parse "$tree_name") || return 1
|
|
|
|
echo "$tree" | jq -r '.steps | keys[]'
|
|
}
|
|
|
|
# Get dependencies for a step
|
|
# Arguments: tree_name step_name
|
|
# Returns: newline-separated list of dependency step names
|
|
# Example: deps=$(tree_get_deps "landing-page" "add-component")
|
|
tree_get_deps() {
|
|
local tree_name="$1"
|
|
local step_name="$2"
|
|
|
|
local step
|
|
step=$(tree_get_step "$tree_name" "$step_name") || return 1
|
|
|
|
echo "$step" | jq -r '.depends_on // [] | .[]'
|
|
}
|
|
|
|
# Get topological sort of steps (execution order)
|
|
# Arguments: tree_name
|
|
# Returns: newline-separated list of step names in execution order
|
|
# Example: order=$(tree_execution_order "landing-page")
|
|
tree_execution_order() {
|
|
local tree_name="$1"
|
|
|
|
local tree
|
|
tree=$(tree_parse "$tree_name") || return 1
|
|
|
|
# Kahn's algorithm for topological sort
|
|
# Build adjacency list and in-degree count
|
|
local steps_json
|
|
steps_json=$(echo "$tree" | jq '.steps')
|
|
|
|
# Use jq to compute the topological order
|
|
echo "$steps_json" | jq -r '
|
|
# Build in-degree map and adjacency list
|
|
. as $steps |
|
|
(keys | map({key: ., value: 0}) | from_entries) as $initial_degrees |
|
|
reduce keys[] as $step (
|
|
{degrees: $initial_degrees, adj: {}};
|
|
. as $state |
|
|
($steps[$step].depends_on // []) as $deps |
|
|
reduce $deps[] as $dep (
|
|
$state;
|
|
.degrees[$step] += 1 |
|
|
.adj[$dep] = ((.adj[$dep] // []) + [$step])
|
|
)
|
|
) |
|
|
|
|
# Kahns algorithm
|
|
. as $graph |
|
|
{
|
|
result: [],
|
|
queue: [$graph.degrees | to_entries | .[] | select(.value == 0) | .key],
|
|
degrees: $graph.degrees,
|
|
adj: $graph.adj
|
|
} |
|
|
until(.queue | length == 0;
|
|
.queue[0] as $node |
|
|
.result += [$node] |
|
|
.queue = .queue[1:] |
|
|
reduce ((.adj[$node] // [])[] ) as $neighbor (
|
|
.;
|
|
.degrees[$neighbor] -= 1 |
|
|
if .degrees[$neighbor] == 0 then
|
|
.queue += [$neighbor]
|
|
else
|
|
.
|
|
end
|
|
)
|
|
) |
|
|
.result[]
|
|
'
|
|
}
|
|
|
|
# Check if a step's dependencies are satisfied
|
|
# Arguments: tree_name step_name completed_steps_json
|
|
# Returns: 0 if ready, 1 if not
|
|
# Example: if tree_step_ready "landing-page" "add-component" '["create-project"]'; then ...
|
|
tree_step_ready() {
|
|
local tree_name="$1"
|
|
local step_name="$2"
|
|
local completed_steps="$3" # JSON array of completed step names
|
|
|
|
local deps
|
|
deps=$(tree_get_deps "$tree_name" "$step_name") || return 1
|
|
|
|
if [[ -z "$deps" ]]; then
|
|
return 0 # No dependencies, always ready
|
|
fi
|
|
|
|
# Check each dependency
|
|
while IFS= read -r dep; do
|
|
if ! echo "$completed_steps" | jq -e --arg d "$dep" 'index($d) != null' > /dev/null; then
|
|
return 1 # Dependency not satisfied
|
|
fi
|
|
done <<< "$deps"
|
|
|
|
return 0
|
|
}
|
|
|
|
# Expand Go template variables in a string
|
|
# Arguments: template_string vars_json outputs_json
|
|
# Returns: expanded string
|
|
# Example: result=$(tree_expand_template "{{ .vars.project_name }}" '{"project_name":"test"}' '{}')
|
|
tree_expand_template() {
|
|
local template="$1"
|
|
local vars_json="${2:-{}}"
|
|
local outputs_json="${3:-{}}"
|
|
|
|
# Build context for template expansion
|
|
local context
|
|
context=$(jq -n \
|
|
--argjson vars "$vars_json" \
|
|
--argjson outputs "$outputs_json" \
|
|
'{vars: $vars, outputs: $outputs}')
|
|
|
|
# Simple template expansion using sed
|
|
# Handle {{ .vars.NAME }} patterns
|
|
local result="$template"
|
|
|
|
# Extract and replace all {{ .vars.xxx }} patterns
|
|
while [[ "$result" =~ \{\{[[:space:]]*\.vars\.([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*\}\} ]]; do
|
|
local var_name="${BASH_REMATCH[1]}"
|
|
local var_value
|
|
var_value=$(echo "$vars_json" | jq -r ".[\"$var_name\"] // \"\"")
|
|
result="${result//\{\{ .vars.$var_name \}\}/$var_value}"
|
|
result="${result//\{\{.vars.$var_name\}\}/$var_value}"
|
|
done
|
|
|
|
# Extract and replace all {{ .outputs.step.key }} patterns
|
|
while [[ "$result" =~ \{\{[[:space:]]*\.outputs\.([a-zA-Z_][a-zA-Z0-9_-]*)\.([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*\}\} ]]; do
|
|
local step_name="${BASH_REMATCH[1]}"
|
|
local key_name="${BASH_REMATCH[2]}"
|
|
local out_value
|
|
out_value=$(echo "$outputs_json" | jq -r ".[\"$step_name\"][\"$key_name\"] // \"\"")
|
|
result="${result//\{\{ .outputs.$step_name.$key_name \}\}/$out_value}"
|
|
result="${result//\{\{.outputs.$step_name.$key_name\}\}/$out_value}"
|
|
done
|
|
|
|
echo "$result"
|
|
}
|
|
|
|
# Expand templates in a step definition
|
|
# Arguments: step_json vars_json outputs_json
|
|
# Returns: step JSON with templates expanded
|
|
tree_expand_step() {
|
|
local step_json="$1"
|
|
local vars_json="${2:-{}}"
|
|
local outputs_json="${3:-{}}"
|
|
|
|
# Convert step to string and expand templates
|
|
local step_str
|
|
step_str=$(echo "$step_json" | jq -c '.')
|
|
|
|
local expanded
|
|
expanded=$(tree_expand_template "$step_str" "$vars_json" "$outputs_json")
|
|
|
|
echo "$expanded"
|
|
}
|
|
|
|
# List all available trees
|
|
# Returns: newline-separated list of tree names
|
|
# Example: trees=$(tree_list)
|
|
tree_list() {
|
|
_tree_check_yq || return 1
|
|
|
|
for f in "$TREES_DIR"/*.yaml; do
|
|
[[ -e "$f" ]] || continue
|
|
basename "$f" .yaml
|
|
done
|
|
}
|
|
|
|
# List trees with descriptions
|
|
# Returns: formatted list of trees with descriptions
|
|
tree_list_detail() {
|
|
_tree_check_yq || return 1
|
|
|
|
for f in "$TREES_DIR"/*.yaml; do
|
|
[[ -e "$f" ]] || continue
|
|
local name
|
|
name=$(basename "$f" .yaml)
|
|
local desc
|
|
desc=$(yq -r '.description // "No description"' "$f" 2>/dev/null)
|
|
printf " %-20s %s\n" "$name" "$desc"
|
|
done
|
|
}
|
|
|
|
# Get teardown steps
|
|
# Arguments: tree_name
|
|
# Returns: JSON array of teardown steps
|
|
# Example: teardown=$(tree_get_teardown "landing-page")
|
|
tree_get_teardown() {
|
|
local tree_name="$1"
|
|
|
|
local tree
|
|
tree=$(tree_parse "$tree_name") || return 1
|
|
|
|
echo "$tree" | jq '.teardown // []'
|
|
}
|
|
|
|
# Get step action type
|
|
# Arguments: step_json
|
|
# Returns: action type string
|
|
tree_step_action() {
|
|
local step_json="$1"
|
|
echo "$step_json" | jq -r '.action // "unknown"'
|
|
}
|
|
|
|
# Get step on_error behavior
|
|
# Arguments: step_json
|
|
# Returns: "fail" or "continue"
|
|
tree_step_on_error() {
|
|
local step_json="$1"
|
|
echo "$step_json" | jq -r '.on_error // "fail"'
|
|
}
|
|
|
|
# Get output extraction rules from step
|
|
# Arguments: step_json
|
|
# Returns: JSON array of output rules [{name, jq_path}]
|
|
tree_step_outputs() {
|
|
local step_json="$1"
|
|
|
|
# outputs can be:
|
|
# - array of {key: jq_path}
|
|
# Example: [{project_id: ".data.name"}]
|
|
echo "$step_json" | jq '
|
|
.outputs // [] |
|
|
map(to_entries | .[0] | {name: .key, jq_path: .value})
|
|
'
|
|
}
|
|
|
|
# Validate tree YAML
|
|
# Arguments: tree_name
|
|
# Returns: 0 if valid, 1 with error messages if not
|
|
tree_validate() {
|
|
local tree_name="$1"
|
|
|
|
local tree
|
|
tree=$(tree_parse "$tree_name") || return 1
|
|
|
|
local errors=()
|
|
|
|
# Check required fields
|
|
local name
|
|
name=$(echo "$tree" | jq -r '.name // ""')
|
|
if [[ -z "$name" ]]; then
|
|
errors+=("Missing required field: name")
|
|
fi
|
|
|
|
# Check that all steps have action field
|
|
local steps_without_action
|
|
steps_without_action=$(echo "$tree" | jq -r '.steps | to_entries | .[] | select(.value.action == null) | .key')
|
|
if [[ -n "$steps_without_action" ]]; then
|
|
while IFS= read -r step; do
|
|
errors+=("Step '$step' missing required field: action")
|
|
done <<< "$steps_without_action"
|
|
fi
|
|
|
|
# Check that dependencies reference existing steps
|
|
local all_steps
|
|
all_steps=$(echo "$tree" | jq -r '.steps | keys')
|
|
local invalid_deps
|
|
invalid_deps=$(echo "$tree" | jq -r --argjson all_steps "$all_steps" '
|
|
.steps | to_entries | .[] |
|
|
.key as $step |
|
|
(.value.depends_on // [])[] |
|
|
select(. as $dep | $all_steps | index($dep) == null) |
|
|
"\($step) depends on non-existent step: \(.)"
|
|
')
|
|
if [[ -n "$invalid_deps" ]]; then
|
|
while IFS= read -r err; do
|
|
errors+=("$err")
|
|
done <<< "$invalid_deps"
|
|
fi
|
|
|
|
# Check for cycles (execution_order will fail if there are cycles)
|
|
if ! tree_execution_order "$tree_name" > /dev/null 2>&1; then
|
|
errors+=("Dependency cycle detected")
|
|
fi
|
|
|
|
# Report errors
|
|
if [[ ${#errors[@]} -gt 0 ]]; then
|
|
echo "Validation errors in tree '$tree_name':" >&2
|
|
for err in "${errors[@]}"; do
|
|
echo " - $err" >&2
|
|
done
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|