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
2 changes: 1 addition & 1 deletion .github/workflows/agentic-auto-upgrade.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions DEVGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)"
Expand Down
69 changes: 69 additions & 0 deletions pkg/parser/schema_embed_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
161 changes: 161 additions & 0 deletions scripts/check-stale-schema-binary.sh
Original file line number Diff line number Diff line change
@@ -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 <path>] [--schemas-dir <dir>] [--base-ref <git-ref>]
#
# Options:
# --binary <path> Path to the gh-aw binary (default: ./gh-aw).
# --schemas-dir <dir> Directory containing the JSON schema files
# (default: pkg/parser/schemas).
# --base-ref <git-ref> Git base ref for detecting changed files via
# `git diff <base-ref>...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 <path>] [--schemas-dir <dir>] [--base-ref <git-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
Loading