stableFor developers

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

  1. In pixmoat, open the project’s Settings and the Git Provider tab.
  2. Set Provider to Generic and select Enable integration.
  3. Fill in Repository identifier. pixmoat puts it wherever a template says {repo}, for example acme/storefront.
  4. Paste your Git host’s API token into Access Token.
  5. Fill in Status URL template and Status body template. The Gitea example shows both.
  6. Under State mapping, enter the status strings your host expects for Approved, Rejected and In Review. The defaults are success, failure and pending.
  7. 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:

WhenState map entry
The build startsIn Review
The build finishes with nothing to reviewApproved
The build finishes with changes to reviewRejected (Approved if the project is in report-only mode)
The review is finished and approvedApproved
The review is finished and something was rejectedRejected

If the status template is empty, pixmoat sends no statuses.

Placeholders

Templates replace placeholders in curly braces. There are no expressions, loops or conditions.

PlaceholderValueAvailable in
{repo}The Repository identifierStatus URL and body
{sha}The build’s commit SHAStatus URL and body
{status_state}The status string from the state mapStatus body
{web_url}The link to the build in pixmoatStatus 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:

ReferenceValue
${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 generic object or the form’s fields, not both.
  • token and webhook_secret fields 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/json is allowed as a content type, and headers such as Host and Content-Length are 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:

  1. Event header name and Expected value: the header and value that mark a merge delivery. Deliveries with another value are answered with "status": "ignored".

  2. Merge field mapping: a JSON object that says where each field is in the webhook’s JSON, as dot-separated paths without a $. prefix:

    KeyRequiredValue
    target_branchYesThe branch that was merged into
    merged_shaYesThe merge commit
    source_branchYesThe merged branch. It must match the PIXMOAT_BRANCH your CI sets.
    repo_pathYesThe repository path
    source_head_shaNoThe last commit on the merged branch
    merged_byNoWho merged
  3. 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.

  4. Webhook Secret: a random string, for example the output of openssl rand -hex 32. The sender signs with the same string.

  5. 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

SymptomCause and fix
“unknown placeholder” when savingA template uses a placeholder that isn’t in the list.
“unknown environment variable” when savingA 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 appearsThe status template is empty, the token is wrong, or the host expects another authorization header. See Custom methods and headers.
Webhook returns 401The signature is missing or doesn’t match. Check that the sender signs the raw body with the Webhook Secret.
Webhook returns 403The 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 fieldsA path in Merge field mapping doesn’t match the payload. Compare it with a delivery your host sent.