Skip to content

Migrate to labelled pull requests

PullRequestDraft and PullRequest gained a Labels field. If you never set labels, nothing about your code changes and you can stop reading.

If you do set them, one thing changes and it is sharp.

Create can return a pull request and an error

When a label could not be applied, Create returns the created pull request together with an error wrapping ErrNotHonoured. The pull request exists. The error says a label did not land.

So this, which was correct before, is now a bug:

pr, err := prs.Create(ctx, owner, repo, draft)
if err != nil {
    return err // treats a successful create as a failure
}

Create makes no at-most-once claim, so the retry that usually follows a returned error opens a second pull request.

Write this instead:

pr, err := prs.Create(ctx, owner, repo, draft)
if err != nil && !errors.Is(err, forge.ErrNotHonoured) {
    return err // a real failure; nothing was created
}

// pr exists either way.
if err != nil {
    // A label was dropped. errors.Hints names which.
    log.Warn("pull request created without every label", "err", err)
}

This is the same shape ReleasePublisher.CreateRelease already uses, and the same two guarantees hold: the returned value is populated whenever the thing exists, and ErrNotHonoured never travels alone. If your codebase calls both, check they agree — two call sites disagreeing about one sentinel is the defect this shape produces.

Every label must already exist

A name that matches no label on the repository is not created. It is not sent, and it comes back in the ErrNotHonoured hint.

Matching is exact. Release::Pending does not resolve to a release::pending label.

GitLab and GitHub would both create an unknown name on the spot, and Gitea cannot, because its API takes label IDs rather than names. Rather than guarantee something one adapter provably cannot honour, the contract declines everywhere — a guarantee that holds on three forges of four is worse than none, because it reads as safe and only fails where nobody tested.

Seed your labels first. On GitLab a group label is inherited by every subproject and appears in the project's own label listing, so one group label covers the whole group with no per-project setup. GitHub has no organisation-level inheritance, so seeding is per-repository.

Labels cannot be changed afterwards

There is no AddLabels, no SetLabels, no RemoveLabels, and Update does not touch them. Labels is set once, at creation.

Mutating a label is how tooling encodes state on a pull request, and a caller that reads a forge record as evidence of what landed is the failure ResolveMergedCommit exists to prevent. If you genuinely need it, reach your platform's own client through ProviderUnwrapper. That is deliberately the awkward path.

Reference