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"}
]'
| Key | Usage |
|---|---|
type | github, gitlab, gitea (or forgejo), bitbucket, or generic for other git servers (cloning only) |
url | The host's web URL. Repositories are recognized by their hostname |
token, or token_env | A 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_env | The secret its webhooks send. Default: QABOARD_WEBHOOK_SECRET |
api_url | Set 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 |
hostnames | Other names of the host, e.g. used in ssh remotes or by older projects: ["gitlab", "git.internal"] |
name | Shown to users |
user_avatar_url | Users' 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
| Host | To clone private repositories | For CI integrations and avatars |
|---|---|---|
| GitHub | Contents: Read (fine-grained token) or repo | Actions: Read and write to show and start workflows |
| GitLab | read_repository | read_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/Forgejo | read:repository | |
| Bitbucket | a 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.
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_HOSTwithGITLAB_ACCESS_TOKEN. Without a scheme (gitlab), we usehttps://. If it has credentials (https://user:password@gitlab) and there is noGITLAB_ACCESS_TOKEN, they're used to clone. If neitherGITLAB_HOSTnorQABOARD_GIT_HOSTSis set, it defaults to https://gitlab.com.QABOARD_GITLAB_HOSTS(comma-separated): hostnames likegitlab-aliasare other names ofGITLAB_HOST(the same server, token and users). URLs likehttps://gitlab2.example.comare other GitLab servers that getGITLAB_ACCESS_TOKEN. To give them their own token, useQABOARD_GIT_HOSTS.- github.com is always known, with
GITHUB_ACCESS_TOKENif it's set: public GitHub repositories can send their push events without any configuration, like before.QABOARD_GITHUB_HOSTSlists GitHub Enterprise hostnames that get the same token. Without a scheme, we use the one of the repositories'web_url(httpsby default). - If
QABOARD_GIT_HOSTSlists 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_HOSTis a GitHub Enterprise server that is also inQABOARD_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_HOSTset to your GitLab, or list your GitLab inQABOARD_GIT_HOSTS, with the hostnames older projects used (e.g. in ssh remotes, or an old DNS name) inhostnames, - 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: giveQABOARD_GIT_HOSTSas JSON inconfig: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):
| Host | Webhook URL | Secret |
|---|---|---|
| GitHub | /webhook/github | Secret, checked with X-Hub-Signature-256 |
| GitLab | /webhook/gitlab | Secret token (X-Gitlab-Token) |
| Gitea, Forgejo | /webhook/gitea or /webhook/forgejo | Secret (X-Gitea-Signature, X-Forgejo-Signature) |
| Bitbucket Cloud | /webhook/bitbucket | Secret (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.
| Status | Why |
|---|---|
400 | The payload is not a JSON object, or not a push event we can read |
401 | The secret is wrong or missing, or the host has no secret while others have one |
403 | The repository is on a host that isn't configured. We don't look further into the payload |
409 | A 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 |
413 | The payload is over 25 MB, the most GitHub sends |
503 | The 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:
- 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. - The avatar URL a push event gave for them, from any host (e.g. GitHub only gives the pusher's).
- If a push event from the repository's host gave their username, their avatar there:
https://github.com/<login>.pngon github.com,<url>/<login>.pngon GitHub Enterprise,<url>/user/avatar/<username>/-1on Gitea/Forgejo, or the host'suser_avatar_url. - 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).