Skip to content

What colophon needs from a pipeline

The list a pipeline has to satisfy for colophon to work, on any forge. The how-to guides for GitLab CI and GitHub Actions show each item as configuration; this page is the contract they implement.

1. A full checkout, with tags

Every verb starts by reading the checkout (history.Read): the worktree root, the remote, and for plan and propose the current version, which is the highest semver tag reachable from HEAD, then every commit since it. A shallow clone cannot answer that. It hides tags and it truncates the walk, and colophon fails closed rather than guessing:

ERRO reading commits: iterating commits: object not found

That is what a depth-1 clone (no tags visible) and GitLab's default depth of 20 (one tag visible) both produce, measured with colophon 0.2.1 against go/config-afero on 2026-09-04. The message does not yet say "shallow"; when you see it in a pipeline, the checkout depth is the first thing to check.

forge what makes the checkout full
GitLab CI GIT_DEPTH: "0" on the job. The cicd component sets it on every job that runs the colophon binary (colophon-release-check runs only git and fetches its own depth); do not override it with a project-wide shallow depth. Tags are fetched with a full clone.
GitHub Actions actions/checkout with fetch-depth: 0, which its README defines as "all history for all branches and tags".

Only the reading side depends on the checkout. Everything that writes (the release commit, the tag, the containment check, the notes at a commit) goes through pkg/tree, which clones from the remote in memory. So a job that passes this requirement for plan passes it for every verb.

2. A token that can push tags and talk to the forge

The token is read from <FORGE>_TOKEN, upper-cased from the forge the remote points at: GITLAB_TOKEN, GITHUB_TOKEN, CODEBERG_TOKEN, BITBUCKET_TOKEN. The forge is detected from the remote's host (pkg/remote, knownHosts), and a host it does not know is refused with ErrUnknownForge; a self-hosted Gitea or GitLab is therefore not usable today, and GITEA_TOKEN exists in the credential table without a way to reach it. See the configuration reference for the basic-auth user each token is sent as.

It needs to: read and write merge requests or pull requests, push the release branch, push the tag, and create the release. On GitLab that is api + write_repository. Where tags are protected, the token's owner must be allowed to create them (v* at Maintainers on this estate's projects).

It must not be the pipeline's own job token. A tag pushed with GitLab's CI_JOB_TOKEN fires no tag pipeline, and GitHub documents that "events triggered by the GITHUB_TOKEN will not create a new workflow run" apart from workflow_dispatch and repository_dispatch. Either way the build that should follow the tag never starts. Use a project or group token on GitLab, and a GitHub App installation token or a personal access token on GitHub.

plan needs no token at all.

3. An identity for the release commit and tag

colophon writes an annotated tag and a release commit and signs them with a name and email (--author-name, --author-email; the component inputs of the same names). Defaults are colophon <[email protected]>. Set them explicitly rather than letting git take one from the runner's ambient configuration, or the same release is attributed differently depending on where it ran.

4. publish before propose, as separate jobs

After a release merge request merges, the release commit is on the target but the tag does not exist yet, and in that window colophon computes the same release again. So on the default branch publish runs first and propose second, and they are two jobs: publish tags through its own clone and pushes to the remote, and propose sees that tag only because it is a separate job whose fetch happened after the push. One job running both commands re-opens a merge request for the release just tagged. The reasoning in full: Tagging what actually landed.

5. Runs on the target branch only

Both verbs act on the branch named by --target (default main). Gate the jobs to the default branch and to nothing else: not merge-request pipelines, not tags, not schedules. The release merge request's own pipeline should run only what proves that merge request is what colophon wrote; the cicd component's colophon-release-check is that job.

6. Project settings the tool assumes

  • Merge requests merge by fast-forward, with a rebase when the branch is behind. colophon resolves the landed commit against the target rather than from the request, so squash and rebase both work, but fast-forward is what keeps CHANGELOG.md's commit and the tag on the same object.
  • Merging requires a passing pipeline.
  • CHANGELOG.md is colophon's to write. Hand edits belong in an ordinary merge request, not the release one.
  • .colophon.yaml is optional: it holds a release, or sets where release notes go and under what heading. See the release manifest.

7. For releases with assets, a tag pipeline

A repository whose releases carry binaries publishes with --tag-only on the default branch, builds and uploads on the tag pipeline, and then creates the release carrying links to what was uploaded (colophon release --tag, or publish --assets --asset-base on older components). The tag pipeline needs the same full checkout and the same token. See Publish a release with its assets.

8. The image the job runs in

colophon itself needs nothing from the image but a git remote it can reach. One thing bites jobs that run the git CLI against the checkout from a non-root image: the runner's helper creates the checkout as root, and git refuses a directory owned by another user. git diff a b refuses silently and prints error: Could not access '<sha>' as if the object were missing. Run git config --global --add safe.directory "$CI_PROJECT_DIR" first, or set safe.directory '*' in the image. colophon's own verbs are unaffected: go-git does not consult safe.directory.