Skip to main content

Connecting git hosts (GitHub, GitLab...)

QA-Board works with repositories on GitHub (github.com and GitHub Enterprise Server), GitLab (gitlab.com and self-managed), Gitea/Forgejo and Bitbucket Cloud, and can clone from any other git server. The server:

  • receives the repositories' push events, clones them to read commits and qaboard.yaml,
  • links to the code, shows committers' avatars,
  • shows and starts CI jobs: GitHub Actions workflows, GitLab CI manual jobs (see integrations).

Configuration​

List your hosts in QABOARD_GIT_HOSTS, as a JSON list, or as the path to a JSON or YAML file with the list (e.g. a mounted secret):

QABOARD_GIT_HOSTS='[
{"type": "github", "url": "https://github.com", "token": "github_pat_..."},
{"type": "gitlab", "url": "https://gitlab.example.com", "token_env": "GITLAB_ACCESS_TOKEN", "webhook_secret": "...", "hostnames": ["gitlab"]},
{"type": "forgejo", "url": "https://codeberg.org"}
]'
KeyUsage
typegithub, gitlab, gitea (or forgejo), bitbucket, or generic for other git servers (cloning only)
urlThe host's web URL. Repositories are recognized by their hostname
token, or token_envA token, or the name of the environment variable that has it. Only sent to this host. Use user:password if the host needs a user name (e.g. Bitbucket app passwords)
webhook_secret, or webhook_secret_envThe secret its webhooks send. Default: QABOARD_WEBHOOK_SECRET
api_urlSet it only if the host's API is somewhere else than usual. Defaults: https://api.github.com for github.com, <url>/api/v3 for GitHub Enterprise, <url>/api/v4 for GitLab, <url>/api/v1 for Gitea/Forgejo, https://api.bitbucket.org/2.0 for Bitbucket Cloud
hostnamesOther names of the host, e.g. used in ssh remotes or by older projects: ["gitlab", "git.internal"]
nameShown to users
user_avatar_urlUsers' avatars, with {url} and {username}. Defaults: {url}/{username}.png for GitHub, {url}/user/avatar/{username}/-1 for Gitea/Forgejo. For GitLab, only for users who didn't upload an avatar (default: QABOARD_AVATAR_URL)

The server reads tokens and webhook secrets (including the variables named by token_env and webhook_secret_env) from its environment only, not from the CLI's shared secrets file (QA_SECRETS): it may have e.g. a CI token that isn't meant for the server. Other settings can also come from your site package.

The /api/v1/config endpoint tells the web app the type, URL and name of each host, never their tokens or their other hostnames. It's public: the web app needs it to build links to the code, and the hosts' URLs are in the projects' web_url anyway.

If a setting is invalid (e.g. QABOARD_GIT_HOSTS is not valid JSON, its file can't be read, or an entry has an unknown key), the server still starts and migrations still run: it logs the error, ignores what's invalid, and refuses all webhooks (503) until it's fixed, since it can't know which webhook secrets you meant.

Tokens​

HostTo clone private repositoriesFor CI integrations and avatars
GitHubContents: Read (fine-grained token) or repoActions: Read and write to show and start workflows
GitLabread_repositoryread_api to show CI job statuses and committers' avatars (it lists the users), api to play manual jobs. With an admin token, avatars also match committers by email
Gitea/Forgejoread:repository
Bitbucketa repository access token with Read, or user:app_password

Clones are authenticated by a git credential helper given in git's environment, only for the host's URL: tokens are not saved in the clones' .git/config. It needs git 2.31 or newer on the backend (for GIT_CONFIG_COUNT): the backend image has it, check yours if you build your own. git uses it when the host asks for credentials (401), so if the host redirects git to another host, that host doesn't get the token. Redirects on the same host keep working (e.g. http to https, or GitHub's for renamed repositories). Likewise, API calls only send the token (and the GITLAB_AUTH session cookies) to the host they were made to.

Clones are in $QABOARD_DATA_GIT_DIR/<group>/<repo> (default: $QABOARD_DATA_DIR/git, i.e. /var/qaboard/git). Repositories are cloned in a temporary folder next to it, then renamed. The server never deletes a folder it didn't clone: a repository path inside a clone, or that has clones inside it (e.g. group), is refused. A clone that git can't read anymore is cloned again, and only replaced once the new clone succeeded.

Clones made by older versions

Before October 2026, clones were made from URLs with the token in them (https://oauth2:<token>@...), and git saved it in their .git/config. They keep using it to fetch. Remove it once, in the backend container:

cd "${QABOARD_DATA_GIT_DIR:-${QABOARD_DATA_DIR:-/var/qaboard}/git}"
find . -name .git -prune | while read -r dotgit; do
repo="${dotgit%/.git}"
url=$(git -C "$repo" remote get-url origin) || continue
clean=$(printf '%s' "$url" | sed -E 's#://[^/@]+@#://#')
if [ "$url" != "$clean" ]; then git -C "$repo" remote set-url origin "$clean"; fi
done

Then consider rotating the tokens that were saved there.

Backward compatibility​

The variables we used before still work, and are merged with QABOARD_GIT_HOSTS:

  • GITLAB_HOST with GITLAB_ACCESS_TOKEN. Without a scheme (gitlab), we use https://. If it has credentials (https://user:password@gitlab) and there is no GITLAB_ACCESS_TOKEN, they're used to clone. If neither GITLAB_HOST nor QABOARD_GIT_HOSTS is set, it defaults to https://gitlab.com.
  • QABOARD_GITLAB_HOSTS (comma-separated): hostnames like gitlab-alias are other names of GITLAB_HOST (the same server, token and users). URLs like https://gitlab2.example.com are other GitLab servers that get GITLAB_ACCESS_TOKEN. To give them their own token, use QABOARD_GIT_HOSTS.
  • github.com is always known, with GITHUB_ACCESS_TOKEN if it's set: public GitHub repositories can send their push events without any configuration, like before. QABOARD_GITHUB_HOSTS lists GitHub Enterprise hostnames that get the same token. Without a scheme, we use the one of the repositories' web_url (https by default).
  • If QABOARD_GIT_HOSTS lists one of those hosts without a token, it gets the token above.
  • A GitLab and a GitHub host can have the same hostname (e.g. GITLAB_HOST is a GitHub Enterprise server that is also in QABOARD_GITHUB_HOSTS): GitHub repositories use the GitHub one.

The default host, used for projects that don't say where they are hosted, is GITLAB_HOST if it's set (or https://gitlab.com if nothing is configured), else the first host of QABOARD_GIT_HOSTS. Pushes to /webhook/gitlab from hostnames we don't know are attributed to the default host if it's a GitLab (else to the first GitLab host), like before, and so are GitLab projects whose data.git.host is not configured anymore. Pushes from other unknown hosts are refused (403).

Upgrading from older versions​

A database migration (d1a6c0f9e2b3) records each existing project's host in its data.git (hosting_type and host), using the git host settings of the deploy that runs it. GitLab projects whose repository URL matches no configured host are recorded on the default host, whatever its type (GitHub projects on an unknown host are left as they are). If the git host settings are invalid, it records nothing and logs a warning. So, before upgrading:

  • keep GITLAB_HOST set to your GitLab, or list your GitLab in QABOARD_GIT_HOSTS, with the hostnames older projects used (e.g. in ssh remotes, or an old DNS name) in hostnames,
  • make sure the migrations see those settings: with docker compose they run in the backend container. With the Helm chart, the migrations Job gets the chart's config: values, but not its secret or extra volumes: give QABOARD_GIT_HOSTS as JSON in config: rather than as the path to a mounted file.

If projects were recorded on the wrong host, remove what they got, e.g. if your projects are not on gitlab.com:

UPDATE projects SET data = data #- '{git,host}' #- '{git,hosting_type}' WHERE data->'git'->>'host' = 'https://gitlab.com';

The server then finds their host from their web_url, like before the migration, and their next push event records it again.

Webhooks​

Each repository should send its push events to QA-Board (see Project setup):

HostWebhook URLSecret
GitHub/webhook/githubSecret, checked with X-Hub-Signature-256
GitLab/webhook/gitlabSecret token (X-Gitlab-Token)
Gitea, Forgejo/webhook/gitea or /webhook/forgejoSecret (X-Gitea-Signature, X-Forgejo-Signature)
Bitbucket Cloud/webhook/bitbucketSecret (X-Hub-Signature)

Set a secret: without it, anyone who can reach the server can send fake push events. If any host has a secret (or QABOARD_WEBHOOK_SECRET is set), push events for hosts without one are refused: otherwise anyone could send a push event that claims its repository is on such a host, e.g. github.com. Other events (e.g. GitHub's ping) are answered with 200 and ignored.

StatusWhy
400The payload is not a JSON object, or not a push event we can read
401The secret is wrong or missing, or the host has no secret while others have one
403The repository is on a host that isn't configured. We don't look further into the payload
409A project with this repository path is already on another git host (project ids are repository paths, they don't say on which host). To move a repository to another host, delete the project, or update its data.git
413The payload is over 25 MB, the most GitHub sends
503The git hosts settings are invalid, see the server's logs

Push events from all hosts are stored the same way, in the projects' data.git:

{
"hosting_type": "github",
"host": "https://github.com",
"path_with_namespace": "org/repo",
"web_url": "https://github.com/org/repo",
"name": "repo",
"description": "...",
"avatar_url": "https://...",
"default_branch": "main",
"namespace": "org",
"git_http_url": "https://github.com/org/repo.git",
"git_ssh_url": "git@github.com:org/repo.git"
}

Bitbucket projects have no git_http_url and git_ssh_url. GitLab projects keep all the other fields of GitLab's push events. Projects can use those fields in their integrations, e.g. ${git.web_url}. A database migration adds hosting_type and host to existing projects (see Upgrading).

Avatars​

To show the avatars of commit authors quickly, the server learns about them from push events (names, emails, usernames, avatar URLs) and from the CLI (emails). It shares what it learned between workers via redis. For each commit author, it shows:

  1. On GitLab hosts with a token: the avatar of the GitLab user with the same name (users are listed once, and cached for 12h). If they never uploaded one and the host has a user_avatar_url (default: QABOARD_AVATAR_URL), that one instead.
  2. The avatar URL a push event gave for them, from any host (e.g. GitHub only gives the pusher's).
  3. If a push event from the repository's host gave their username, their avatar there: https://github.com/<login>.png on github.com, <url>/<login>.png on GitHub Enterprise, <url>/user/avatar/<username>/-1 on Gitea/Forgejo, or the host's user_avatar_url.
  4. Otherwise the Gravatar of their email.

The web app loads images from github.com, Bitbucket, Codeberg and the configured hosts that aren't GitLab directly. The others (GitLab avatars and badges, Gravatars, other hosts) go through the server's /api/v1/git/proxy. It only fetches images from the configured git hosts (and their user_avatar_url), the host of QABOARD_AVATAR_URL, Gravatar, GitHub avatars and QABOARD_PROXY_HOSTS. It can reach hosts users can't, and can log into GitLab with GITLAB_AUTH ({"hostname": {"user": "...", "password": "...", "type": "ldap_user"}}) to see private avatars and badges.

The CLI​

qa also reads QABOARD_GIT_HOSTS, to set commit statuses and wait for the CI of reference commits (see CI integration). Like its other settings, it reads it, and the variables named by token_env, from its environment, the secrets files (QA_SECRETS, ~/.qaboard/secrets.yaml) or your site package. It only uses type (github or gitlab: it doesn't talk to the API of other hosts), url, api_url, token or token_env, and hostnames (a list of lowercase hostnames).

Like the server, qa accepts a JSON list, the path to a JSON/YAML file with the list, or a list (e.g. in a YAML secrets file or a site package).