diff --git a/.github/workflows/agentic-auto-upgrade.yml b/.github/workflows/agentic-auto-upgrade.yml index e169cea32b6..7035101ee35 100644 --- a/.github/workflows/agentic-auto-upgrade.yml +++ b/.github/workflows/agentic-auto-upgrade.yml @@ -34,7 +34,7 @@ name: Agentic Auto-Upgrade on: schedule: - - cron: "11 4 * * 6" # Weekly (auto-upgrade) + - cron: "21 3 * * 5" # Weekly (auto-upgrade) workflow_dispatch: permissions: diff --git a/DEVGUIDE.md b/DEVGUIDE.md index 0116a88218c..aba1bf1be4b 100644 --- a/DEVGUIDE.md +++ b/DEVGUIDE.md @@ -341,6 +341,24 @@ make build make recompile ``` +#### Schema changes not taking effect / stale schema errors +Files under `pkg/parser/schemas/` are embedded in the binary at compile time via +`//go:embed`. Changing those JSON files without rebuilding means the running binary +still holds the old schema. + +**Solution**: Rebuild after any schema edit: +```bash +make build +``` + +Running `go test ./pkg/parser/...` (or `make test-unit`) validates that every +embedded schema is well-formed JSON via the `TestEmbeddedSchemasAreValid` test, +but it does **not** rebuild the `./gh-aw` CLI. If you need the running CLI to +reflect your schema edits, `make build` is still required. + +The `make check-stale-schema-binary` target (also run as part of `make lint`) uses +`git diff` to detect schema-file changes and verifies the binary is up to date. + #### "cannot find package" errors **Solution**: Clean and reinstall dependencies: ```bash diff --git a/Makefile b/Makefile index 73854ebf738..9bf7966dde9 100644 --- a/Makefile +++ b/Makefile @@ -807,6 +807,28 @@ check-workflow-drift: fi @bash scripts/check-workflow-drift.sh ./$(BINARY_NAME) +# Guard against a stale binary when files under pkg/parser/schemas/ are modified. +# Schema files are embedded at compile time via //go:embed, so changing them without +# rebuilding means the running binary still holds the old schema. This target +# detects when schema files have been modified (via git) but the binary has not been +# rebuilt. It is the schema-side parallel of check-stale-lock-files. +# Requires the gh-aw binary to exist (run `make build` first). +.PHONY: check-stale-schema-binary +check-stale-schema-binary: + @base_ref="$${CHECK_STALE_SCHEMA_BASE_REF:-}"; \ + if [ -z "$$base_ref" ] && [ -n "$${GITHUB_BASE_REF:-}" ]; then \ + if git rev-parse --verify "origin/$${GITHUB_BASE_REF}^{commit}" >/dev/null 2>&1; then \ + base_ref="origin/$${GITHUB_BASE_REF}"; \ + elif git rev-parse --verify "$${GITHUB_BASE_REF}^{commit}" >/dev/null 2>&1; then \ + base_ref="$${GITHUB_BASE_REF}"; \ + fi; \ + fi; \ + if [ -n "$$base_ref" ]; then \ + bash scripts/check-stale-schema-binary.sh --binary "./$(BINARY_NAME)" --base-ref "$$base_ref"; \ + else \ + bash scripts/check-stale-schema-binary.sh --binary "./$(BINARY_NAME)"; \ + fi + # Format code .PHONY: fmt fmt: fmt-go fmt-cjs fmt-json @@ -958,7 +980,7 @@ lint-action-sh: # Validate all project files .PHONY: lint -lint: check-stale-lock-files fmt-check fmt-check-json lint-cjs golint validate-model-alias-chains lint-action-sh +lint: check-stale-lock-files fmt-check fmt-check-json lint-cjs golint validate-model-alias-chains lint-action-sh check-stale-schema-binary @echo "✓ All validations passed" # Install the binary locally @@ -1219,6 +1241,7 @@ help: @echo " validate-workflows - Validate compiled workflow lock files (depends on build)" @echo " check-workflow-drift - Check for drift between .md sources and .lock.yml files (builds binary if missing)" @echo " check-stale-lock-files - Fast guard: detect modified .md files without regenerated .lock.yml (no binary needed)" + @echo " check-stale-schema-binary - Guard: detect modified schema files under pkg/parser/schemas/ without a binary rebuild" @echo " install - Install binary locally" @echo " sync-action-pins - Sync actions-lock.json from .github/aw to pkg/actionpins/data and pkg/workflow/data (runs automatically during build)" @echo " sync-action-scripts - Sync install-gh-aw.sh and install-gh-aw.ps1 to actions/setup-cli/ (runs automatically during build)" diff --git a/pkg/parser/schema_embed_test.go b/pkg/parser/schema_embed_test.go new file mode 100644 index 00000000000..f8423e37336 --- /dev/null +++ b/pkg/parser/schema_embed_test.go @@ -0,0 +1,69 @@ +//go:build !integration + +package parser + +import ( + "encoding/json" + "testing" +) + +// TestEmbeddedSchemasAreValid verifies that every schema file embedded via //go:embed +// is both valid JSON and a compilable JSON schema. +// +// This test acts as a fast guard: because //go:embed re-reads the files at compile +// time, running `go test ./pkg/parser/...` (or `make test-unit`) is equivalent to +// rebuilding the binary with respect to schema embedding. Any change to a file +// under pkg/parser/schemas/ that introduces malformed JSON or an invalid schema +// will therefore fail here before the broken schema can reach a deployed binary. +func TestEmbeddedSchemasAreValid(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + schemaVar string + schemaURL string + }{ + { + name: "main_workflow_schema", + schemaVar: mainWorkflowSchema, + schemaURL: "http://contoso.com/main-workflow-schema.json", + }, + { + name: "mcp_config_schema", + schemaVar: mcpConfigSchema, + schemaURL: "http://contoso.com/mcp-config-schema.json", + }, + { + name: "repo_config_schema", + schemaVar: RepoConfigSchema, + schemaURL: "http://contoso.com/repo-config-schema.json", + }, + { + name: "aw_manifest_schema", + schemaVar: awManifestSchema, + schemaURL: "http://contoso.com/aw-manifest-schema.json", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // 1. Verify the embedded content is non-empty. + if len(tc.schemaVar) == 0 { + t.Fatalf("embedded schema %q is empty; check the //go:embed directive and the file path", tc.name) + } + + // 2. Verify the embedded content is valid JSON. + var doc any + if err := json.Unmarshal([]byte(tc.schemaVar), &doc); err != nil { + t.Fatalf("embedded schema %q is not valid JSON: %v\nRun `make build` after editing files under pkg/parser/schemas/", tc.name, err) + } + + // 3. Verify the schema can be compiled by the JSON schema compiler. + if _, err := CompileSchema(tc.schemaVar, tc.schemaURL); err != nil { + t.Fatalf("embedded schema %q failed to compile: %v\nRun `make build` after editing files under pkg/parser/schemas/", tc.name, err) + } + }) + } +} diff --git a/scripts/check-stale-schema-binary.sh b/scripts/check-stale-schema-binary.sh new file mode 100755 index 00000000000..d059059e480 --- /dev/null +++ b/scripts/check-stale-schema-binary.sh @@ -0,0 +1,161 @@ +#!/bin/bash +set +o histexpand + +# check-stale-schema-binary.sh - Guard against a stale binary when schema files change +# +# Detects whether any file under pkg/parser/schemas/ was modified (relative to a +# base ref or the current working tree) without a corresponding rebuild of the +# gh-aw binary. When drift is found the script exits non-zero with a clear +# remediation message. +# +# Because schema files are embedded at compile time via //go:embed, changing them +# without rebuilding means the running binary still holds the old schema. This +# script is the parallel of check-stale-lock-files.sh for the schema → binary +# relationship. +# +# Usage: +# check-stale-schema-binary.sh [--binary ] [--schemas-dir ] [--base-ref ] +# +# Options: +# --binary Path to the gh-aw binary (default: ./gh-aw). +# --schemas-dir Directory containing the JSON schema files +# (default: pkg/parser/schemas). +# --base-ref Git base ref for detecting changed files via +# `git diff ...HEAD`. Intended for CI use. +# +# Exit codes: +# 0 - No modified schema files, or all modified schemas have a binary newer +# than the most recently changed schema file +# 1 - Schema files were modified but the binary has not been rebuilt + +set -euo pipefail + +# Disable colors when not connected to a TTY, when NO_COLOR is set, or when +# TERM=dumb — keeps output readable in CI step summaries. +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != "dumb" ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + NC='\033[0m' +else + RED='' + GREEN='' + YELLOW='' + NC='' +fi + +BINARY="./gh-aw" +SCHEMAS_DIR="pkg/parser/schemas" +BASE_REF="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --binary) + BINARY="${2:?--binary requires an argument}" + shift 2 + ;; + --schemas-dir) + SCHEMAS_DIR="${2:?--schemas-dir requires an argument}" + shift 2 + ;; + --base-ref) + BASE_REF="${2:?--base-ref requires an argument}" + shift 2 + ;; + *) + echo -e "${RED}ERROR${NC}: unknown argument: $1" >&2 + echo "Usage: check-stale-schema-binary.sh [--binary ] [--schemas-dir ] [--base-ref ]" >&2 + exit 1 + ;; + esac +done + +if [ ! -d "$SCHEMAS_DIR" ]; then + echo -e "${RED}ERROR${NC}: schemas directory not found: $SCHEMAS_DIR" >&2 + exit 1 +fi + +collect_modified_files() { + if [ -n "$BASE_REF" ]; then + if git rev-parse --verify "${BASE_REF}^{commit}" >/dev/null 2>&1; then + git diff --name-only "${BASE_REF}...HEAD" + return + fi + echo -e "${YELLOW}WARN${NC}: --base-ref not found (${BASE_REF}); falling back to working-tree check." >&2 + fi + + # Local path: staged/unstaged changes relative to HEAD plus untracked files. + git diff --name-only HEAD 2>/dev/null || true + git ls-files --others --exclude-standard "$SCHEMAS_DIR" 2>/dev/null || true +} + +all_modified=$(collect_modified_files) + +# Filter to JSON files within the schemas directory. +schemas_prefix="${SCHEMAS_DIR#./}" +modified_schemas=$(printf '%s\n' "$all_modified" \ + | grep "^${schemas_prefix}/.*\.json$" \ + || true) + +if [ -z "$modified_schemas" ]; then + echo -e "${GREEN}✓ No modified schema files detected.${NC}" + exit 0 +fi + +echo "Modified schema file(s) detected:" +while IFS= read -r f; do + [ -n "$f" ] || continue + echo " $f" +done <<< "$modified_schemas" +echo "" + +# If the binary does not exist, it definitely hasn't been rebuilt. +if [ ! -f "$BINARY" ]; then + echo -e "${RED}ERROR${NC}: Binary not found at '$BINARY' — schema files were modified but the binary has not been built." + echo "" + echo -e "${YELLOW}Fix:${NC} Rebuild the binary so the updated schemas are embedded:" + echo "" + echo " make build" + echo "" + exit 1 +fi + +# Compare modification times: the binary must be newer than every changed schema file. +# Note: git checkout does not preserve file timestamps, so this check is meaningful +# only in a local working tree where file edits set real mtimes. CI workflows should +# always run `make build` before executing the binary regardless. +# +# For deleted schema files the individual path no longer exists; use the schemas +# directory mtime as a proxy (directory mtime updates on any entry add/remove). +stale_schemas=() +while IFS= read -r f; do + [ -n "$f" ] || continue + if [ -f "$f" ]; then + ref_path="$f" + else + # Deleted file: fall back to the schemas directory mtime. + ref_path="$SCHEMAS_DIR" + fi + if [ "$ref_path" -nt "$BINARY" ]; then + stale_schemas+=("$f") + fi +done <<< "$modified_schemas" + +if [ ${#stale_schemas[@]} -eq 0 ]; then + echo -e "${GREEN}✓ Binary is up to date with all schema changes.${NC}" + exit 0 +fi + +echo -e "${RED}ERROR${NC}: The following schema change(s) are not yet reflected in the binary at '${BINARY}':" +echo "" +for f in "${stale_schemas[@]}"; do + echo " $f" +done +echo "" +echo -e "${YELLOW}Fix:${NC} Rebuild the binary so the updated schemas are embedded:" +echo "" +echo " make build" +echo "" +echo "Schema files are embedded at compile time via //go:embed." +echo "Running \`go test ./pkg/parser/...\` validates schemas but does not rebuild the ./gh-aw binary." +exit 1