Other Git hosts
Use the Generic provider when your Git host isn’t GitLab or Bitbucket, for example Gitea or Forgejo. You describe the requests yourself:
- a status template: the URL and JSON body pixmoat sends to set a commit status;
- an optional inbound merge webhook mapping: which webhook deliveries are merges, and where the branch and commit are in their JSON.
For GitLab or Bitbucket, use GitLab CI or Bitbucket instead.
The Generic provider doesn’t comment on pull requests, doesn’t rerun CI, and has no Test connection check. The support matrix lists what’s supported on each Git host.
Before you start
Your CI uploads screenshots with the pixmoat fixture and sets PIXMOAT_BRANCH and PIXMOAT_COMMIT to the branch and commit your Git host knows. pixmoat sends each status to the commit in PIXMOAT_COMMIT. See Connect Playwright and the client environment variables.
pixmoat sends requests only to public internet addresses. A Git host on a private network can’t receive statuses. It also doesn’t follow redirects, so enter the final API URL.
Set up commit statuses
- In pixmoat, open the project’s Settings and the Git Provider tab.
- Set Provider to Generic and select Enable integration.
- Fill in Repository identifier. pixmoat puts it wherever a template says
{repo}, for exampleacme/storefront. - Paste your Git host’s API token into Access Token.
- Fill in Status URL template and Status body template. The Gitea example shows both.
- Under State mapping, enter the status strings your host expects for Approved, Rejected and In Review. The defaults are
success,failureandpending. - Select Save changes. You’ll see “Git provider settings saved.”
Requests created in the form use POST and send the token as Authorization: Bearer <token>. If your host needs another method or another authorization header, set the templates through the API instead. See Custom methods and headers.
Saving checks the templates, but doesn’t contact your Git host or check the token. To test the setup, push a commit and check that the status appears on it.
pixmoat sends a status when a build starts, when it finishes, and when its review is finished:
| When | State map entry |
|---|---|
| The build starts | In Review |
| The build finishes with nothing to review | Approved |
| The build finishes with changes to review | Rejected (Approved if the project is in report-only mode) |
| The review is finished and approved | Approved |
| The review is finished and something was rejected | Rejected |
If the status template is empty, pixmoat sends no statuses.
Placeholders
Templates replace placeholders in curly braces. There are no expressions, loops or conditions.
| Placeholder | Value | Available in |
|---|---|---|
{repo} | The Repository identifier | Status URL and body |
{sha} | The build’s commit SHA | Status URL and body |
{status_state} | The status string from the state map | Status body |
{web_url} | The link to the build in pixmoat | Status body |
{status_context} | The status name, pixmoat/visual/<project-slug> | Status body |
Use {status_context} as the status name. It includes the project slug, so two pixmoat projects on the same repository post two separate statuses.
Saving fails with “unknown placeholder” if a template uses any other name. {pr}, {branch}, {target_branch} and {comment_markdown} are also accepted, but they’re always empty in status templates.
Template rules
- The body must be valid JSON. Put placeholders inside quoted strings, for example
"state": "{status_state}". pixmoat escapes quotes and newlines in the values. - Placeholders can’t be used in JSON keys, or in the scheme, host or port of the URL.
- A URL template can be up to 4,096 bytes, a body template up to 64 KiB, and each state map value 1 to 128 bytes.
Credentials in templates
Two references insert the project’s stored secrets:
| Reference | Value |
|---|---|
${PIXMOAT_GIT_TOKEN} | The Access Token |
${PIXMOAT_GIT_WEBHOOK_SECRET} | The Webhook Secret |
They work in bodies and headers, not in URLs. No other ${...} names are allowed. Don’t paste a token into a template directly: templates aren’t stored as secrets, and project members can see them.
Custom methods and headers
To use a method other than POST, or an authorization header other than Bearer, send the whole configuration to the API as a generic object. You need a personal access token from Account → Personal Access Tokens → Create token, created by a project admin. Project access tokens can’t change settings.
Fragment. For a Gitea or Forgejo host that expects Authorization: token <token>:
curl --fail -X PUT \
-H "Authorization: Bearer $PERSONAL_TOKEN" \
-H "Content-Type: application/json" \
"https://app.pixmoat.com/api/projects/<project-slug>/git-provider" \
-d @- <<'JSON'
{
"kind": "generic",
"repo": "acme/storefront",
"enabled": true,
"generic": {
"status_template": {
"method": "POST",
"url": "https://git.example.com/api/v1/repos/{repo}/statuses/{sha}",
"body": "{\"state\": \"{status_state}\", \"target_url\": \"{web_url}\", \"description\": \"pixmoat visual review\", \"context\": \"{status_context}\"}",
"headers": { "Authorization": "token ${PIXMOAT_GIT_TOKEN}" }
},
"state_map": { "approved": "success", "rejected": "failure", "in_review": "pending" }
}
}
JSON
- A request sends either the
genericobject or the form’s fields, not both. tokenandwebhook_secretfields left out of the request keep their stored values. An empty string clears them. You can also set them on the Git Provider tab.- Saving the form later keeps the method and headers you set through the API.
- Only
Content-Type: application/jsonis allowed as a content type, and headers such asHostandContent-Lengthare rejected.
If the project’s Generic provider was set up before pixmoat started checking templates, it stays inactive until a project admin reviews its settings and selects Save changes again.
Promote baselines on merge
When a pull request is merged, pixmoat can copy its approved baselines to the target branch. This is merge promotion. Without it, the next default-branch build shows the changes you already approved as Changed again, and a project admin can promote a build by hand.
Only send completed merges. pixmoat recognizes a merge by one request header, not by the payload’s action or merged flag. If your host sends the same header for every pull request event, don’t point it at pixmoat directly. Put a relay in between that forwards only completed merges and signs what it forwards.
On the Git Provider tab, under Inbound merge webhook:
-
Event header name and Expected value: the header and value that mark a merge delivery. Deliveries with another value are answered with
"status": "ignored". -
Merge field mapping: a JSON object that says where each field is in the webhook’s JSON, as dot-separated paths without a
$.prefix:Key Required Value target_branchYes The branch that was merged into merged_shaYes The merge commit source_branchYes The merged branch. It must match the PIXMOAT_BRANCHyour CI sets.repo_pathYes The repository path source_head_shaNo The last commit on the merged branch merged_byNo Who merged -
Signature header: the header that carries an HMAC-SHA256 signature of the request body. It must differ from the event header. Signature scheme is
hmac-sha256. -
Webhook Secret: a random string, for example the output of
openssl rand -hex 32. The sender signs with the same string. -
Select Save changes, then copy the Webhook URL. It looks like
https://app.pixmoat.com/api/projects/<project-slug>/webhooks/generic. Configure your sender to deliver to it.
pixmoat accepts the signature as bare hex or as sha256=<hex>. A delivery with a missing or wrong signature gets 401. Unsigned webhooks aren’t supported.
To check it, merge a pull request whose build is Approved. The build shows a Merged badge in pixmoat. The Recent deliveries list at the bottom of the Git Provider tab shows each webhook pixmoat received, with its status.
Example: Gitea and Forgejo
Commit status templates:
-
Status URL template:
https://git.example.com/api/v1/repos/{repo}/statuses/{sha} -
Status body template:
{"state": "{status_state}", "target_url": "{web_url}", "description": "pixmoat visual review", "context": "{status_context}"} -
State mapping:
success,failure,pending.
The form sends the token as Authorization: Bearer <token>. If your server only accepts Authorization: token <token>, use the API request above.
Gitea and Forgejo send X-Gitea-Event: pull_request for every pull request event, including opening and closing. Connect their webhooks to pixmoat only through a relay that forwards completed merges. If the relay forwards Gitea’s payload unchanged, this mapping matches it:
{
"target_branch": "pull_request.base.ref",
"merged_sha": "pull_request.merge_commit_sha",
"source_branch": "pull_request.head.ref",
"repo_path": "repository.full_name",
"source_head_sha": "pull_request.head.sha",
"merged_by": "pull_request.merged_by.login"
}
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| “unknown placeholder” when saving | A template uses a placeholder that isn’t in the list. |
| “unknown environment variable” when saving | A template uses a ${...} name other than PIXMOAT_GIT_TOKEN or PIXMOAT_GIT_WEBHOOK_SECRET. |
| “outbound destination or request is not permitted” | The URL, or an address its host name resolves to, isn’t a public internet address. pixmoat can’t reach Git hosts on private networks. |
| No status appears | The status template is empty, the token is wrong, or the host expects another authorization header. See Custom methods and headers. |
Webhook returns 401 | The signature is missing or doesn’t match. Check that the sender signs the raw body with the Webhook Secret. |
Webhook returns 403 | The repo_path value in the delivery doesn’t match Repository identifier. |
Webhook returns 200 with "status": "ignored" | The event header or its value doesn’t match Event header name and Expected value. |
| Webhook returns an error about missing fields | A path in Merge field mapping doesn’t match the payload. Compare it with a delivery your host sent. |