Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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.

51 changes: 26 additions & 25 deletions .github/workflows/schema-consistency-checker.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ 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 frontmatter type definitions.
# pkg/parser/frontmatter.go is only a logger declaration; frontmatter fields
# are extracted and represented in pkg/workflow/frontmatter_types.go.
FRONTMATTER_FIELDS=$(grep -Eho '(json|yaml):"[^"]*"' pkg/workflow/frontmatter_types.go 2>/dev/null \
| sed -E 's/^(json|yaml):"//;s/"$//' \
| sed 's/,omitempty//' \
| sed 's/,.*$//' \
| grep -v '^-$' \
Expand Down Expand Up @@ -74,14 +75,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 +99,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 +126,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 +246,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 +297,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 +352,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 +418,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
41 changes: 40 additions & 1 deletion docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -19212,6 +19212,11 @@ safe-outputs:
# (optional)
failure-issue-repo: "example-value"

# Controls whether to report failed non-builtin jobs as issues (default: true).
# Set to false to disable.
# (optional)
report-failed-jobs: true

# Maximum number of bot trigger references (e.g. 'fixes #123', 'closes #456')
# allowed in output before all of them are neutralized. Default: 10. Supports
# integer or GitHub Actions expression (e.g. '${{ inputs.max-bot-mentions }}').
Expand Down Expand Up @@ -20117,7 +20122,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
Loading