workflow: auto-update recipe-reference on release (#5988)

This commit is contained in:
dianed-square
2025-12-15 12:11:00 -08:00
committed by GitHub
parent eec5df76c6
commit e2c3c98cb4
15 changed files with 2439 additions and 0 deletions
@@ -0,0 +1,212 @@
#!/bin/bash
# Compare two validation structure files and output changes
# Usage: ./diff-validation-structures.sh old-validation-structure.json new-validation-structure.json
# Output: validation-changes.json
set -e
OLD_FILE=${1:-"old-validation-structure.json"}
NEW_FILE=${2:-"new-validation-structure.json"}
if [ ! -f "$OLD_FILE" ]; then
echo "Error: Old validation structure file not found: $OLD_FILE" >&2
exit 1
fi
if [ ! -f "$NEW_FILE" ]; then
echo "Error: New validation structure file not found: $NEW_FILE" >&2
exit 1
fi
# Extract versions for metadata
OLD_VERSION=$(jq -r '.version' "$OLD_FILE")
NEW_VERSION=$(jq -r '.version' "$NEW_FILE")
# Build the changes JSON using jq
jq -n \
--arg old_version "$OLD_VERSION" \
--arg new_version "$NEW_VERSION" \
--arg compared_at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--argjson old_data "$(cat "$OLD_FILE")" \
--argjson new_data "$(cat "$NEW_FILE")" \
'
{
old_version: $old_version,
new_version: $new_version,
compared_at: $compared_at,
has_changes: false,
changes: {
struct_fields: {
added: [],
removed: [],
type_changed: [],
comment_changed: []
},
validation_functions: {
added: [],
removed: [],
signature_changed: [],
error_messages_changed: []
}
}
} |
# Detect field changes
. as $result |
# Find added fields (in new but not in old) - compare by struct.field
($new_data.struct_fields | map(.struct + "." + .field)) as $new_fields |
($old_data.struct_fields | map(.struct + "." + .field)) as $old_fields |
($new_fields - $old_fields) as $added_field_keys |
# Find removed fields (in old but not in new) - compare by struct.field
($old_fields - $new_fields) as $removed_field_keys |
# Find fields with type changes - compare by struct.field
(
$new_data.struct_fields |
map(select((.struct + "." + .field) as $key | $old_fields | contains([$key])) |
{struct: .struct, field: .field, new_type: .type, new_comment: .inline_comment}
)
) as $new_common |
(
$old_data.struct_fields |
map(select((.struct + "." + .field) as $key | $new_fields | contains([$key])) |
{struct: .struct, field: .field, old_type: .type, old_comment: .inline_comment}
)
) as $old_common |
# Compare types and comments for common fields
(
$new_common | map(
. as $new_item |
($old_common | map(select(.struct == $new_item.struct and .field == $new_item.field)) | .[0]) as $old_item |
if $old_item.old_type != $new_item.new_type then
{
struct: $new_item.struct,
field: $new_item.field,
old_type: $old_item.old_type,
new_type: $new_item.new_type
}
else
empty
end
)
) as $type_changed |
(
$new_common | map(
. as $new_item |
($old_common | map(select(.struct == $new_item.struct and .field == $new_item.field)) | .[0]) as $old_item |
if $old_item.old_comment != $new_item.new_comment then
{
struct: $new_item.struct,
field: $new_item.field,
old_comment: $old_item.old_comment,
new_comment: $new_item.new_comment
}
else
empty
end
)
) as $comment_changed |
# Find validation function changes
($new_data.validation_functions | map(.function)) as $new_funcs |
($old_data.validation_functions | map(.function)) as $old_funcs |
($new_funcs - $old_funcs) as $added_funcs |
($old_funcs - $new_funcs) as $removed_funcs |
# Find functions with signature changes
(
$new_data.validation_functions |
map(select(.function as $f | $old_funcs | contains([$f])) |
{function: .function, new_signature: .signature, new_errors: .error_messages}
)
) as $new_common_funcs |
(
$old_data.validation_functions |
map(select(.function as $f | $new_funcs | contains([$f])) |
{function: .function, old_signature: .signature, old_errors: .error_messages}
)
) as $old_common_funcs |
(
$new_common_funcs | map(
. as $new_func |
($old_common_funcs | map(select(.function == $new_func.function)) | .[0]) as $old_func |
if $old_func.old_signature != $new_func.new_signature then
{
function: $new_func.function,
old_signature: $old_func.old_signature,
new_signature: $new_func.new_signature
}
else
empty
end
)
) as $signature_changed |
(
$new_common_funcs | map(
. as $new_func |
($old_common_funcs | map(select(.function == $new_func.function)) | .[0]) as $old_func |
if $old_func.old_errors != $new_func.new_errors then
{
function: $new_func.function,
old_errors: $old_func.old_errors,
new_errors: $new_func.new_errors
}
else
empty
end
)
) as $error_messages_changed |
# Build final result with detected changes
.changes.struct_fields.added = (
$added_field_keys | map(
. as $key |
($key | split(".")) as $parts |
$new_data.struct_fields | map(select(.struct == $parts[0] and .field == $parts[1])) | .[0]
)
) |
.changes.struct_fields.removed = (
$removed_field_keys | map(
. as $key |
($key | split(".")) as $parts |
$old_data.struct_fields | map(select(.struct == $parts[0] and .field == $parts[1])) | .[0]
)
) |
.changes.struct_fields.type_changed = $type_changed |
.changes.struct_fields.comment_changed = $comment_changed |
.changes.validation_functions.added = (
$added_funcs | map(
. as $func |
$new_data.validation_functions | map(select(.function == $func)) | .[0]
)
) |
.changes.validation_functions.removed = (
$removed_funcs | map(
. as $func |
$old_data.validation_functions | map(select(.function == $func)) | .[0]
)
) |
.changes.validation_functions.signature_changed = $signature_changed |
.changes.validation_functions.error_messages_changed = $error_messages_changed |
# Set has_changes flag
.has_changes = (
(.changes.struct_fields.added | length) > 0 or
(.changes.struct_fields.removed | length) > 0 or
(.changes.struct_fields.type_changed | length) > 0 or
(.changes.struct_fields.comment_changed | length) > 0 or
(.changes.validation_functions.added | length) > 0 or
(.changes.validation_functions.removed | length) > 0 or
(.changes.validation_functions.signature_changed | length) > 0 or
(.changes.validation_functions.error_messages_changed | length) > 0
)
'
@@ -0,0 +1,161 @@
#!/bin/bash
# Extract and resolve Recipe schema from OpenAPI spec at a specific git version
# Usage: ./extract-schema.sh <version>
# Example: ./extract-schema.sh v1.15.0
set -e
VERSION=${1:-"main"}
GOOSE_REPO=${GOOSE_REPO:-"$HOME/Development/goose"}
if [ ! -d "$GOOSE_REPO" ]; then
echo "Error: GOOSE_REPO directory not found: $GOOSE_REPO" >&2
exit 1
fi
cd "$GOOSE_REPO"
# Verify version exists (for non-main versions)
if [ "$VERSION" != "main" ]; then
if ! git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "Error: Version $VERSION not found in git history" >&2
exit 1
fi
fi
# Extract OpenAPI spec from git
if [ "$VERSION" = "main" ]; then
if [ ! -f ui/desktop/openapi.json ]; then
echo "Error: ui/desktop/openapi.json not found in working directory" >&2
exit 1
fi
OPENAPI_JSON=$(cat ui/desktop/openapi.json)
else
OPENAPI_JSON=$(git show "$VERSION:ui/desktop/openapi.json" 2>/dev/null || {
echo "Error: Could not find ui/desktop/openapi.json at version $VERSION" >&2
exit 1
})
fi
# Use Node.js to extract and resolve Recipe schema
echo "$OPENAPI_JSON" | node -e "
const openApiSpec = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
/**
* Resolves \$ref references in OpenAPI schemas by expanding them with the actual schema definitions
* Ported from ui/desktop/src/recipe/validation.ts
*/
function resolveRefs(schema, openApiSpec) {
if (!schema || typeof schema !== 'object') {
return schema;
}
// Handle \$ref
if (typeof schema.\$ref === 'string') {
const refPath = schema.\$ref.replace('#/', '').split('/');
let resolved = openApiSpec;
for (const segment of refPath) {
if (resolved && typeof resolved === 'object' && segment in resolved) {
resolved = resolved[segment];
} else {
console.warn(\`Could not resolve \$ref: \${schema.\$ref}\`);
return schema; // Return original if can't resolve
}
}
if (resolved && typeof resolved === 'object') {
// Recursively resolve refs in the resolved schema
return resolveRefs(resolved, openApiSpec);
}
return schema;
}
// Handle allOf (merge schemas)
if (Array.isArray(schema.allOf)) {
const merged = {};
for (const subSchema of schema.allOf) {
if (typeof subSchema === 'object' && subSchema !== null) {
const resolved = resolveRefs(subSchema, openApiSpec);
Object.assign(merged, resolved);
}
}
// Keep other properties from the original schema
const { allOf, ...rest } = schema;
return { ...merged, ...rest };
}
// Handle oneOf/anyOf (keep as union)
if (Array.isArray(schema.oneOf)) {
return {
...schema,
oneOf: schema.oneOf.map((subSchema) =>
typeof subSchema === 'object' && subSchema !== null
? resolveRefs(subSchema, openApiSpec)
: subSchema
),
};
}
if (Array.isArray(schema.anyOf)) {
return {
...schema,
anyOf: schema.anyOf.map((subSchema) =>
typeof subSchema === 'object' && subSchema !== null
? resolveRefs(subSchema, openApiSpec)
: subSchema
),
};
}
// Handle object properties
if (schema.type === 'object' && schema.properties && typeof schema.properties === 'object') {
const resolvedProperties = {};
for (const [key, value] of Object.entries(schema.properties)) {
if (typeof value === 'object' && value !== null) {
resolvedProperties[key] = resolveRefs(value, openApiSpec);
} else {
resolvedProperties[key] = value;
}
}
return {
...schema,
properties: resolvedProperties,
};
}
// Handle array items
if (schema.type === 'array' && schema.items && typeof schema.items === 'object') {
return {
...schema,
items: resolveRefs(schema.items, openApiSpec),
};
}
// Return schema as-is if no refs to resolve
return schema;
}
// Extract Recipe schema
const recipeSchema = openApiSpec.components?.schemas?.Recipe;
if (!recipeSchema) {
console.error('Error: Recipe schema not found in OpenAPI specification');
process.exit(1);
}
// Resolve all \$refs in the schema
const resolvedSchema = resolveRefs(recipeSchema, openApiSpec);
// Convert OpenAPI schema to JSON Schema format
const jsonSchema = {
'\$schema': 'http://json-schema.org/draft-07/schema#',
...resolvedSchema,
title: resolvedSchema.title || 'Recipe',
description: resolvedSchema.description || 'A Recipe represents a personalized, user-generated agent configuration that defines specific behaviors and capabilities within the Goose system.',
};
// Output the resolved schema
console.log(JSON.stringify(jsonSchema, null, 2));
"
@@ -0,0 +1,185 @@
#!/bin/bash
# Extract validation structure from Rust source files
# Usage: ./extract-validation-structure.sh <version>
# Example: ./extract-validation-structure.sh v1.15.0
set -e
VERSION=${1:-"main"}
GOOSE_REPO=${GOOSE_REPO:-"$HOME/Development/goose"}
if [ ! -d "$GOOSE_REPO" ]; then
echo "Error: GOOSE_REPO directory not found: $GOOSE_REPO" >&2
exit 1
fi
cd "$GOOSE_REPO"
# Verify version exists (for non-main versions)
if [ "$VERSION" != "main" ]; then
if ! git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "Error: Version $VERSION not found in git history" >&2
exit 1
fi
fi
# Start JSON output
# Use ISO 8601 format that works on both macOS and Linux
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -Iseconds 2>/dev/null || date -u)
cat << EOF
{
"version": "$VERSION",
"extracted_at": "$TIMESTAMP",
"struct_fields": [
EOF
# Extract fields from multiple structs
FIRST_FIELD=true
# Get file content from git history or working directory
if [ "$VERSION" = "main" ]; then
MOD_RS_CONTENT=$(cat crates/goose/src/recipe/mod.rs)
else
MOD_RS_CONTENT=$(git show "$VERSION:crates/goose/src/recipe/mod.rs" 2>/dev/null || {
echo "Error: Failed to read mod.rs from version $VERSION" >&2
exit 1
})
fi
# List of structs to extract (in order of appearance in file)
STRUCTS="Recipe Author Settings Response SubRecipe RecipeParameter"
# Create a temporary file to collect all fields
TEMP_FIELDS=$(mktemp)
for STRUCT_NAME in $STRUCTS; do
# Extract just this struct's definition (from "pub struct Name" to the closing "}")
echo "$MOD_RS_CONTENT" | awk "
/^pub struct $STRUCT_NAME/ { in_struct=1; next }
in_struct && /^}/ { exit }
in_struct && /^[[:space:]]+pub [a-z_]+:/ { print }
" | while IFS= read -r line; do
# Extract field name (word after 'pub ')
field_name=$(echo "$line" | sed -E 's/.*pub ([a-z_]+):.*/\1/')
# Extract type (between : and , or // or end of line)
field_type=$(echo "$line" | sed -E 's/.*:\s*([^,\/]+).*/\1/' | sed 's/[[:space:]]*$//')
# Extract inline comment (after //)
inline_comment=""
if echo "$line" | grep -q "//"; then
inline_comment=$(echo "$line" | sed -E 's/.*\/\/\s*(.*)$/\1/' | sed 's/[[:space:]]*$//' | sed 's/"/\\"/g')
fi
# Check if optional
is_optional="false"
if echo "$field_type" | grep -q "Option<"; then
is_optional="true"
fi
# Output to temp file
cat << FIELD_JSON >> "$TEMP_FIELDS"
{
"struct": "$STRUCT_NAME",
"field": "$field_name",
"type": "$field_type",
"optional": $is_optional,
"inline_comment": "$inline_comment"
}
FIELD_JSON
done
done
# Output fields with proper comma separation
if [ -s "$TEMP_FIELDS" ]; then
# Read all JSON objects into an array and format with commas
jq -s '.' "$TEMP_FIELDS" | jq -r 'to_entries | .[] | (if .key > 0 then "," else "" end) + " " + (.value | tostring)'
fi
rm -f "$TEMP_FIELDS"
# Close struct_fields array, start validation_functions
cat << EOF
],
"validation_functions": [
EOF
# Extract validation functions with error messages and code snippets
FIRST_FUNC=true
# Get validation file content from git history or working directory
# Note: validate_recipe.rs may not exist in older versions
if [ "$VERSION" = "main" ]; then
if [ -f crates/goose/src/recipe/validate_recipe.rs ]; then
VALIDATE_RS_CONTENT=$(cat crates/goose/src/recipe/validate_recipe.rs)
else
VALIDATE_RS_CONTENT=""
fi
else
VALIDATE_RS_CONTENT=$(git show "$VERSION:crates/goose/src/recipe/validate_recipe.rs" 2>/dev/null || echo "")
fi
if [ -n "$VALIDATE_RS_CONTENT" ]; then
echo "$VALIDATE_RS_CONTENT" | rg "^fn validate_" -A 30 | \
awk '
/^fn validate_/ {
if (func_name != "") {
# Output previous function
if (first_func == "true") {
first_func = "false"
} else {
print ","
}
printf " {\n"
printf " \"function\": \"%s\",\n", func_name
printf " \"signature\": \"%s\",\n", signature
printf " \"error_messages\": [%s],\n", error_msgs
printf " \"code_snippet\": %s\n", code_snippet
printf " }"
}
# Start new function
func_name = $0
gsub(/^fn /, "", func_name)
gsub(/\(.*/, "", func_name)
signature = $0
gsub(/"/, "\\\"", signature)
error_msgs = ""
code_snippet = "\"...\""
first_func = (first_func == "") ? "true" : first_func
}
/anyhow::anyhow!\("/ {
# Extract error message
msg = $0
gsub(/.*anyhow::anyhow!\("/, "", msg)
gsub(/".*/, "", msg)
gsub(/"/, "\\\"", msg)
if (error_msgs != "") error_msgs = error_msgs ", "
error_msgs = error_msgs "\"" msg "\""
}
END {
# Output last function
if (func_name != "") {
if (first_func != "true") {
print ","
}
printf " {\n"
printf " \"function\": \"%s\",\n", func_name
printf " \"signature\": \"%s\",\n", signature
printf " \"error_messages\": [%s],\n", error_msgs
printf " \"code_snippet\": %s\n", code_snippet
printf " }"
}
}
'
fi
# Close validation_functions array and JSON
cat << EOF
]
}
EOF
@@ -0,0 +1,126 @@
#!/bin/bash
# End-to-end pipeline test
# Usage: ./run-pipeline.sh <old_version> <new_version>
# Example: ./run-pipeline.sh v1.9.0 v1.15.0
set -e
OLD_VERSION=${1:-"v1.9.0"}
NEW_VERSION=${2:-"v1.15.0"}
echo "=========================================="
echo "Recipe Validation Documentation Pipeline"
echo "=========================================="
echo "Old Version: $OLD_VERSION"
echo "New Version: $NEW_VERSION"
echo ""
# Change to output directory
cd "$(dirname "$0")/../output"
echo "Step 1: Extracting validation structure from $OLD_VERSION..."
if ! ../scripts/extract-validation-structure.sh "$OLD_VERSION" > old-validation-structure.json 2>&1; then
echo "✗ Failed to extract validation structure from $OLD_VERSION" >&2
echo "Error output:" >&2
cat old-validation-structure.json >&2
exit 1
fi
echo "✓ Extracted $(jq '.struct_fields | length' old-validation-structure.json) fields, $(jq '.validation_functions | length' old-validation-structure.json) functions"
echo ""
echo "Step 1b: Extracting schema from $OLD_VERSION..."
if ! ../scripts/extract-schema.sh "$OLD_VERSION" > old-schema.json 2>&1; then
echo "✗ Failed to extract schema from $OLD_VERSION" >&2
echo "Error output:" >&2
cat old-schema.json >&2
exit 1
fi
echo "✓ Extracted schema ($(jq '.properties | length' old-schema.json) properties)"
echo ""
echo "Step 2: Extracting validation structure from $NEW_VERSION..."
if ! ../scripts/extract-validation-structure.sh "$NEW_VERSION" > new-validation-structure.json 2>&1; then
echo "✗ Failed to extract validation structure from $NEW_VERSION" >&2
echo "Error output:" >&2
cat new-validation-structure.json >&2
exit 1
fi
echo "✓ Extracted $(jq '.struct_fields | length' new-validation-structure.json) fields, $(jq '.validation_functions | length' new-validation-structure.json) functions"
echo ""
echo "Step 2b: Extracting schema from $NEW_VERSION..."
if ! ../scripts/extract-schema.sh "$NEW_VERSION" > new-schema.json 2>&1; then
echo "✗ Failed to extract schema from $NEW_VERSION" >&2
echo "Error output:" >&2
cat new-schema.json >&2
exit 1
fi
echo "✓ Extracted schema ($(jq '.properties | length' new-schema.json) properties)"
echo ""
echo "Step 3: Comparing validation structures..."
../scripts/diff-validation-structures.sh old-validation-structure.json new-validation-structure.json > validation-changes.json 2>&1
HAS_CHANGES=$(jq -r '.has_changes' validation-changes.json)
echo "✓ Comparison complete. Has changes: $HAS_CHANGES"
if [ "$HAS_CHANGES" = "true" ]; then
echo ""
echo "Changes detected:"
echo " - Fields added: $(jq '.changes.struct_fields.added | length' validation-changes.json)"
echo " - Fields removed: $(jq '.changes.struct_fields.removed | length' validation-changes.json)"
echo " - Fields type changed: $(jq '.changes.struct_fields.type_changed | length' validation-changes.json)"
echo " - Fields comment changed: $(jq '.changes.struct_fields.comment_changed | length' validation-changes.json)"
echo " - Validation functions added: $(jq '.changes.validation_functions.added | length' validation-changes.json)"
echo " - Validation functions removed: $(jq '.changes.validation_functions.removed | length' validation-changes.json)"
echo " - Validation functions signature changed: $(jq '.changes.validation_functions.signature_changed | length' validation-changes.json)"
echo " - Validation functions error messages changed: $(jq '.changes.validation_functions.error_messages_changed | length' validation-changes.json)"
echo ""
echo "Step 4: Synthesizing validation changes documentation..."
# Run goose and capture output, filtering out session logs
goose run --recipe ../recipes/synthesize-validation-changes.yaml 2>&1 | \
sed -E 's/\x1B\[[0-9;]*[mK]//g' | \
grep -v "^starting session" | \
grep -v "^ session id:" | \
grep -v "^ working directory:" | \
grep -v "^─── text_editor" | \
grep -v "^path:" | \
grep -v "^command:" | \
grep -v "^Closing session" | \
grep -v "^Loading recipe:" | \
grep -v "^Description:" | \
sed '/^$/N;/^\n$/D' > validation-changes.md.tmp
# Check if we got meaningful content (more than just whitespace)
if [ -s validation-changes.md.tmp ] && grep -q "# Recipe Validation Changes" validation-changes.md.tmp; then
mv validation-changes.md.tmp validation-changes.md
echo "✓ Generated validation-changes.md ($(wc -l < validation-changes.md) lines)"
echo ""
echo "=========================================="
echo "Pipeline Complete!"
echo "=========================================="
echo ""
echo "Output files:"
echo " - old-validation-structure.json"
echo " - old-schema.json"
echo " - new-validation-structure.json"
echo " - new-schema.json"
echo " - validation-changes.json"
echo " - validation-changes.md"
echo ""
echo "Review validation-changes.md for documentation updates."
else
echo "✗ Failed to generate validation-changes.md"
exit 1
fi
else
echo ""
echo "=========================================="
echo "No Changes Detected"
echo "=========================================="
echo ""
echo "No validation changes between $OLD_VERSION and $NEW_VERSION."
echo "Documentation update not needed."
fi