repokeeper init selects GitLab when the origin remote's host contains gitlab; pass
--platform gitlab otherwise (a self-hosted instance under another name, or no remote yet).
What differs from GitHub:
| GitHub | GitLab | |
|---|---|---|
| Templates, CODEOWNERS | .github/ |
.gitlab/ |
| CI | caller workflows of reusable workflows | every job generated into .gitlab-ci.yml |
| Dependency updates | Dependabot | Renovate (renovate.json) |
| Releases | release-please, through a release pull request | semantic-release, on every push to the default branch |
Jobs you add to .gitlab-ci.yml are kept; repokeeper manages only its own top-level keys
(workflow, the jobs of the stacks below, commits, repokeeper, renovate, release).
Every stack has its CI job on GitLab. A job runs the commands the stack's GitHub workflow runs, in a container image instead of on a runner that a setup action prepares:
| Stack | Job | Image | Notes |
|---|---|---|---|
| node | node |
node:<version> |
npm cache; corepack for pnpm and yarn |
| python | python |
python:<version> |
pip install uv first when the project has a uv.lock |
| go | go |
golang:<version> |
stable is the image's latest tag |
| rust | rust |
rust:latest |
the toolchain named in versions is installed with rustfmt and clippy |
| ruby | ruby |
ruby:<version> |
|
| php | php |
php:<version>-cli |
Composer, git and unzip are installed first; the image has PHP's default extensions only |
| dart | dart |
dart:<version>, or ghcr.io/cirruslabs/flutter:<version> for Flutter |
|
| java, kotlin | java, kotlin |
maven:3-eclipse-temurin-<version>, gradle:jdk<version>, or eclipse-temurin:<version>-jdk when the project has its own gradlew |
|
| dotnet | dotnet |
mcr.microsoft.com/dotnet/sdk:<newest version> |
one job; the other SDKs in versions are installed beside it |
| docker | docker |
hadolint/hadolint:<version>-debian |
the lint only; build has no effect |
| script | shell, powershell, script-test |
koalaman/shellcheck-alpine, mcr.microsoft.com/dotnet/sdk (for its PowerShell) |
ShellCheck and shfmt; PSScriptAnalyzer; your test command |
What to know:
- Linux only.
stack_options.<stack>.oshas no effect, and repokeeper says so. The script stack's test command runs once, on Linux, where on GitHub it also runs on Windows. - No dependency caches except npm's. A cache has to live inside the project folder, where formatters and linters would read it as the project's own code.
- Stacks at the root only. A stack in a folder of its own (
stack_options.<stack>.directory) is for GitHub. - Releases write the version themselves. semantic-release knows
package.jsonand nothing else, so for the other stacks the release runsrepokeeper bump <version>, which writes the version into the stack's file:pyproject.toml(orsetup.cfg,setup.py),pubspec.yaml,pom.xml,Cargo.tomlandCargo.lock,composer.jsonwhen it has aversion,lib/<name>/version.rbandGemfile.lock,Directory.Build.props, andgradle.propertiesbetween the markers repokeeper puts around its version line. Those files andCHANGELOG.mdare what the release commits. A stack without a version file (Go, scripts, a Gradle build withoutversion=) is released by its changelog and tag alone. Try it withrepokeeper bump 1.2.3 --dry-run.
Two things to know when you add jobs of your own:
- The managed
workflowruns pipelines for merge requests, the default branch, schedules and tags. repokeeper's own jobs skip tags, so a tag pipeline holds only your jobs (publishing, for example). A job meant for other branches never starts. To write theworkflowrules yourself, list the key underownedin.repokeeper.yml:owned: [".gitlab-ci.yml#workflow"]. - repokeeper's jobs use GitLab's default stages
testanddeploy. If you declarestages, include both; repokeeper warns when one is missing.
Two jobs stay inactive until you set them up in the project's CI/CD settings:
releaseneeds a CI/CD variableGITLAB_TOKEN: a project access token with theapiandwrite_repositoryscopes and a role that may push to the default branch. Every push to the default branch with afeat,fixor breaking change then releases at once: version bump,CHANGELOG.md, tag and GitLab release. There is no release merge request. The release commit (chore(release): x.y.z) starts none of repokeeper's jobs, and it carries no[skip ci], so the pipeline of its tag does run: that is where a publishing job of your own belongs (rules: [{ if: $CI_COMMIT_TAG }]). A repository without avX.Y.Ztag starts at1.0.0; tag the current version first to continue from it. A variable marked Protected is only visible on protected branches, so protect the default branch or leave the variable unprotected; otherwise the job silently stays away.renovateneeds a CI/CD variableRENOVATE_TOKEN(same scopes) and a pipeline schedule, for example weekly. The schedule alone decides how often Renovate runs. It opens one merge request for minor and patch updates, one per major, and an issue named Dependency Dashboard.- Renovate leaves alone the images of repokeeper's own jobs in
.gitlab-ci.yml(node,renovate/renovate, the .NET SDK): the standard names their versions andrepokeeper updatemoves them. A job of your own in that file that uses one of these images is not updated either. - Changelogs in its merge requests come from github.com, which answers few requests without a
token. A CI/CD variable
GITHUB_COM_TOKEN(a GitHub token with no scopes at all) lifts that limit; without it Renovate works and says in the dashboard that release notes were skipped.
- Renovate leaves alone the images of repokeeper's own jobs in
Settings of the project itself are kept under gitlab: in .repokeeper.yml. Only the keys you
write are managed; anything left out stays as it is on GitLab.
gitlab:
description: Keeps the build tools in one place
topics: [cli, tooling]
merge:
method: ff # merge | rebase_merge | ff
squash: default_on # never | always | default_on | default_off
delete_source_branch: true
pipeline_must_succeed: true
discussions_must_be_resolved: true
protect: # the default branch; false removes the protection
push: maintainers # none | developers | maintainers
merge: developers
allow_force_push: false
renovate_schedule: "0 5 * * 1" # cron, in UTC; false removes the schedulerepokeeper gitlab apply --dry-run # show what differs, change nothing
repokeeper gitlab apply # show it, ask, then apply; --yes skips the questionIt needs a token with the api scope and the Maintainer role on the project: GITLAB_TOKEN in the
environment, or the one the GitLab CLI holds after glab auth login. The host is the one of the
origin remote, so a self-hosted instance works the same way.
repokeeper init --preset strict writes two of these keys for you, and names gitlab apply as the
next step:
gitlab:
merge:
pipeline_must_succeed: true
protect:
push: maintainers
merge: maintainers
allow_force_push: falseOn gitlab.com a new project already protects its default branch this way, so the first apply
often changes the merge setting only; the protect keys then keep the protection from being
loosened unnoticed.
What to know:
protectcovers the default branch only. Keys you leave out take GitLab's own defaults: Maintainers push, Maintainers merge, no force push. A protection that differs is removed and created again, so the branch is unprotected for a moment whileapplyruns. The token of thereleasejob has to be allowed to push: withpush: maintainers, give it the Maintainer role.renovate_schedulecreates one pipeline schedule, described asrepokeeper: renovate, on the default branch. Schedules under any other name are never touched. This is the schedule therenovatejob above waits for.- CI/CD variables are never written.
applyonly tells you whenGITLAB_TOKEN(for a repository with releases) orRENOVATE_TOKEN(with dependency updates) is not set in the project. A variable inherited from a group is not visible to this check, so the note can be wrong there. - No approval rules. The number of approvals a merge request needs is a paid-tier setting on GitLab, and repokeeper does not manage it.
Back to the README.