Trail Metadata Takes Any YAML Shape¶
Summary¶
config.metadata: values used to be strings only. They can now be a string, a list, or a map,
nested to any depth. This is a deliberate breaking change: older Trailblaze versions can’t parse a
trail or results file whose metadata holds a list or a map.
config:
metadata:
jira: PROJ-123
owners: [payments, checkout]
tracker:
ticket: PROJ-123
suites: [smoke, nightly]
Key Decisions¶
First-class type, not a side channel. We first tried a separate metadataLists field next to
the string map, which kept binary compatibility. That meant every reader had to check two places,
and it made lists second-class. Since a breaking change was acceptable, metadata is now
Map<String, TrailMetadataValue> (a sealed type: StringValue, ListValue, MapValue), and all
~30 call sites were migrated. Callers read it with metadata.string(key), .list(key), and
.map(key). Use leaves to search a value of any shape and displayText to show one.
Go all the way to arbitrary nesting now. Lists alone would have solved the immediate need. Metadata is mostly informational, so the point is to let each team shape it however they like. Doing lists now and maps later would have meant a second breaking change for the same field.
Leaves stay strings. ticket: 1017 still reads as "1017", exactly as the old string map did.
YAML scalar typing (numbers, booleans, yes/no) is a trap, and we don’t want a reader to have to
guess which type it got back. JSON numbers and booleans in a session log read back as their text
too.
Null is the only rejection. An empty value (owner:) or ~ anywhere fails with its path, e.g.
owner.team. That’s almost always an authoring mistake, and there’s no string to read it as.
Keys with a meaning read a string, and never coerce another shape. A few keys are more than
informational. The reserved source / sourceReason bridge keys lower into TrailConfig.source,
the report uses owner as the run’s subtitle and Owner sort, and integrations such as CI tooling
can read their own keys to change behavior. Every such reader uses metadata.string(key). A list
or map under one of these keys either errors or stays plain metadata. It is never coerced, so a
one-item list [false] can’t pass for false.
A list under the reserved source key is not a source bridge. Only a string value under a
reserved bridge key lowers into TrailConfig.source. Anything else under that key stays as the
author’s own metadata.
How It Works¶
TrailMetadataValueSerializeruses aSerialKind.CONTEXTUALdescriptor, so kaml hands over the raw node whatever its shape. With any other kind, kaml refuses a scalar, list, or map before the serializer sees it. The JSON path readsdecodeJsonElement(), so session logs written before this change still decode.- On the wire and in reports, lists are JSON arrays and maps are JSON objects.
- The DTO TypeScript codegen can’t see the shape of a CONTEXTUAL descriptor (it would emit
unknown). It now has a smallcustomWireShapestable that emits a named, recursive alias:string | TrailMetadataValue[] | { [key: string]: TrailMetadataValue }. - The trail YAML JSON Schema accepts any non-null value. The report’s Info tab renders nested values
as
ticket: PROJ-123, suites: [smoke, nightly].
Gotchas¶
- Precompiled JVM callers can link and still fail at runtime. Type erasure keeps the
metadataproperty ABI asMapeven though its values changed fromStringtoTrailMetadataValue. A caller compiled against the old type can get aClassCastExceptionwhen it reads a value as aStringor serializes a config it constructed with string values. Recompile consumers against the new models when updating their Trailblaze pin. - Version skew. A consumer pinned to an older Trailblaze fails to parse list or map metadata. Keep trails that older pins read string-only until those pins move to a version with this change.
- Test comparisons still compile. A test that compares a
TrailMetadataValueto a rawStringwithisEqualToorassertEqualsstill compiles (both sides areAny) but fails at runtime. When migrating, compare withmetadataOf(...)or read with.string(key).
Links¶
- Model and serializer:
trailblaze-models/.../yaml/TrailMetadata.kt - Tests:
TrailMetadataTest