Forge Integration¶
Gradient connects to a Git forge through named integrations owned by each organization. An integration is either inbound (the forge pushes events to Gradient) or outbound (Gradient reports build status back). Each project links to at most one inbound and one outbound integration.
Supported forges: Gitea, Forgejo, GitLab, and GitHub (via the GitHub App).
1. Create an integration¶
In the Gradient UI:
- Open your organization → Integrations.
- Click New integration.
- Fill in:
- Name - short slug (
production,gitea-status, ...); must be unique within the organization and kind. - Kind - Inbound (Gradient receives webhooks) or Outbound (Gradient calls the forge API).
- Forge type - Gitea / Forgejo / GitLab / GitHub. For GitHub, also enter the Installation ID (see GitHub App below).
- Name - short slug (
Then, depending on the kind:
- Inbound: Gradient generates an HMAC-SHA256 secret (client-side via
crypto.getRandomValues, displayed once). Copy both the secret and the forge-specific webhook URL - the page shows a URL selector so you can switch between/hooks/gitea/...,/hooks/forgejo/..., and/hooks/gitlab/...from a single inbound row. - Outbound: enter the forge base URL (e.g.
https://gitea.example.com) and an API token with permission to post commit statuses.
Secrets and tokens are stored encrypted with the server's crypt key; the API never returns them again, only a boolean indicating their presence.
GitHub App rows¶
GitHub integrations are created by entering a GitHub App Installation ID
in the New Integration form (select forge_type: github). The server validates
the id against the configured App and creates an inbound and outbound pair
automatically, named github-<account> (e.g. github-acme-corp). Multiple
installations per org are supported - one per GitHub account.
The install webhook also auto-creates these pairs when the App is installed on a GitHub account whose repos match an existing org project.
GitHub rows can be deleted to remove the binding, but cannot be edited
(PATCH returns 400). Reference them from project triggers (inbound) and
project outbound_integration (outbound) like any other integration.
2. Report build status¶
To report build status back to a forge, create a forge_status_report action on the project (see Actions).
When a project is created and its repository URL unambiguously matches one of the organization's integrations, Gradient auto-attaches the wiring: a push trigger for the matching inbound integration and a forge_status_report action for the matching outbound integration (at most one of each). Ambiguous matches are left for manual setup.
3. Configure the forge webhook¶
Gitea / Forgejo¶
Repository or organization webhook → POST → application/json, URL from
Gradient, secret = the integration's secret. Under Trigger On choose
Custom Events (or Send everything) and enable Push, Pull Request,
Issue Comment, Pull Request Comment, Pull Request Review, and
Release. A push-only webhook never delivers PR CI, the /gradient run /
/gradient approve comment commands, or review-based approval.
Both the Forgejo (X-Forgejo-Event / X-Forgejo-Signature) and Gitea
(X-Gitea-Event / X-Gitea-Signature) header families are accepted; signatures
are HMAC-SHA256 over the raw body.
GitLab¶
Project or group webhook → URL from Gradient, Secret token = the
integration's secret. Enable the Push events, Tag push events, Merge
request events, Comments (note events), and Releases events triggers.
A push-only webhook never delivers MR CI or the /gradient run / /gradient
approve comment commands (GitLab emits no review webhook). Gradient compares
the X-Gitlab-Token header against the stored secret.
GitHub App¶
The GitHub integration uses a single GitHub App registered against your Gradient server. There are three roles to consider:
- Server operator - once per Gradient instance, register the App and put its credentials into the server's config. See the GitHub App setup operator doc.
- Organization admin - once the server has the App configured, install the App on the organization's GitHub account.
- GitHub repository owner - installing the App fires the
installationwebhook, which carries the list of granted repositories. Gradient writes agithub_installationrow for every org owning a project whose repository URL resolves to one of those repositories, and seeds thegithub-<account>inbound + outbound integration pair. Matching is purely on the repository URL: the organization name and the Gradient project name need not match GitHub, and the flake shorthand (github:owner/repo) is recognized alongside the https and SSH clone URLs. Multiple installations per org are supported (one per GitHub account). Subsequent push / pull-request deliveries route to the corresponding integration pair, and projects can link to the outbound row to enable status reporting.
Webhook deliveries are signed with the App's webhook secret and verified server-side. Build statuses are reported back via the App's installation token.
Rotating or deleting an integration¶
From the Integrations page:
- Edit - update name/URL, or paste a new secret/token to replace the existing one. Submitting an empty string for a secret/token clears it.
- Delete - removes the row. Any project linked to it has the link cleared
(
ON DELETE SET NULL).
Inbound URL reference¶
All inbound webhook URLs have the form:
where {forge} is gitea, forgejo, or gitlab. GitHub deliveries go
through the App webhook at /api/v1/hooks/github and are not per-integration.
A single inbound integration can serve all three Gitea/Forgejo/GitLab forges
simultaneously - the signature scheme is selected by the {forge} path
segment.
Webhook response body¶
Both POST /api/v1/hooks/{forge}/{org}/{integration_name} and POST /api/v1/hooks/github
return the standard envelope with a WebhookResponse payload describing what happened:
{
"error": false,
"message": {
"event": "push",
"repository_urls": ["https://github.com/acme/widgets.git"],
"projects_scanned": 2,
"queued": [
{
"project_id": "...",
"project_name": "widgets",
"organization": "acme",
"evaluation_id": "..."
}
],
"skipped": [
{
"project_id": "...",
"project_name": "widgets-staging",
"organization": "acme",
"reason": "already_in_progress"
}
]
}
}
The reason field for skipped projects is one of:
already_in_progress- an evaluation for the same revision is already queued or runningno_previous_evaluation- the project has not yet been bootstrappeddb_error- a per-project persistence failure (the request as a whole still succeeded)
Non-push GitHub App events (ping, installation, installation_repositories, unknown)
return the same envelope with event set accordingly and empty queued / skipped arrays.
Source IP restrictions¶
Each inbound integration can carry a CIDR allowlist (allowed_ips). When set,
deliveries whose source IP does not match are rejected with
403 forbidden_source_ip after signature verification succeeds. An empty or
omitted list allows any source.
For the per-forge route (POST /hooks/{forge}/{org}/{integration_name}) the
check applies to the resolved client IP. For the GitHub App route
(POST /hooks/github), the check is applied per-installation: integrations
whose allowlist rejects the source IP are simply skipped, while integrations
whose list matches (or is empty) are dispatched as usual.
Forge IP ranges to allowlist:
- GitHub: published at
https://api.github.com/meta(thehooksarray). - GitLab.com: published at https://docs.gitlab.com/ee/user/gitlab_com/#ip-range.
- Gitea / Forgejo: typically self-hosted; allowlist your own forge's egress IPs.
The source IP is resolved from the connection peer with X-Forwarded-For
honored only when the peer is in GRADIENT_NETWORK_TRUSTED_PROXIES.
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
401 Unauthorized in delivery log |
Secret mismatch - re-copy the secret from Gradient or rotate and reconfigure the forge. |
403 forbidden_source_ip |
The forge's egress IP isn't in the integration's allowed_ips list. Add it or clear the list. |
404 Not Found |
Wrong organization or integration name in the URL, or {forge}=github (use the App webhook). |
200 OK but no evaluation runs |
No project links to this inbound integration, or the repository URL doesn't match any project. |
PR CI or /gradient comments never fire |
The forge webhook is push-only. Enable PR/merge-request, comment/note, and review events (see Configure the forge webhook). |
503 Service Unavailable |
The integration row has no secret set yet - paste or generate one on the Integrations page. |