feat(get trail): add --output markdown format#953
Merged
Conversation
Render `kosli get trail` as GitHub-Flavored Markdown so the result can be piped into a CI job summary (GitHub's $GITHUB_STEP_SUMMARY) or a GitLab summary.md artifact. This is the first slice of an explicit, opt-in alternative to an implicit per-command CI summary (see #904): output formatting is already a format->renderer registry, so `markdown` slots in next to `table`/`json`. - add printTrailAsMarkdown + mdCell cell-escaping helper - extract eventFields from eventRow so table and markdown share field logic (table output is byte-identical) - register markdown in the get trail output map and update its --output help - golden test for `get trail --output markdown` + a server-free renderer unit test Refs #904 Signed-off-by: Mike Long <mikelong2005@gmail.com>
Contributor
|
Claude finished @ToreMerkely's task in 3m 29s —— View job Review:
|
| Check | Result |
|---|---|
go build ./... |
✅ pass |
go vet ./cmd/... (compiles test files too) |
✅ pass |
go mod tidy + git diff go.mod go.sum |
✅ clean |
(go test isn't runnable in this sandbox — permission-gated — but go vet compiles the test package, so the new test files build.)
Verdict: looks good to merge 👍
This is a clean, well-scoped slice. The format → renderer registry wiring is the right seam to hang markdown off, and extracting eventFields from eventRow so both renderers share the description logic (keeping table output byte-identical) is exactly the kind of refactor that makes this safe.
Notably, the feedback from earlier review rounds has been addressed in the current head:
mdCellnow normalizesnil → ""instead of<nil>(getTrail.go:163).- The short-sha slice is length-guarded:
len(sha1) >= 7(getTrail.go:445). - A zero-attestation artifact is skipped rather than emitting an empty table (
getTrail.go:251-253). - New server-free unit tests cover the git-commit block,
mdCellescaping (|, CR/CRLF, angle brackets), the_No events._path, attestation statuses, environment/attestation links, and the short-sha guard.
Strengths
- Escaping is thorough and correct.
mdCellhandles&/</>/|and normalizes CR, CRLF, LF before converting to<br>— the comment explaining why CR counts as a line ending in CommonMark is a nice touch. - Linking logic anchors carefully.
mdEventDescriptionanchors on"for "and the quoted env name to avoid mislinking an attestation type that shares a name with its reference — and that edge is locked in by a test. - Test design. The
goldenFileExacthelper (byte-for-byte vs. the regex-per-linegoldenFile) is the right call for deterministic markdown containing regex metacharacters like[name](url), and the rationale is documented on the struct fields.
Remaining nits (all low priority, non-blocking)
- Unchecked type assertions in the shared
eventFieldspath.event.(map[string]interface{})(getTrail.go:426),eventMap["timestamp"].(float64)(:427), andeventMap["type"].(string)(:458) still panic on a missing/partial field. These are pre-existing (lifted fromeventRow) and the server always returns well-formed payloads, so risk is minimal — but sinceeventFieldsis now reachable from two renderers, a comma-ok guard returning an error would be more defensive. Your call whether it's worth it for server-controlled data. Fix this → - Scope is correctly bounded to
get trail; the follow-up plan for the other read commands reads sensibly.
Nice work — happy for this to land as the first slice.
• feat/output-markdown
Review findings on the markdown renderer: - mdCell escaped LF but not CR/CRLF; per CommonMark a bare CR is a line ending, so a CRLF git commit message terminated a table row mid-cell. Normalize CRLF and CR to LF before the <br> replacement. - mdCell(nil) rendered the literal "<nil>" for missing fields (e.g. a trail with no description); render an empty cell instead. - Add a unit case covering the git-commit block, the empty-events path, and pipe/LF/CRLF/CR escaping - none of which the original fixture exercised. Refs #904 Signed-off-by: Mike Long <mikelong2005@gmail.com>
…output Improvements driven by rendering a real production trail: - Trail heading links to the trail page in the Kosli app (host/org/flows/flow/trails/name), so the CI summary links back to Kosli. printTrailAsMarkdown becomes a method on getTrailOptions to access the flow name. - Git commit sha links to the commit URL, both in the Git commit block (replacing the separate URL row) and in the events table. eventFields now returns a trailEventFields struct carrying the commit URL, which also removes the unused named returns. - Compliance values get a glanceable emoji prefix: COMPLIANT, NON_COMPLIANT, INCOMPLETE and per-event compliant/non-compliant. - Only the first line of the commit message is shown; a full PR-description-sized message flattened with <br> dominated the summary. - mdCell also escapes &, < and > so commit authors like "Name <email>" are not swallowed as HTML by GFM renderers. - New Origin row links the summary to the CI run that produced the trail, when origin_url is set. Refs #904 Signed-off-by: Mike Long <mikelong2005@gmail.com>
Started/stopped running events now link the environment name to the
environment snapshot in the Kosli app:
{host}/{org}/environments/{env}/{snapshot-index}, falling back to the
environment page when no snapshot index is present.
eventFields captures environment_name and snapshot_index for the two
running event types, and the merged switch case derives the verb from
the event type, keeping table output identical.
Approval events are intentionally left unlinked as the feature is
slated for deprecation.
Refs #904
Signed-off-by: Mike Long <mikelong2005@gmail.com>
…wn output
Attestation events now link their reference (e.g. artifact.snyk-scan, or
the template reference name for trail-level attestations) to the
attestation on the trail page: {trail-url}?attestation_id={id}. Events
without an attestation_id stay unlinked.
The replacement is anchored on "for " so an attestation type sharing its
name with the reference cannot be linked by mistake. The trail URL is
now computed once and shared by the heading and event links.
Refs #904
Signed-off-by: Mike Long <mikelong2005@gmail.com>
Contributor
Author
…tables The trail metadata and git commit tables are key/value, so the column headers add noise. GFM tables require a header row, so use an empty one. Refs #904 Signed-off-by: Mike Long <mikelong2005@gmail.com>
Render an "### Attestations" section: headerless two-column tables of attestation name (linked to the attestation on the trail page via ?attestation_id=) and its compliance status as an emoji, grouped by the trail and by each artifact (with the artifact's own compliance state). All server-defined statuses are handled (per server trails.py / compliance_checker.py): MISSING -> ⏳, COMPLETE+is_compliant true -> ✅, COMPLETE+is_compliant false -> ❌, and the unexpected flag (reported but not in the template) ->⚠️ . mdComplianceState also gains MISSING for artifact-level status. The section is omitted when a trail has no attestation statuses. The get-trail integration golden gains the section because its template declares a trail attestation (bar) and an artifact (cli/foo) that are MISSING on a freshly-begun trail. Refs #904 Signed-off-by: Mike Long <mikelong2005@gmail.com>
CI surfaced two issues with the markdown slice: - The golden-file test helper compares each line as a regex, so the markdown links ([name](url)) and query strings (?attestation_id=) were parsed as regex and failed (invalid char class range 'i-b' from [cli-build-1]). Add a goldenFileExact test option that compares the file byte-for-byte (via the existing compareFileBytes), and use it for the deterministic markdown output. The regex-based goldenFile remains for output with varying parts like timestamps. - Lint (staticcheck QF1002): mdAttestationCompliance now uses a tagged switch on status. Refs #904 Signed-off-by: Mike Long <mikelong2005@gmail.com>
Contributor
Author
|
Example output: Trail: a4ea0d5a233553c61efcb00724b98a3e97e024a8
Git commit
Attestationsartifact — ✅ COMPLIANT
Events
|
Two low-risk fixes from PR #953 review: - Skip the per-artifact attestation block when the artifact has zero attestations. Previously it emitted a "**name** — status" header followed by an empty table when another artifact or the trail had attestations (so the section-level total > 0 guard passed). - Length-guard the short-sha slice on the shared event path (sha1[0:7]) so a malformed sha under 7 chars can't panic. Pre-existing (lifted from eventRow), now reachable from the markdown renderer too. Adds server-free unit tests for both paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mbevc1
approved these changes
Jun 16, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.



What
Adds
markdownas an output format forkosli get trail:This is the first thin slice of an explicit, opt-in approach to #904 (CI summaries), proposed as an alternative to implicitly summarising every command in CI. See the discussion comment for the full rationale.
The idea: output formatting is already a
format → rendererregistry, somarkdownslots in next totable/json. The user redirects it wherever they want — which works identically for both CIs and sidesteps GitLab's lack of a native job summary:How
printTrailAsMarkdownrenders trail metadata, an optional git-commit block, and the events list as GitHub-Flavored Markdown tables.mdCellescapes|/newlines so cell values can't break the table layout.eventFieldsfromeventRowso the table and markdown renderers share the event-description logic — table output is byte-identical.markdownin theget trailoutput map and updated its--outputhelp text.Tests
get trail --output markdown→testdata/output/get/get-trail-markdown.txt(runs in the integration suite against the local server).TestPrintTrailAsMarkdown) feeds a fixture throughprintTrailAsMarkdownand asserts it matches the golden byte-for-byte — passes locally without the test server.Scope / open questions
get trailonly. If the approach lands, next slices extend to the other read/detail commands (get artifact,get snapshot,diff snapshots,assert artifact), then docs with the CI usage examples above.attest *,report artifact) — they emit a one-line message, not structured data worth rendering.Refs #904
🤖 Generated with Claude Code