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:
timelinein.uigraph.yaml— scanned on everyuigraph-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
| Type | Source | sourceRef |
|---|---|---|
decision | An ADR matched by timeline.decisions.paths | adr:<file path> |
incident | A postmortem matched by timeline.incidents.paths | incident:<file path> |
release | A changelog section, or a run of uigraph-cli release | release:<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/**/*.mdwill 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 field | decision reads | incident reads |
|---|---|---|
| Title | title, else the first # heading. Required — a file with neither fails the sync. | Same |
| Summary | summary, else the first paragraph under Context, else under Decision. | summary, else the first paragraph under Summary, else under Impact. |
| Status | status, else the first non-empty line under Status. | — |
| ADR number | adr, else the leading digits of the filename (0007-... → 0007). | — |
| Date | See 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:
- Front-matter
date. AcceptsYYYY-MM-DDor full RFC 3339. - A
YYYY-MM-DDanywhere in the filename — incidents only (2026-07-14-payment-outage.md). Decision files do not use this. - The date of the commit that added the file.
- 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.
Source links
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
| Flag | Default | Description |
|---|---|---|
--version | the tag at HEAD | Release version. An untagged HEAD is a hard error: no --version given and HEAD is not tagged. |
--date | the tag date | Release date, YYYY-MM-DD or RFC 3339. |
--notes | — | Release notes text. |
--notes-file | — | Path to a file holding the release notes. |
--changelog | CHANGELOG.md | Changelog to pull notes from when no notes are given. |
--title | the version | Event title. |
--config | .uigraph.yaml | Path to the config file. |
--api-url | UIGRAPH_GATEWAY_URL | Gateway URL. |
--enterprise | — | Record the release on hosted UIGraph. Add =DEV for the development environment. Cannot be combined with --api-url. |
--dry-run | false | Print 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):
--notes--notes-file- The body of the annotated tag — the message below the subject line, not the subject
- The section of
--changelogmatching the version (a leadingvis ignored on both sides, sov1.4.0finds## [1.4.0]) - 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.