Skip to main content

Timeline and Releases

A service timeline is the dated history of a service: the decisions that shaped it, the incidents it had, and the releases it shipped. The CLI builds it from files already in the repository, so there is nothing to maintain separately. Two commands write events:

  • timeline in .uigraph.yaml — scanned on every uigraph-cli sync. For ADRs, postmortems, and a checked-in changelog.
  • uigraph-cli release — run from a tag-triggered CI job. Records one release event at the moment the tag is cut, when the notes and commit range are freshest.

Event types

TypeSourcesourceRef
decisionAn ADR matched by timeline.decisions.pathsadr:<file path>
incidentA postmortem matched by timeline.incidents.pathsincident:<file path>
releaseA changelog section, or a run of uigraph-cli releaserelease:<version>

Every event is upserted by its sourceRef, derived from the file path or the version — never from the title. Retitling an ADR or rewording a changelog entry updates the existing event; renaming or moving the file creates a new one, because the path is the key.

Config shape

timeline:
decisions:
paths:
- docs/adr/*.md
- docs/decisions/*.md
incidents:
paths:
- docs/postmortems/*.md
releases:
changelogPath: CHANGELOG.md

All three sub-sections are optional; declare only the sources you have. A service block is required — a config without a service cannot sync timeline events.

Path patterns

paths entries are shell-style globs, expanded relative to the working directory the CLI runs in.

  • Single-level * only. docs/**/*.md will not walk subdirectories; recursive patterns are not supported. List each directory explicitly instead.
  • Directories matched by a pattern are skipped; only files become events.
  • Overlapping patterns are de-duplicated and sorted.
  • A pattern that matches nothing is not an error. It silently contributes zero events, which is the usual reason a timeline comes back empty.

Decisions and incidents

Each matched file becomes one event. Front matter always wins; the body is the fallback.

Event fielddecision readsincident reads
Titletitle, else the first # heading. Required — a file with neither fails the sync.Same
Summarysummary, else the first paragraph under Context, else under Decision.summary, else the first paragraph under Summary, else under Impact.
Statusstatus, else the first non-empty line under Status.
ADR numberadr, else the leading digits of the filename (0007-...0007).
DateSee Dates.Same, plus a filename date.

Heading lookups match the heading text at any level, case-insensitively — ## Context, ### context, and # Context are all found.

status, when present, is lower-cased and must be one of proposed, accepted, superseded, deprecated. Anything else fails the sync with decision status "draft" must be one of: proposed, accepted, superseded, deprecated.

An ADR needs no front matter at all — this file yields every field:

# 0007. Use a queue for settlement retries

## Status

Accepted

## Context

Inline retries held the request open for the full backoff window.

Add a front-matter block with any of title, adr, status, date, or summary to override what the body would have supplied.

Dates

Resolved in this order, stopping at the first source that yields one:

  1. Front-matter date. Accepts YYYY-MM-DD or full RFC 3339.
  2. A YYYY-MM-DD anywhere in the filename — incidents only (2026-07-14-payment-outage.md). Decision files do not use this.
  3. The date of the commit that added the file.
  4. The file's modification time, for a file not committed yet.

A malformed explicit date is a hard error rather than a fall-through, so a typo is reported (date "14-07-2026" is not YYYY-MM-DD or RFC3339) instead of silently becoming the commit date.

Every scanned event links back to its file, built from service.repository.url and the current branch. Only github and gitlab have a known layout — a bitbucket repository syncs the events without a source link rather than guessing a URL.

Releases from a changelog

timeline.releases.changelogPath points at a Keep a Changelog file. The path must exist or validation fails. Each ## version heading opens a release whose body runs to the next ## heading:

## [1.4.0] - 2026-07-14

### Added
- Durable retry queue for settlements.

The recognised format is ## [1.2.3] - 2026-01-02. Brackets are optional, a leading v is stripped (## [v1.2.3] is stored as 1.2.3), -, , and all work as the separator, and the date is optional — a section without one falls back to the changelog file's modification time.

A heading that does not start with a digit — including ## [Unreleased] — is skipped, because there is no version to key the event on. If a changelog produces no release events, the headings are almost always the reason.

Recording releases from CI: uigraph-cli release

uigraph-cli release records exactly one release event and exits. It needs service.name in the config and UIGRAPH_TOKEN, as with sync.

uigraph-cli release
uigraph-cli release --version v1.4.0 --notes "Durable retry queue." --dry-run
FlagDefaultDescription
--versionthe tag at HEADRelease version. An untagged HEAD is a hard error: no --version given and HEAD is not tagged.
--datethe tag dateRelease date, YYYY-MM-DD or RFC 3339.
--notesRelease notes text.
--notes-filePath to a file holding the release notes.
--changelogCHANGELOG.mdChangelog to pull notes from when no notes are given.
--titlethe versionEvent title.
--config.uigraph.yamlPath to the config file.
--api-urlUIGRAPH_GATEWAY_URLGateway URL.
--enterpriseRecord the release on hosted UIGraph. Add =DEV for the development environment. Cannot be combined with --api-url.
--dry-runfalsePrint the event without sending it.

Where the notes come from

The command walks these sources in order, stops at the first that yields text, and prints which one won (📝 Notes from: annotated tag):

  1. --notes
  2. --notes-file
  3. The body of the annotated tag — the message below the subject line, not the subject
  4. The section of --changelog matching the version (a leading v is ignored on both sides, so v1.4.0 finds ## [1.4.0])
  5. The commit subjects between the previous tag and this one

Unless the notes already came from the commit subjects, the commit list is appended to the summary, capped at 50 entries with an … and N more line.

Full history is required

The command resolves the previous tag and the commit range between it and this release, so the job must check out full history — in GitHub Actions, actions/checkout with fetch-depth: 0. The default shallow clone has no tags and no range. See CI/CD Setup for the complete tag-triggered job.

Do not use both release sources

timeline.releases.changelogPath and uigraph-cli release write the same event — both key it as release:<version> with the leading v stripped. So the two compete, and whichever runs last overwrites the other's title, notes, and date. Pick one: run uigraph-cli release and omit timeline.releases if the pipeline cuts releases through CI, otherwise keep changelogPath and let sync pick up the changelog.

Output and scope

Timeline scanning is a step of uigraph-cli sync, reported in the summary as Timeline Events: 12 (3 created, 9 updated). sync --dry-run prints the scanned events under === DRY RUN: Timeline Events === and sends nothing; uigraph-cli release --dry-run prints the resolved event including its sourceRef, date, source URL, and summary. The token needs timeline:write for both — without it the request gets a 403 and the command stops with exit code 2.