Skip to content

feat(get trail): add --output markdown format#953

Merged
ToreMerkely merged 11 commits into
mainfrom
feat/output-markdown
Jun 16, 2026
Merged

feat(get trail): add --output markdown format#953
ToreMerkely merged 11 commits into
mainfrom
feat/output-markdown

Conversation

@meekrosoft

Copy link
Copy Markdown
Contributor

What

Adds markdown as an output format for kosli get trail:

kosli get trail $TRAIL --flow $FLOW --output markdown

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 → renderer registry, so markdown slots in next to table/json. The user redirects it wherever they want — which works identically for both CIs and sidesteps GitLab's lack of a native job summary:

# GitHub Actions
kosli get trail $TRAIL --flow $FLOW --output markdown >> "$GITHUB_STEP_SUMMARY"

# GitLab (then expose summary.md as an artifact)
kosli get trail $TRAIL --flow $FLOW --output markdown > summary.md

How

  • printTrailAsMarkdown renders trail metadata, an optional git-commit block, and the events list as GitHub-Flavored Markdown tables.
  • mdCell escapes |/newlines so cell values can't break the table layout.
  • Extracted eventFields from eventRow so the table and markdown renderers share the event-description logic — table output is byte-identical.
  • Registered markdown in the get trail output map and updated its --output help text.

Tests

  • Golden test: get trail --output markdowntestdata/output/get/get-trail-markdown.txt (runs in the integration suite against the local server).
  • A server-free renderer unit test (TestPrintTrailAsMarkdown) feeds a fixture through printTrailAsMarkdown and asserts it matches the golden byte-for-byte — passes locally without the test server.

Scope / open questions

  • Draft to work out the approach before fanning out.
  • This slice covers get trail only. 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.
  • Intentionally does not touch write commands (attest *, report artifact) — they emit a one-line message, not structured data worth rendering.

Refs #904

🤖 Generated with Claude Code

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>
@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ToreMerkely's task in 3m 29s —— View job


Review: feat(get trail): add --output markdown format

  • Build / vet / go mod tidy checks
  • Review getTrail.go markdown renderer
  • Review tests & golden fixtures
  • Post review summary

Static analysis

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:

  • mdCell now normalizes nil → "" 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, mdCell escaping (|, CR/CRLF, angle brackets), the _No events._ path, attestation statuses, environment/attestation links, and the short-sha guard.

Strengths

  • Escaping is thorough and correct. mdCell handles &/</>/| 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. mdEventDescription anchors 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 goldenFileExact helper (byte-for-byte vs. the regex-per-line goldenFile) 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 eventFields path. event.(map[string]interface{}) (getTrail.go:426), eventMap["timestamp"].(float64) (:427), and eventMap["type"].(string) (:458) still panic on a missing/partial field. These are pre-existing (lifted from eventRow) and the server always returns well-formed payloads, so risk is minimal — but since eventFields is 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

Comment thread cmd/kosli/getTrail_markdown_test.go
Comment thread cmd/kosli/getTrail.go
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>
Comment thread cmd/kosli/getTrail.go
…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>
@meekrosoft

Copy link
Copy Markdown
Contributor Author

Adding some screenshots:

image image image

…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>
Comment thread cmd/kosli/getTrail.go
Comment thread cmd/kosli/getTrail.go
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>
@meekrosoft

meekrosoft commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Example output:

Trail: a4ea0d5a233553c61efcb00724b98a3e97e024a8

Name a4ea0d5a233553c61efcb00724b98a3e97e024a8
Description
Compliance ✅ COMPLIANT
Last modified at Sun, 14 Jun 2026 12:10:46 CEST • 21 hours ago
Origin https://github.com/kosli-dev/server/actions/runs/27403054727

Git commit

Sha1 a4ea0d5a233553c61efcb00724b98a3e97e024a8
Author AlexKantor87 <alex@kosli.com>
Timestamp Fri, 12 Jun 2026 10:03:58 CEST • 3 days ago
Message Extract _boolean_rule_required to remove the is_required leak class (#5860)

Attestations

artifact — ✅ COMPLIANT

snyk-container-test ✅ compliant
snyk-code-test ✅ compliant
todo-wip-count ✅ compliant
lint-src ✅ compliant
lint-tests ✅ compliant
validate-openapi ✅ compliant
unit-test-junit ✅ compliant
unit-test-coverage ✅ compliant
integration-test-junit ✅ compliant
integration-test-coverage ✅ compliant
combined-test-coverage ✅ compliant
system-test ✅ compliant
pull-request ✅ compliant
frontend-dependencies ✅ compliant
frontend-format ✅ compliant
frontend-test-junit ✅ compliant
frontend-cucumber ✅ compliant
frontend-type-check ✅ compliant
frontend-lint ✅ compliant
quality-assurance-decision ✅ compliant (+)
binary-provenance-decision ✅ compliant (+)
code-review-decision ✅ compliant (+)
snyk-scan ✅ compliant (+)

Events

Time Description Git commit Compliance
Fri, 12 Jun 2026 10:12:22 CEST trail started a4ea0d5
Fri, 12 Jun 2026 10:12:37 CEST 'pull_request' attestation reported for artifact.pull-request a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:12:39 CEST 'generic' attestation reported for artifact.todo-wip-count a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:13:55 CEST 'snyk' attestation reported for artifact.snyk-code-test a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:14:35 CEST artifact '772819027869.dkr.ecr.eu-central-1.amazonaws.com/merkely:a4ea0d5' created for template name 'artifact' a4ea0d5
Fri, 12 Jun 2026 10:15:21 CEST 'custom:frontend-dependencies' attestation reported for artifact.frontend-dependencies a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:15:40 CEST 'generic' attestation reported for artifact.lint-tests a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:15:42 CEST 'generic' attestation reported for artifact.validate-openapi a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:15:51 CEST 'generic' attestation reported for artifact.lint-src a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:15:52 CEST 'generic' attestation reported for artifact.frontend-format a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:15:58 CEST 'generic' attestation reported for artifact.frontend-type-check a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:16:02 CEST 'custom:frontend-lint' attestation reported for artifact.frontend-lint a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:16:04 CEST 'generic' attestation reported for artifact.frontend-cucumber a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:16:10 CEST 'junit' attestation reported for artifact.frontend-test-junit a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:16:12 CEST 'junit' attestation reported for artifact.system-test a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:17:06 CEST artifact '772819027869.dkr.ecr.eu-central-1.amazonaws.com/merkely:a4ea0d5' created for template name 'artifact' a4ea0d5
Fri, 12 Jun 2026 10:17:07 CEST 'generic' attestation reported for artifact.snyk-container-test a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:17:49 CEST 'junit' attestation reported for artifact.integration-test-junit a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:17:52 CEST 'generic' attestation reported for artifact.integration-test-coverage a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:18:03 CEST 'junit' attestation reported for artifact.unit-test-junit a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:18:06 CEST 'generic' attestation reported for artifact.unit-test-coverage a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:18:26 CEST 'system:decision' attestation reported for artifact.quality-assurance-decision a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:18:27 CEST 'system:decision' attestation reported for artifact.binary-provenance-decision a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:18:34 CEST 'system:decision' attestation reported for artifact.code-review-decision a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:18:39 CEST 'generic' attestation reported for artifact.combined-test-coverage a4ea0d5 ✅ compliant
Fri, 12 Jun 2026 10:22:33 CEST artifact 'artifact' started running in 'staging-aws'
Fri, 12 Jun 2026 10:23:07 CEST artifact 'artifact' started running in 'infra-dev-aws'
Fri, 12 Jun 2026 10:44:37 CEST approval #3027 created by 'external://ToreMerkely'
Fri, 12 Jun 2026 10:44:37 CEST approval #3028 created by 'external://ToreMerkely'
Fri, 12 Jun 2026 10:44:38 CEST approval #3029 created by 'external://ToreMerkely'
Fri, 12 Jun 2026 10:44:38 CEST approval #3030 created by 'external://ToreMerkely'
Fri, 12 Jun 2026 10:46:17 CEST artifact 'artifact' started running in 'prod-us-aws'
Fri, 12 Jun 2026 10:46:24 CEST artifact 'artifact' started running in 'natwest-aws'
Fri, 12 Jun 2026 10:46:25 CEST artifact 'artifact' started running in 'prod-aws'
Fri, 12 Jun 2026 10:47:03 CEST artifact 'artifact' started running in 'blackstone-aws'
Fri, 12 Jun 2026 11:11:06 CEST artifact 'artifact' stopped running in 'infra-dev-aws'
Fri, 12 Jun 2026 11:11:33 CEST artifact 'artifact' stopped running in 'staging-aws'
Fri, 12 Jun 2026 12:30:45 CEST 'snyk' attestation reported for artifact.snyk-scan 91f30bc ✅ compliant
Sat, 13 Jun 2026 12:00:41 CEST 'snyk' attestation reported for artifact.snyk-scan 4f7ef6b ✅ compliant
Sun, 14 Jun 2026 12:10:46 CEST 'snyk' attestation reported for artifact.snyk-scan 4f7ef6b ✅ compliant

Replace the " — ⚠️ unexpected" suffix on unexpected attestations with a
terse " +" marker (e.g. "✅ compliant +").

Refs #904

Signed-off-by: Mike Long <mikelong2005@gmail.com>
Refs #904

Signed-off-by: Mike Long <mikelong2005@gmail.com>
Comment thread cmd/kosli/getTrail.go Outdated
Comment thread cmd/kosli/getTrail.go
@meekrosoft meekrosoft marked this pull request as ready for review June 16, 2026 07:13
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>
@ToreMerkely ToreMerkely merged commit 8bf9e8b into main Jun 16, 2026
20 checks passed
@ToreMerkely ToreMerkely deleted the feat/output-markdown branch June 16, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants