Skip to content
Open
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: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ build/

# Generated files
*.lock.yml
schemas/audit.schema.json
schemas/logs.schema.json

# DevContainer configuration (should not be reformatted)
.devcontainer/devcontainer.json
Expand Down
15 changes: 14 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1256,9 +1256,22 @@ sync-install-script-hashes:
@bash scripts/update-install-script-hashes.sh
@echo "✓ Install script hashes synced successfully"

# Generate audit and logs JSON schemas
.PHONY: schemas
schemas: build
@set -eu; \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot create target to make schemas

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in d44e604. make schemas now generates the audit and logs schema artifacts, and make recompile depends on it.

for schema in audit logs; do \
target="schemas/$$schema.schema.json"; \
tmp=$$(mktemp "$$target.tmp.XXXXXX"); \
trap 'rm -f "$$tmp"' EXIT HUP INT TERM; \
./$(BINARY_NAME) json-schema "$$schema" > "$$tmp"; \
mv "$$tmp" "$$target"; \
trap - EXIT HUP INT TERM; \
done

# Recompile all workflow files
.PHONY: recompile
recompile: build
recompile: schemas
./$(BINARY_NAME) init --codespaces ""
./$(BINARY_NAME) compile --validate --verbose --purge --schedule-seed github/gh-aw
# ./$(BINARY_NAME) compile --dir pkg/cli/workflows --validate --verbose --purge
Expand Down
7 changes: 7 additions & 0 deletions cmd/gh-aw/argument_syntax_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ func TestArgumentSyntaxConsistency(t *testing.T) {
argsValidator: "MinimumNArgs(1)",
shouldValidate: func(cmd *cobra.Command) error { return cmd.Args(cmd, []string{"123456"}) },
},
{
name: "json-schema command requires schema",
command: cli.NewJSONSchemaCommand(),
expectedUse: "json-schema <schema>",
argsValidator: "ExactArgs(1)",
shouldValidate: func(cmd *cobra.Command) error { return cmd.Args(cmd, []string{"audit"}) },
},
{
name: "trial command requires workflow-spec",
command: cli.NewTrialCommand(validateEngine),
Expand Down
1 change: 1 addition & 0 deletions cmd/gh-aw/command_groups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func TestCommandGroupAssignments(t *testing.T) {
{name: "completion command in utilities group", commandName: "completion", expectedGroup: "utilities", shouldHaveGroup: true},
{name: "hash-frontmatter command in utilities group", commandName: "hash-frontmatter", expectedGroup: "utilities", shouldHaveGroup: true},
{name: "project command in utilities group", commandName: "project", expectedGroup: "utilities", shouldHaveGroup: true},
{name: "json-schema command in utilities group", commandName: "json-schema", expectedGroup: "utilities", shouldHaveGroup: true},

// Commands without groups (intentionally)
{name: "version command without group", commandName: "version", expectedGroup: "", shouldHaveGroup: false},
Expand Down
6 changes: 4 additions & 2 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ type commandSet struct {
addCmd, addWizardCmd, editCmd, updateCmd, deployCmd, trialCmd, initCmd, statusCmd, listCmd *cobra.Command
mcpCmd, logsCmd, auditCmd, viewCmd, healthCmd, outcomesCmd, mcpServerCmd, prCmd, secretsCmd *cobra.Command
fixCmd, upgradeCmd, completionCmd, hashCmd, projectCmd, doctorCmd, checksCmd, validateCmd, lintCmd *cobra.Command
domainsCmd, experimentsCmd, forecastCmd, gradersCmd, modelsCmd, envCmd *cobra.Command
domainsCmd, experimentsCmd, forecastCmd, gradersCmd, modelsCmd, envCmd, jsonSchemaCmd *cobra.Command
}

func fixPathForCommand(s string) string {
Expand Down Expand Up @@ -743,6 +743,7 @@ func createCommandSet() commandSet {
gradersCmd: cli.NewGradersCommand(),
envCmd: cli.NewEnvCommand(),
modelsCmd: cli.NewModelsCommand(),
jsonSchemaCmd: cli.NewJSONSchemaCommand(),
}
cli.RegisterEngineFlagCompletion(cmds.initCmd)
return cmds
Expand Down Expand Up @@ -861,6 +862,7 @@ func assignCommandGroups(cmds commandSet) {
cmds.statusCmd.GroupID, cmds.listCmd.GroupID, cmds.experimentsCmd.GroupID, cmds.forecastCmd.GroupID, cmds.modelsCmd.GroupID = "analysis", "analysis", "analysis", "analysis", "analysis"
cmds.gradersCmd.GroupID = "analysis"
cmds.mcpServerCmd.GroupID, cmds.prCmd.GroupID, cmds.completionCmd.GroupID, cmds.hashCmd.GroupID, cmds.projectCmd.GroupID = "utilities", "utilities", "utilities", "utilities", "utilities"
cmds.jsonSchemaCmd.GroupID = "utilities"
}

func addCommandsToRoot(cmds commandSet) {
Expand All @@ -869,7 +871,7 @@ func addCommandsToRoot(cmds commandSet) {
runCmd, removeCmd, cmds.statusCmd, cmds.listCmd, enableCmd, disableCmd, cmds.logsCmd, cmds.auditCmd, cmds.viewCmd,
cmds.healthCmd, cmds.outcomesCmd, cmds.checksCmd, cmds.mcpCmd, cmds.mcpServerCmd, cmds.prCmd, versionCmd, cmds.secretsCmd,
cmds.fixCmd, cmds.validateCmd, cmds.lintCmd, cmds.completionCmd, cmds.hashCmd, cmds.projectCmd, cmds.doctorCmd,
cmds.domainsCmd, cmds.experimentsCmd, cmds.forecastCmd, cmds.gradersCmd, cmds.modelsCmd, cmds.envCmd,
cmds.domainsCmd, cmds.experimentsCmd, cmds.forecastCmd, cmds.gradersCmd, cmds.modelsCmd, cmds.envCmd, cmds.jsonSchemaCmd,
)
}

Expand Down
50 changes: 50 additions & 0 deletions docs/adr/59720-expose-json-schemas-for-cli-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# ADR-59720: Expose JSON schemas for CLI output

**Date**: 2026-09-09
**Status**: Draft
**Deciders**: gh-aw maintainers

---

### Context

`gh aw audit --json` and `gh aw logs --json` already emit structured data, but the repository did not provide a supported way for users or downstream tooling to discover those payload shapes. This PR adds a `json-schema` CLI command, checked-in schema artifacts, Makefile regeneration, tests, and documentation centered on the audit and logs outputs. The change also keeps the Go output types as the source of truth by generating schemas directly from `AuditData` and `LogsData`. The repository needs an explicit decision on whether structured CLI outputs should expose versioned machine-readable schemas as part of the developer-facing interface.

### Decision

We will expose JSON schemas for the structured `audit` and `logs` CLI outputs through a new `gh aw json-schema <schema>` command and commit regenerated schema artifacts under `schemas/`. We will generate those schemas directly from the existing Go output types and share the same serialization path between CLI output and checked-in artifacts so the published schemas stay deterministic and consistent. We will also regenerate the artifacts during `make recompile` so schema freshness becomes part of the normal repository regeneration flow.

### Alternatives Considered

#### Alternative 1: Keep structured JSON output undocumented and schema-less

The project could continue emitting `audit --json` and `logs --json` without publishing schemas. This was considered because it avoids adding a new command, generated files, and regeneration logic. It was not chosen because downstream automation would still need to reverse-engineer payloads, and the PR explicitly adds tests and docs to make the output contract inspectable.

#### Alternative 2: Hand-maintain static schema files separately from the Go types

Another option would be to write `audit.schema.json` and `logs.schema.json` manually and update them when the output types change. This was considered because it could avoid adding schema-generation entry points to the CLI. It was not chosen because the PR evidence shows a stronger preference for using `GenerateOutputSchema[...]()` and shared marshaling so JSON tags and type definitions remain the single source of truth.

#### Alternative 3: Expose schemas only in repository files, without a CLI command

The project could check in generated schema artifacts but omit a user-facing command. This was considered because consumers could read the committed files directly from the repository. It was not chosen because the PR intentionally adds `gh aw json-schema audit` and `gh aw json-schema logs`, making schema discovery available from the installed CLI and not only from the source tree.

### Consequences

#### Positive
- Downstream tools gain a supported machine-readable contract for `gh aw audit --json` and `gh aw logs --json`.
- Generating schemas from `AuditData` and `LogsData` reduces drift between implementation and published schema.
- Deterministic CLI output and checked-in artifacts make schema changes easier to test and review.

#### Negative
- The repository now carries large generated schema artifacts that must be regenerated when output types change.
- `make recompile` takes on additional responsibility, so schema generation failures can block broader regeneration workflows.
- Exposing schemas makes output-shape changes more visible and may increase compatibility expectations for future changes.

#### Neutral
- A new `json-schema` command is added to the CLI utilities group.
- Tests now verify command registration, argument handling, schema validity, determinism, and artifact freshness.
- `.prettierignore` is extended to exclude the generated schema files from formatting.

---

*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*
11 changes: 11 additions & 0 deletions docs/src/content/docs/reference/audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ With multiple comparisons, `--json` emits a single object for one comparison or

When artifacts are present, audit processing also persists extracted skill-activation data into `run_summary.json`, which downstream automation can consume alongside the rendered report.

## JSON output schemas

Use `gh aw json-schema` to generate a JSON Schema for structured audit or logs output. The `audit` schema describes `gh aw audit --json`, while the `logs` schema describes `gh aw logs --json`. The schema is written to stdout and can be redirected to a file:

```bash
gh aw json-schema audit > audit.schema.json
gh aw json-schema logs > logs.schema.json
```

`make recompile` regenerates the checked-in `schemas/audit.schema.json` and `schemas/logs.schema.json` files. These schemas derive directly from the Go `AuditData` and `LogsData` types, so changes to either type may change its generated schema.

## `gh aw logs --format <fmt>`

Generate a cross-run security and performance audit report across multiple recent workflow runs.
Expand Down
39 changes: 39 additions & 0 deletions pkg/cli/json_schema_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cli

import (
"fmt"
"io"
"os"

"github.com/github/gh-aw/pkg/constants"
"github.com/spf13/cobra"
)

// NewJSONSchemaCommand creates the json-schema command.
func NewJSONSchemaCommand() *cobra.Command {
return newJSONSchemaCommand(os.Stdout)
}

func newJSONSchemaCommand(output io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "json-schema <schema>",
Short: "Generate JSON Schemas for structured command output",
Long: "Generate the JSON Schema for audit or logs JSON output.",
Example: ` ` + string(constants.CLIExtensionPrefix) + ` json-schema audit
` + string(constants.CLIExtensionPrefix) + ` json-schema logs`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{"audit", "logs"},
RunE: func(cmd *cobra.Command, args []string) error {
data, err := GenerateNamedOutputSchema(args[0])
if err != nil {
return err
}
if _, err := output.Write(data); err != nil {
return fmt.Errorf("failed to write %s schema: %w", args[0], err)
}
return nil
},
SilenceUsage: true,
}
return cmd
}
Loading
Loading