Skip to content

Publish releases to a site

Goal: have every release appear on a page that renders from data — a changelog, a feed, a status board — without anybody copying it across.

The file adapter appends each release to a list in a file in another repository and commits it. The push is what deploys, so the row is live a few minutes later.

Configure it

# .colophon.yaml
announce:
  file:
    repo: phpboyscout/blog
    token: $BLOG_TOKEN
    branch: main
    strip_prefix: phpboyscout/
    path: data/releases/{path_dashed}.yaml
    list: releases
    key: url
    entry:
      name: "{name}"
      tag: "{tag}"
      date: "{released_at}"
      url: "{url}"
    header:
      project: "{path}"
      name: "{project}"
      url: "{project_url}"
    message: "chore(releases): {path} {tag}"

The manifest reference lists every placeholder. The ones that matter most:

  • {path_dashed} turns go/config into go-config, which is what makes one file per project rather than one shared file. Two pipelines writing two files never conflict; two appending to one would race each other into merge conflicts, and releases in this estate arrive in trains.
  • {released_at} is when the forge published the release, not when the job ran. A release cut late, or a job retried, still sorts where it belongs.

YAML or JSON

Both work. The extension decides: .yaml, .yml or .json. If your path has no extension, or has one that does not match the contents, name it:

    format: json

A JSON file is edited in place — the adapter inserts the entry and leaves the rest of the file exactly as it found it, rather than decoding and re-encoding, which would reshuffle the fields of every entry already there.

Give it a token

token names an environment variable. A value written into the manifest is refused when the manifest is parsed, before anything runs.

The variable needs write_repository on the target repository and nothing else. Set it as a CI variable on the project that releases, masked.

Run it after the release exists

colophon-announce:
  stage: notify
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - colophon --ci announce --tag "$CI_COMMIT_TAG"

The verb reads the release object, so it must run after the release exists — not beside the job that creates it.

Running it twice is safe

The adapter reads the file first and looks for an entry whose key already matches this release. If it is there it writes nothing and reports:

  file      unchanged

That is worth relying on. A retried pipeline, a re-run job, or two jobs racing each other all end with one entry, because the record itself is the check — there is no marker file and no state kept anywhere else.

If the branch is protected

    mode: merge_request
    merge: when_ready

This pushes a branch and opens a merge request instead of committing directly. The branch name comes from the project and tag, so a re-run finds its own request rather than opening a second one.

merge: when_ready asks the forge to merge once its checks pass. A forge that cannot wait for checks is reported rather than given a plain merge instead:

  file      announced
  note      merge when_ready is not supported by this forge; the request is open

That refusal is deliberate. "Merge when the checks pass" and "merge now" differ exactly when the checks would have failed, which is the case you set it for.

What it will not do

It only ever appends. Existing entries are never reordered, reformatted or corrected, because the file is a record of what shipped: a person may have edited an entry by hand, and a release withdrawn upstream is still something that happened. Correcting an entry is a job for a merge request.

See also