Commit trailers and annotations¶
Everything colophon reads out of a commit message, in one place. A commit can carry meaning in three positions, and colophon reads all three:
- the subject line, which decides the version bump and the changelog entry
- the trailer block, the message's last paragraph, which carries the
Key: valueannotations below - a fenced block in the body, for note text too long for one line
Nothing else in a commit is read. A commit that carries none of this is still counted, and still appears in the considered total.
At a glance¶
| Annotation | Where | What it does |
|---|---|---|
type(scope): subject |
subject line | decides the bump and writes the changelog entry |
type!: subject |
subject line | marks the change breaking |
BREAKING CHANGE: |
trailer block | marks the change breaking |
Release-As: |
trailer block | sets this release's version outright |
Release-Note: |
trailer block | contributes one line of prose to the notes |
```release-note |
body | contributes longer prose to the notes |
Updates: |
trailer block | carries a statement to a forge object when the commit lands |
Keys are matched case-insensitively, so Release-As:, release-as: and
RELEASE-AS: are the same annotation.
What counts as a trailer¶
Git treats a line as a trailer only when it sits in the message's last
paragraph, and only when every line in that paragraph is a Key: value
pair. A Key: value line followed by more prose is ordinary text, and
colophon will not read it. The one exception colophon makes, for a forge
closing reference, is below.
feat: add the widget
Some explanation of the change.
Release-Note: The widget config key moved to widget.enabled.
A value may continue onto indented lines beneath it, and colophon folds those into the value as git does:
Every value for a key is read, not just the last. Two Release-Note: lines
contribute two notes; two conflicting Release-As: lines are an error rather
than a silent tie-break, because keeping one would hide both the conflict and
any malformed value that happened to come first.
Closing references are allowed¶
A commit that ends with a forge closing reference still has its annotations read:
fix(publish): read the manifest at the released commit
Release-Note: publish now reads tag_also at the commit being released.
Closes #13
Closes #13, Fixes cicd#42, Refs !18 and See group/sub/project#7 are
tolerated, whether they sit in the trailer block or in a paragraph of their
own below it. They contribute nothing themselves; they simply stop
disqualifying the annotations above them.
This reads more than git does, deliberately. git interpret-trailers
--parse treats a bare Closes #13 as prose, and prose in the last paragraph
means no trailer block at all — so git reports no trailers for the message
above, and colophon reports the note. Everything git does read, colophon
reads identically; what is added is the message whose git form was got wrong
by accident, which is otherwise discarded in silence. Writing Closes: #13
with a colon satisfies both tools.
The tolerance is narrow on purpose: a keyword, then one reference token
beginning # or !, optionally with a project path. A fence, a sentence, or
Closes issue #15 is not tolerated, because the rule exists to stop a fenced
Release-As: inside documentation cutting a permanent tag.
An annotation colophon cannot read is reported¶
Where the block still does not qualify — prose after the trailers, or an
annotation inside a fenced example — plan and propose say so:
A warning rather than an error: a message may quote an annotation in prose, and refusing would make a repository unreleasable over a commit already on its branch.
The subject line¶
type decides the bump:
| Type | Bump |
|---|---|
feat |
minor |
fix |
patch |
perf |
patch |
refactor |
patch |
anything else (chore, docs, test, ci, style, …) |
none |
A type that moves nothing is still considered and still counted; it simply does not decide a version. A commit whose subject is not Conventional Commits at all is treated the same way: counted, deciding nothing, and its trailers are still read.
scope is optional. It appears in the changelog entry and has no effect on
the version.
! before the colon marks the change breaking, with the effect below.
Breaking changes¶
A change is breaking when its subject carries !, or its trailer block
carries a BREAKING CHANGE: footer.
At 1.0.0 and above a breaking change is a major bump.
Below 1.0.0 it is a minor bump, not a major one, and the plan says so:
breaking change, held to a minor below 1.0 — promote deliberately with a `Release-As: 1.0.0` trailer
Semantic versioning permits breaking changes in a minor below 1.0, and
reaching 1.0.0 is a decision a maintainer makes rather than something a commit
footer does to them. Promote with Release-As: 1.0.0 when the project is
ready. See
How the version is decided.
Release-As:¶
Sets the version for this release, overriding the computed one.
| Value | A semantic version. A leading v is accepted and stripped. |
| Scope | Honoured wherever it appears among the commits being considered. |
| Conflict | Two different values in one range is an error, not a tie-break. |
| Invalid value | An error. It does not fall back to the computed version. |
| Reported as | Note: version set to <v> by a Release-As: trailer |
See Set the version explicitly.
Release-Note:¶
Contributes prose to the release's notes section.
| Trailer key | release-note |
| Fence info string | release-note |
| Per commit | Any number, in the order they appear in the message. |
| Ordering | Fenced blocks always precede trailers, as a consequence of the last-paragraph rule. |
| Unterminated block | Ignored, rather than read to the end of the message. |
Trailer form, for one line:
Fenced form, for anything longer:
```release-note
Gadgets built before 0.2 need regenerating.
Run `gadget regen` once after upgrading.
```
Where the section appears and what it is headed is set in
.colophon.yaml. See
Write a release note.
Updates:¶
Carries a statement to a forge object when the commit lands, rather than when
a release is cut. Applied by colophon apply, which a
pipeline runs on every push to the target branch. It is how a record outside the repository — an issue, a
merge request, a wiki page — is told what became of the work.
Target names the object. A project is optional everywhere, and omitting it means the repository the commit is in — which is the common case, and the one to reach for first:
| Form | Means |
|---|---|
#123 |
issue 123 in this repository |
!45 |
merge request 45 in this repository |
/wikis/specs/0079-thing |
a wiki page in this repository |
phpboyscout/cicd#123 |
issue 123 in another project |
phpboyscout/cicd!45 |
merge request 45 in another project |
phpboyscout/cicd/wikis/specs/0079-thing |
a wiki page in another project |
Two things about this are easy to get wrong, and both fail late:
- A project is a full path, not a name.
cicdis not a project; the project isphpboyscout/cicd. A bare name parses perfectly well and then resolves to a project of that name at the root namespace, so the mistake surfaces as404 Not Foundwhen the trailer is applied — after the merge, when the commit can no longer be changed. - The wiki form for this repository needs its leading slash.
/wikis/specs/0079-thingis a page in this repository;wikis/specs/0079-thingnames nothing and is refused, because without the slash there is no separator to split a project from a path.
Keyword is one of a small closed vocabulary. A word colophon does not know is refused and reported, never guessed at:
| Keyword | On an issue or merge request | On a wiki page |
|---|---|---|
shipped |
comments "Shipped." | sets status: IMPLEMENTED |
implemented |
comments "Implemented." | sets status: IMPLEMENTED |
Text after the keyword is optional. It is added to the comment on a tracker object, and ignored on a wiki page, where there is nowhere to put it; the run says so rather than dropping it silently.
fix(publish): read the manifest at the released commit
Updates: #13 shipped Fixed in v0.5.0.
Updates: /wikis/specs/0079-pipeline-churn implemented
The same commit moving a spec in another project names it in full:
Each comment carries a marker naming the commit that asserted it, so a pipeline retried by a person or a runner posts nothing twice. A later commit saying the same thing about the same object is a second comment, because it is a second assertion.
A target the forge cannot carry — a wiki on a forge without one, an issue
comment on Bitbucket — is reported and skipped, and the rest still apply. A
skipped target does not fail the run, because no change to the repository could
clear it. A target the forge refuses does: the rest are still carried, and
the failure is reported to the caller. See colophon apply for
what each forge can do and what the run exits.
Colophon never decides for itself that work is finished. A record is updated
because a commit said so, exactly as Release-As: states a version the tool
cannot work out. See
spec 0012.
Where each one is read¶
| Annotation | plan |
propose |
publish |
release |
|---|---|---|---|---|
subject line and ! |
yes | yes | — | — |
BREAKING CHANGE: |
yes | yes | — | — |
Release-As: |
yes | yes | — | — |
Release-Note: and its fence |
yes | yes | — | when notes are generated |
Updates: |
— | — | — | — |
apply reads Updates: and nothing else; the release verbs read everything
else and not it.
publish reads the changelog file at the merged commit rather than the
commits themselves, which is why the annotations do not appear in its column:
by then they have already been rendered into the file propose wrote. See
Tagging what actually landed.