Platforms
Datasources and managers are platform-neutral. The platform is where the merge requests, the dashboard issue, the local> presets and the project listing live: one interface (publish.Platform) with two implementations.
- GitLab: proven in production across an estate of 200 repositories
- GitHub: in production since September 2026 on a first repository, as a GitHub App without automerge; automerge is proven against a fake that speaks the API
- Others: a package under
platform/, a test server, a case inwire
GitLab
Proven in production across an estate of 200 repositories.
| Variable | |
|---|---|
PINUP_GITLAB_URL / CI_SERVER_URL | the instance |
PINUP_GITLAB_TOKEN / GITLAB_TOKEN | a personal or project access token with api and write_repository; CI_JOB_TOKEN is used when none is set, and it can only read |
PINUP_REGISTRY_HOST / CI_REGISTRY | the instance's container registry, where the token is exchanged for a pull token |
Merge requests are found by source branch (open ones only; a closed one is history and is not reopened), created and aligned with title, description, labels and merge-when-pipeline-succeeds. An automerge the permissions refuse stands next to the request, not as an error. A request whose branch the plan no longer names is closed with autoclosed appended to its title. A commit's signature verdict comes from the API, so a run knows whether its commits show as Verified. The dashboard is the bot's own issue with exactly that title; an issue somebody else names that way is not the dashboard. local> presets come through the raw-file endpoint. Autodiscovery walks every non-archived project the token is a member of.
pinup token rotate renews the bot's own token before it expires and writes the new one into the CI variables it names, at group or project level. A job token cannot rotate itself.
GitHub
In production since September 2026 on a first repository: a Helm chart whose workflow runs pinup as a GitHub App and opens pull requests without automerge. The dashboard works with such an installation token from version 0.49.5 on. Automerge on GitHub is proven against a fake that speaks the API, not yet in production.
| Variable | |
|---|---|
PINUP_PLATFORM=github | or implied by a GitHub token with no GitLab instance in the environment |
PINUP_GITHUB_URL / GITHUB_SERVER_URL | the host; github.com when empty; an Enterprise host under /api/v3 |
PINUP_GITHUB_TOKEN / GITHUB_TOKEN | classic repo, or fine-grained with contents, pull requests and issues read/write; on github.com it serves the github-* datasources as well |
Pull requests are found by head as owner:branch (a fork's branch of the same name is a different request), created and aligned (title, body, labels). Auto-merge is armed and withdrawn over GraphQL, the only API GitHub has for it; “not yet” (nothing to wait for) is told apart from “not allowed” (repository setting), and the latter stands next to the request. git authenticates through the askpass as x-access-token. The dashboard is the bot's own issue with exactly that title; the pull requests the issues endpoint returns along with it stay out. A GitHub App installation token and a workflow's GITHUB_TOKEN have no user; /user answers 403. For them the dashboard is the open issue of that title written by a bot account. A person's issue of the same title is never taken, and two bot-written issues of the title are an error, not a choice. Autodiscovery lists the repositories the token may push to and that are not archived.
What GitHub does not know is said, not simulated: a request cannot ask for its branch to be deleted on merge (that is the repository setting delete_branch_on_merge), and auto-merge needs a repository that allows it.
Adding one
An implementation is a package under platform/ with the methods of publish.Platform, an iface.go that asserts them, a test server that speaks the protocol (pagination included) behind the refusing transport, a case in wire.Platform and one in the environment reader. Gitea and Forgejo are close to GitLab's shape.