-
Notifications
You must be signed in to change notification settings - Fork 1
feat(docs): add cmd/docs schema emitter for docs.baseten.co #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pat-baseten
wants to merge
1
commit into
main
Choose a base branch
from
docs-schema-emitter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # internal/cmddocs | ||
|
|
||
| Walks the declarative `cmd.Root` tree and emits a versioned JSON description of | ||
| every command, flag, example, output shape, and typed error. Consumed by | ||
| `docs.baseten.co` to generate the published CLI reference. | ||
|
|
||
| This is a developer tool, not a user-facing `baseten` subcommand. Its only | ||
| caller is the `docs.baseten.co` pipeline. | ||
|
|
||
| ## Output contract | ||
|
|
||
| The JSON shape is defined by the Go types in `schema.go`. The top-level | ||
| `schema_version` field is "1"; bump `SchemaVersion` in `schema.go` whenever an | ||
| existing field is removed or its meaning changes (adding a new optional field | ||
| does **not** require a bump). | ||
|
|
||
| `group_pri` is emitted verbatim: `0` means the flag set no `group-pri`, and the | ||
| consumer applies the framework default (`DefaultFlagGroupPri = 100`). The walker | ||
| does not resolve it. | ||
|
|
||
| ## Running locally | ||
|
|
||
| ```sh | ||
| # Write to stdout (default). | ||
| go run ./internal/cmddocs --cli-version=dev | ||
|
|
||
| # Write to a file. | ||
| go run ./internal/cmddocs --cli-version=v0.1.0 --out=docs.json | ||
|
|
||
| # Reproducible timestamp. | ||
| SOURCE_DATE_EPOCH=1700000000 go run ./internal/cmddocs --cli-version=v0.1.0 --out=docs.json | ||
| ``` | ||
|
|
||
| ## Tests | ||
|
|
||
| ```sh | ||
| go test ./internal/cmddocs/... | ||
| ``` | ||
|
|
||
| Unit tests exercise the walker over synthetic command trees and assert the | ||
| emitted JSON is valid and stable. There is no committed snapshot to maintain: | ||
| schema drift surfaces in the `docs.baseten.co` pipeline, which regenerates the | ||
| reference against a pinned baseten-cli ref and opens a PR on any change. | ||
|
|
||
| ## How `docs.baseten.co` consumes this | ||
|
|
||
| The docs pipeline pins a baseten-cli ref, checks it out, and runs | ||
| `go run ./internal/cmddocs --cli-version=<tag> --out=docs.json`, then feeds the | ||
| result to its MDX generator. Nothing in this repo's release flow runs the | ||
| emitter or publishes `docs.json`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Command cmddocs walks the declarative cmd.Root tree and emits a versioned | ||
| // JSON description for consumption by docs.baseten.co. It is invoked by the | ||
| // docs.baseten.co pipeline against a pinned baseten-cli ref; nothing in this | ||
| // repo's release flow runs it. | ||
| // | ||
| // Usage: | ||
| // | ||
| // go run ./internal/cmddocs --cli-version=v0.1.0 --out=docs.json | ||
| // go run ./internal/cmddocs --cli-version=dev # writes to stdout | ||
| // | ||
| // Set SOURCE_DATE_EPOCH (Unix seconds) to pin GeneratedAt for reproducible | ||
| // output. | ||
| package main | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "flag" | ||
| "fmt" | ||
| "os" | ||
| "strconv" | ||
| "time" | ||
|
|
||
| cmdpkg "github.com/basetenlabs/baseten-cli/cmd" | ||
| ) | ||
|
|
||
| func main() { | ||
| cliVersion := flag.String("cli-version", "dev", "CLI version string to embed in the output (e.g. v0.1.0).") | ||
| outPath := flag.String("out", "-", "Output file path; '-' writes to stdout.") | ||
| flag.Parse() | ||
|
|
||
| generatedAt := time.Now().UTC().Format(time.RFC3339) | ||
| if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" { | ||
| secs, err := strconv.ParseInt(epoch, 10, 64) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "invalid SOURCE_DATE_EPOCH %q: %v\n", epoch, err) | ||
| os.Exit(2) | ||
| } | ||
| generatedAt = time.Unix(secs, 0).UTC().Format(time.RFC3339) | ||
| } | ||
|
|
||
| schema := Walk(*cliVersion, generatedAt, cmdpkg.Root) | ||
| payload, err := json.MarshalIndent(schema, "", " ") | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "marshal: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| payload = append(payload, '\n') | ||
|
|
||
| w := os.Stdout | ||
| if *outPath != "-" { | ||
| f, err := os.Create(*outPath) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "create %s: %v\n", *outPath, err) | ||
| os.Exit(1) | ||
| } | ||
| defer f.Close() | ||
| w = f | ||
| } | ||
| if _, err := w.Write(payload); err != nil { | ||
| fmt.Fprintf(os.Stderr, "write: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| } |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this package be called |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package main | ||
|
|
||
| // This file defines the versioned JSON schema emitted for docs.baseten.co. The | ||
| // schema is the contract; bump SchemaVersion on any breaking change and | ||
| // coordinate with the consumer. | ||
|
|
||
| // SchemaVersion is the major version of the emitted JSON. Increment when an | ||
| // existing field is removed or its semantics change. Adding a new optional | ||
| // field does not require a bump. | ||
| const SchemaVersion = "1" | ||
|
|
||
| // Schema is the top-level document emitted by the walker. | ||
| type Schema struct { | ||
| SchemaVersion string `json:"schema_version"` | ||
| CLIVersion string `json:"cli_version"` | ||
| // GeneratedAt is the UTC RFC3339 timestamp this Schema was emitted at. | ||
| GeneratedAt string `json:"generated_at"` | ||
| StandardErrors []ErrorEntry `json:"standard_errors"` | ||
| Root Command `json:"root"` | ||
| } | ||
|
|
||
| // Command is one node in the command tree. Non-leaf commands (with Children) | ||
| // have empty Flags/Examples/Output fields. | ||
| type Command struct { | ||
| Name string `json:"name"` | ||
| Path []string `json:"path"` | ||
| Summary string `json:"summary"` | ||
| Description string `json:"description"` | ||
| IsLeaf bool `json:"is_leaf"` | ||
| ArgsUsage string `json:"args_usage"` | ||
| ExactArgs int `json:"exact_args"` | ||
| MaxArgs int `json:"max_args"` | ||
| DisableFlagParsing bool `json:"disable_flag_parsing"` | ||
| Flags []Flag `json:"flags"` | ||
| Examples []Example `json:"examples"` | ||
| JQExample *Example `json:"jq_example"` | ||
| TextDescription string `json:"text_description"` | ||
| JSONDescription string `json:"json_description"` | ||
| JSONOutputType string `json:"json_output_type"` | ||
| JSONArrayStreamed bool `json:"json_array_streamed"` | ||
| Errors []ErrorEntry `json:"errors"` | ||
| Children []Command `json:"children"` | ||
| } | ||
|
|
||
| // Flag is one CLI flag on a leaf command. | ||
| type Flag struct { | ||
| Name string `json:"name"` | ||
| Short string `json:"short"` | ||
| Description string `json:"description"` | ||
| Default string `json:"default"` | ||
| Enum []string `json:"enum"` | ||
| Required bool `json:"required"` | ||
| Oneof string `json:"oneof"` | ||
| Type string `json:"type"` | ||
| FieldName string `json:"field_name"` | ||
| Group string `json:"group"` | ||
| // GroupPri is the rendering priority for the flag's group (lower renders | ||
| // earlier), copied verbatim from the source field's group-pri tag. A value | ||
| // of 0 means the field set no group-pri; the consumer applies the framework | ||
| // default (DefaultFlagGroupPri = 100). The walker does not resolve it. | ||
| GroupPri int `json:"group_pri"` | ||
| } | ||
|
|
||
| // Example is one documented invocation of a command. | ||
| type Example struct { | ||
| Description string `json:"description"` | ||
| Command string `json:"command"` | ||
| } | ||
|
|
||
| // ErrorEntry is one typed error a command may surface. Standard errors at the | ||
| // top level are inherited by every leaf; per-command Errors lists additions. | ||
| type ErrorEntry struct { | ||
| Name string `json:"name"` | ||
| Code int `json:"code"` | ||
| Meaning string `json:"meaning"` | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
| ) | ||
|
|
||
| func Test_Schema_VersionIsStable(t *testing.T) { | ||
| if SchemaVersion != "1" { | ||
| t.Fatalf("SchemaVersion = %q, want %q (bump deliberately and update docs.baseten.co consumer)", SchemaVersion, "1") | ||
| } | ||
| } | ||
|
|
||
| func Test_Schema_MarshalsEmpty(t *testing.T) { | ||
| s := Schema{ | ||
| SchemaVersion: SchemaVersion, | ||
| CLIVersion: "v0.0.0-test", | ||
| GeneratedAt: "2026-01-01T00:00:00Z", | ||
| } | ||
| b, err := json.MarshalIndent(s, "", " ") | ||
| if err != nil { | ||
| t.Fatalf("marshal: %v", err) | ||
| } | ||
| got := string(b) | ||
| want := `{ | ||
| "schema_version": "1", | ||
| "cli_version": "v0.0.0-test", | ||
| "generated_at": "2026-01-01T00:00:00Z", | ||
| "standard_errors": null, | ||
| "root": { | ||
| "name": "", | ||
| "path": null, | ||
| "summary": "", | ||
| "description": "", | ||
| "is_leaf": false, | ||
| "args_usage": "", | ||
| "exact_args": 0, | ||
| "max_args": 0, | ||
| "disable_flag_parsing": false, | ||
| "flags": null, | ||
| "examples": null, | ||
| "jq_example": null, | ||
| "text_description": "", | ||
| "json_description": "", | ||
| "json_output_type": "", | ||
| "json_array_streamed": false, | ||
| "errors": null, | ||
| "children": null | ||
| } | ||
| }` | ||
| if got != want { | ||
| t.Fatalf("JSON mismatch.\nGot:\n%s\nWant:\n%s", got, want) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| cmdpkg "github.com/basetenlabs/baseten-cli/cmd" | ||
| ) | ||
|
|
||
| // WalkCommand converts a declarative cmdpkg.Command into the JSON-emittable | ||
| // Command at the given parent path. The returned node's Path is parentPath | ||
| // with c.Name appended. | ||
| func WalkCommand(parentPath []string, c cmdpkg.Command) Command { | ||
| path := append(append([]string{}, parentPath...), c.Name) | ||
| out := Command{ | ||
| Name: c.Name, | ||
| Path: path, | ||
| Summary: c.Summary, | ||
| Description: c.Description, | ||
| IsLeaf: len(c.Children) == 0, | ||
| ArgsUsage: c.ArgsUsage, | ||
| ExactArgs: c.ExactArgs, | ||
| MaxArgs: c.MaxArgs, | ||
| DisableFlagParsing: c.DisableFlagParsing, | ||
| Flags: flagsFor(c), | ||
| } | ||
| applyOutput(&out, c.Output) | ||
| out.Errors = errorEntries(c.Errors) | ||
| for _, child := range c.Children { | ||
| out.Children = append(out.Children, WalkCommand(path, child)) | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| func applyOutput(dst *Command, spec cmdpkg.CommandOutputSpec) { | ||
| if spec == nil { | ||
| return | ||
| } | ||
| dst.TextDescription = spec.Text() | ||
| dst.JSONDescription = spec.JSON() | ||
| dst.JSONArrayStreamed = spec.JSONArrayStreamedBool() | ||
| if t := spec.JSONOutputType(); t != nil { | ||
| dst.JSONOutputType = t.String() | ||
| } | ||
| for _, ex := range spec.ExampleList() { | ||
| dst.Examples = append(dst.Examples, Example{Description: ex.Description, Command: ex.Command}) | ||
| } | ||
| jq := spec.JQ() | ||
| if jq.Command != "" || jq.Description != "" { | ||
| dst.JQExample = &Example{Description: jq.Description, Command: jq.Command} | ||
| } | ||
| } | ||
|
|
||
| func flagsFor(c cmdpkg.Command) []Flag { | ||
| raw := c.LoadFlags() | ||
| if len(raw) == 0 { | ||
| return nil | ||
| } | ||
| out := make([]Flag, 0, len(raw)) | ||
| for _, f := range raw { | ||
| out = append(out, Flag{ | ||
| Name: f.Name, | ||
| Short: f.Short, | ||
| Description: f.Desc, | ||
| Default: f.Default, | ||
| Enum: f.Enum, | ||
| Required: f.Required, | ||
| Oneof: f.Oneof, | ||
| Type: f.Type.String(), | ||
| FieldName: f.FieldName, | ||
| Group: f.Group, | ||
| GroupPri: f.GroupPri, | ||
| }) | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| // Walk produces the full Schema for the given root command. cliVersion is | ||
| // embedded as-is (e.g. "v0.1.0", "dev"); generatedAt is the RFC3339 timestamp | ||
| // to embed. | ||
| func Walk(cliVersion, generatedAt string, root cmdpkg.Command) Schema { | ||
| return Schema{ | ||
| SchemaVersion: SchemaVersion, | ||
| CLIVersion: cliVersion, | ||
| GeneratedAt: generatedAt, | ||
| Root: WalkCommand(nil, root), | ||
| StandardErrors: errorEntries(cmdpkg.StandardErrors()), | ||
| } | ||
| } | ||
|
|
||
| // errorEntries converts the framework's typed error descriptors into the | ||
| // JSON-emittable ErrorEntry shape. | ||
| func errorEntries(src []cmdpkg.ErrorDesc) []ErrorEntry { | ||
| if len(src) == 0 { | ||
| return nil | ||
| } | ||
| out := make([]ErrorEntry, 0, len(src)) | ||
| for _, e := range src { | ||
| out = append(out, ErrorEntry{Name: e.Name, Code: int(e.Code), Meaning: e.Meaning}) | ||
| } | ||
| return out | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this move to
internal/cmddocsto be less user facing? Runninggo run ./internal/cmddocsshould be ok (and changing the other things in the package to bepackage mainshould be harmless for this kind of tool)