Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/schema-consistency-checker.lock.yml

Large diffs are not rendered by default.

52 changes: 27 additions & 25 deletions .github/workflows/schema-consistency-checker.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ pre-agent-steps:
# 1. All top-level fields in the main JSON schema
SCHEMA_FIELDS=$(jq -r '.properties | keys[]' pkg/parser/schemas/main_workflow_schema.json 2>/dev/null | sort -u || echo "")

# 2. yaml-tagged struct fields in pkg/parser/*.go
PARSER_YAML_FIELDS=$(grep -rh 'yaml:"' pkg/parser/*.go 2>/dev/null \
| grep -o 'yaml:"[^"]*"' \
| sed 's/yaml:"//;s/"//' \
# 2. JSON/YAML-tagged fields in the top-level frontmatter type.
# pkg/parser/frontmatter.go is only a logger declaration; frontmatter fields
# are extracted and represented in pkg/workflow/frontmatter_types.go.
FRONTMATTER_FIELDS=$(sed -n '/^type FrontmatterConfig struct {$/,/^}$/p' pkg/workflow/frontmatter_types.go 2>/dev/null \
| grep -Eo '(json|yaml):"[^"]*"' \
| sed -E 's/^(json|yaml):"//;s/"$//' \
| sed 's/,omitempty//' \
| sed 's/,.*$//' \
| grep -v '^-$' \
Expand Down Expand Up @@ -74,14 +76,14 @@ pre-agent-steps:
"\(.key): \(.value.type // (.value.anyOf // .value.oneOf // [] | map(.type // "complex") | unique | join("|")) // "complex")"' \
pkg/parser/schemas/main_workflow_schema.json 2>/dev/null | sort || echo "")

# 6. Fields in schema but absent as yaml tags in parser structs
IN_SCHEMA_NOT_PARSER=$(comm -23 \
# 6. Fields in schema but absent from the frontmatter type definitions
IN_SCHEMA_NOT_FRONTMATTER=$(comm -23 \
<(echo "$SCHEMA_FIELDS") \
<(echo "$PARSER_YAML_FIELDS" | sort -u) 2>/dev/null || echo "")
<(echo "$FRONTMATTER_FIELDS" | sort -u) 2>/dev/null || echo "")

# 7. yaml tags in parser structs absent from schema
IN_PARSER_NOT_SCHEMA=$(comm -23 \
<(echo "$PARSER_YAML_FIELDS" | sort -u) \
# 7. Fields in frontmatter type definitions absent from schema
IN_FRONTMATTER_NOT_SCHEMA=$(comm -23 \
<(echo "$FRONTMATTER_FIELDS" | sort -u) \
<(echo "$SCHEMA_FIELDS") 2>/dev/null || echo "")

# 8. Fields in schema but absent from workflow compiler structs
Expand All @@ -98,24 +100,24 @@ pre-agent-steps:
jq -n \
--arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg schema_fields "$SCHEMA_FIELDS" \
--arg parser_yaml_fields "$PARSER_YAML_FIELDS" \
--arg frontmatter_fields "$FRONTMATTER_FIELDS" \
--arg workflow_yaml_fields "$WORKFLOW_YAML_FIELDS" \
--arg used_in_workflows "$USED_FIELDS" \
--arg field_types "$FIELD_TYPES" \
--arg in_schema_not_parser "$IN_SCHEMA_NOT_PARSER" \
--arg in_parser_not_schema "$IN_PARSER_NOT_SCHEMA" \
--arg in_schema_not_frontmatter "$IN_SCHEMA_NOT_FRONTMATTER" \
--arg in_frontmatter_not_schema "$IN_FRONTMATTER_NOT_SCHEMA" \
--arg in_schema_not_workflow "$IN_SCHEMA_NOT_WORKFLOW" \
--arg in_used_not_schema "$IN_USED_NOT_SCHEMA" \
'{
generated_at: $generated_at,
schema_fields: ($schema_fields | split("\n") | map(select(. != ""))),
parser_yaml_fields: ($parser_yaml_fields | split("\n") | map(select(. != ""))),
frontmatter_fields: ($frontmatter_fields | split("\n") | map(select(. != ""))),
workflow_yaml_fields: ($workflow_yaml_fields | split("\n") | map(select(. != ""))),
used_in_workflows: ($used_in_workflows | split("\n") | map(select(. != ""))),
field_types: ($field_types | split("\n") | map(select(. != ""))),
field_gaps: {
in_schema_not_parser: ($in_schema_not_parser | split("\n") | map(select(. != ""))),
in_parser_not_schema: ($in_parser_not_schema | split("\n") | map(select(. != ""))),
in_schema_not_frontmatter: ($in_schema_not_frontmatter | split("\n") | map(select(. != ""))),
in_frontmatter_not_schema: ($in_frontmatter_not_schema | split("\n") | map(select(. != ""))),
in_schema_not_workflow: ($in_schema_not_workflow | split("\n") | map(select(. != ""))),
in_used_not_schema: ($in_used_not_schema | split("\n") | map(select(. != "")))
}
Expand All @@ -125,11 +127,11 @@ pre-agent-steps:
echo "Summary:"
jq '{
schema_field_count: (.schema_fields | length),
parser_yaml_field_count: (.parser_yaml_fields | length),
frontmatter_field_count: (.frontmatter_fields | length),
workflow_yaml_field_count: (.workflow_yaml_fields | length),
gaps: {
in_schema_not_parser: (.field_gaps.in_schema_not_parser | length),
in_parser_not_schema: (.field_gaps.in_parser_not_schema | length),
in_schema_not_frontmatter: (.field_gaps.in_schema_not_frontmatter | length),
in_frontmatter_not_schema: (.field_gaps.in_frontmatter_not_schema | length),
in_schema_not_workflow: (.field_gaps.in_schema_not_workflow | length),
in_used_not_schema: (.field_gaps.in_used_not_schema | length)
}
Expand Down Expand Up @@ -245,7 +247,7 @@ sandbox:

You are an expert system that detects inconsistencies between:
- The main JSON schema of the frontmatter (`pkg/parser/schemas/main_workflow_schema.json`)
- The parser and compiler implementation (`pkg/parser/*.go` and `pkg/workflow/*.go`)
- The parser and compiler implementation (`pkg/workflow/frontmatter_types.go` and `pkg/workflow/*.go`)
- The documentation (`docs/src/content/docs/**/*.md`)
- The workflows in the project (`.github/workflows/*.md`)

Expand Down Expand Up @@ -296,7 +298,7 @@ Strategy database structure:
**Key files to analyze:**
- `pkg/parser/schemas/main_workflow_schema.json`
- `pkg/parser/schemas/mcp_config_schema.json`
- `pkg/parser/frontmatter.go` and `pkg/parser/*.go`
- `pkg/workflow/frontmatter_types.go` and `pkg/workflow/frontmatter_extraction_yaml.go`
- `pkg/workflow/compiler.go` - main workflow compiler
- `pkg/workflow/tools.go` - tools configuration processing
- `pkg/workflow/safe_outputs.go` - safe-outputs configuration
Expand Down Expand Up @@ -351,7 +353,7 @@ Strategy database structure:
- Validation rules not documented

**Focus on:**
- `pkg/parser/*.go` - frontmatter parsing
- `pkg/workflow/frontmatter_types.go` - frontmatter field definitions
- `pkg/workflow/*.go` - workflow compilation and feature processing

## Detection Strategies
Expand Down Expand Up @@ -417,12 +419,12 @@ echo "=== STRATEGIES ===" && \

The schema diff contains:
- `schema_fields`: All top-level field names in the main JSON schema
- `parser_yaml_fields`: All yaml-tagged struct fields in `pkg/parser/*.go`
- `frontmatter_fields`: All JSON/YAML-tagged fields in `pkg/workflow/frontmatter_types.go`
- `workflow_yaml_fields`: All yaml-tagged struct fields in `pkg/workflow/*.go`
- `used_in_workflows`: All top-level frontmatter keys used in `.github/workflows/*.md`
- `field_types`: Schema field types for all top-level fields
- `field_gaps.in_schema_not_parser`: Fields in schema absent from parser yaml tags
- `field_gaps.in_parser_not_schema`: Fields as parser yaml tags absent from schema
- `field_gaps.in_schema_not_frontmatter`: Fields in schema absent from frontmatter type definitions
- `field_gaps.in_frontmatter_not_schema`: Fields in frontmatter type definitions absent from schema
- `field_gaps.in_schema_not_workflow`: Fields in schema absent from workflow compiler yaml tags
- `field_gaps.in_used_not_schema`: Fields used in workflow files but not in schema

Expand Down
36 changes: 35 additions & 1 deletion docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -20203,7 +20203,41 @@ mcp-scripts:
# workflows are also merged.
# (optional)
runtimes:
{}
# Runtime configuration object identified by runtime ID (e.g., 'node', 'python',
# 'go')
node:
# Runtime version as a string (e.g., '22', '3.12', 'latest') or number (e.g.,
# 22, 3.12). Numeric values are automatically converted to strings at runtime.
# (optional)
version: "example-value"

# GitHub Actions repository for setting up the runtime (e.g., 'actions/setup-node',
# 'custom/setup-runtime'). Overrides the default setup action.
# (optional)
action-repo: "example-value"

# Version of the setup action to use (e.g., 'v4', 'v5'). Overrides the default
# action version.
# (optional)
action-version: "example-value"

# Optional GitHub Actions if condition to control when the runtime setup step runs.
# Supports standard GitHub Actions expression syntax. Useful for conditionally
# installing runtimes based on file presence (e.g., "hashFiles('go.mod') != ''" to
# install Go only when go.mod exists).
# (optional)
if: "example-value"

# Enable a default 3-day dependency cooldown for installs associated with this
# runtime. Set to false to disable.
# (optional)
cooldown: true

# Allow npm pre/post install scripts to execute during package installation. A
# supply chain security warning is emitted at compile time; in strict mode this is
# an error. See: https://github.github.com/gh-aw/reference/frontmatter/#run-install-scripts
# (optional)
run-install-scripts: true

# Checkout configuration for the agent job. Controls how actions/checkout is
# invoked. Can be a single checkout configuration, an array for multiple
Expand Down
1 change: 1 addition & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12115,6 +12115,7 @@
"runtimes": {
"type": "object",
"description": "Runtime environment version overrides. Allows customizing runtime versions (e.g., Node.js, Python) or defining new runtimes. Runtimes from imported shared workflows are also merged.",
"x-example-key": "node",
"patternProperties": {
"^[a-z][a-z0-9-]*$": {
"type": "object",
Expand Down
24 changes: 20 additions & 4 deletions scripts/generate-schema-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ function resolvePropertyRef(prop) {
return prop;
}

/**
* Get the value schema for an object with dynamic keys.
*/
function getDynamicProperty(prop) {
if (prop.additionalProperties && typeof prop.additionalProperties === "object") {
return resolvePropertyRef(prop.additionalProperties);
}

const patternProperties = Object.values(prop.patternProperties || {});
if (patternProperties.length === 1 && typeof patternProperties[0] === "object") {
return resolvePropertyRef(patternProperties[0]);
}

return null;
}

/**
* Format a description as YAML comment
*/
Expand Down Expand Up @@ -224,9 +240,9 @@ function generateVariants(prop, propName, indent = 0, required = []) {
if (variant.properties) {
const subLines = generateProperties(variant.properties, variant.required || [], indent + 2);
lines.push(subLines);
} else if (variant["x-example-key"] && variant.additionalProperties && typeof variant.additionalProperties === "object") {
} else if (variant["x-example-key"] && getDynamicProperty(variant)) {
const exampleKey = variant["x-example-key"];
const addlProp = resolvePropertyRef(variant.additionalProperties);
const addlProp = getDynamicProperty(variant);
if (addlProp.description) {
lines.push(formatComment(addlProp.description, indent + 2));
}
Expand Down Expand Up @@ -295,11 +311,11 @@ function generateProperty(propName, prop, indent = 0, isRequired = false) {
if (prop.properties) {
const subLines = generateProperties(prop.properties, prop.required || [], indent + 2);
lines.push(subLines);
} else if (prop["x-example-key"] && prop.additionalProperties && typeof prop.additionalProperties === "object") {
} else if (prop["x-example-key"] && getDynamicProperty(prop)) {
// Dynamic-key object (additionalProperties pattern): expand an annotated example entry
// so users can see the per-key schema rather than a bare '{}'.
const exampleKey = prop["x-example-key"];
const addlProp = resolvePropertyRef(prop.additionalProperties);
const addlProp = getDynamicProperty(prop);
if (addlProp.description) {
lines.push(formatComment(addlProp.description, indent + 2));
}
Expand Down
11 changes: 8 additions & 3 deletions scripts/generate-schema-docs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ let allPassed = true;
console.log("\nRunning tests...\n");

// Test 1: Engine $ref resolution
allPassed &= assertContains(output, 'engine: "claude"', "Engine $ref resolved - should show string option");
allPassed &= assertContains(output, 'engine: "example-value"', "Engine $ref resolved - should show string option");

allPassed &= assertContains(output, 'id: "claude"', "Engine $ref resolved - should show object variant with id field");
allPassed &= assertContains(output, 'id: "example-value"', "Engine $ref resolved - should show object variant with id field");

allPassed &= assertContains(output, "max-turns:", "Engine $ref resolved - should show max-turns field");

Expand Down Expand Up @@ -100,7 +100,8 @@ allPassed &= assertContains(output, "```yaml wrap", "YAML code block should have
allPassed &= assertNotContains(output, "```yaml\n---\n# Workflow name", "YAML code block should NOT be plain ```yaml without wrap");

// Test 8: Verify that all $refs in schema can be resolved
const allRefs = ["#/$defs/engine_config", "#/$defs/stdio_mcp_tool", "#/$defs/http_mcp_tool", "#/properties/permissions", "#/properties/defaults", "#/properties/concurrency"];
// Defaults is nested within safe-output configuration, not a root schema property.
const allRefs = ["#/$defs/engine_config", "#/$defs/stdio_mcp_tool", "#/$defs/http_mcp_tool", "#/properties/permissions", "#/properties/concurrency"];

for (const ref of allRefs) {
const path = ref.substring(2).split("/");
Expand Down Expand Up @@ -148,6 +149,10 @@ allPassed &= assertContains(output, "workflow:", "dispatch-repository example sh

allPassed &= assertContains(output, "allowed_repositories:", "dispatch-repository example should include 'allowed_repositories' field");

// Test 11: Verify dynamic-key schema properties are expanded
allPassed &= assertContains(output, "run-install-scripts:", "Runtime example should include run-install-scripts");
allPassed &= assertContains(output, "report-failed-jobs:", "Safe outputs example should include report-failed-jobs");

// Summary
console.log("\n" + "=".repeat(50));
if (allPassed) {
Expand Down