Universal-git provider
The universal-git provider lets KiCI treat any git forge that speaks a GitHub-shaped webhook payload as a first-class source. That covers Forgejo, Gitea, Gogs, GitLab, plain GitHub (without the App), and any custom webhook-driven forge you can describe in JSONPath.
Want Check runs on pull requests? Use the GitHub App provider instead — it clones via short-lived installation tokens and drives KiCI’s enriched Checks UI out of the box. The universal-git
github-repopreset is the right fallback when you can’t install an App.
The orchestrator:
- receives the forge’s webhook,
- clones the repo via HTTPS (PAT) or SSH (deploy key) to read the lock
file at
.kici/kici.lock.json, - dispatches workflows that match the push / pull_request event.
No mirror, no GitHub App, no checkout: false escape hatch. The same
trigger matching, global-workflow policy, and agent execution pipeline
that back the GitHub App source also serve universal-git sources.
No shared filesystem between orchestrator and agent? Universal-git is the right choice for the remote-agent case — point it at an
http://git server and the agent clones over the network. When the repo instead lives on the agent’s own filesystem (a vendored / operator-curated repo), use a localfile://source and drive it with thekici-adminCLI.
Which preset do I need?
Section titled “Which preset do I need?”KiCI ships canonical presets so you don’t have to spell out JSONPath for every forge:
| Preset | Forge | Webhook header |
|---|---|---|
forgejo | Forgejo | X-Gitea-Event |
gitea | Gitea | X-Gitea-Event |
gogs | Gogs | X-Gogs-Event |
gitlab-repo | GitLab (per-project webhooks) | X-Gitlab-Event |
github-repo | Plain GitHub (per-repo webhook, no App) | X-GitHub-Event |
custom | Anything else | You supply it |
Pick custom only when the forge’s payload structure or event header
deviates from GitHub’s — you’ll then supply payloadPaths and
eventMapping explicitly.
Create a source (PAT)
Section titled “Create a source (PAT)”kici-admin source add generic \ --org <orgId> \ --name forgejo-main \ --verification hmac_sha256 \ --secret <random-hex> \ --preset forgejo \ --git-url-template 'https://forgejo.example.com/{owner}/{name}.git' \ --credential-ref pat \ --credential-type pat \ --credential-user bot-userThen seed the PAT under the source’s own secret scope:
# The scope __source__/<sourceId> is the orchestrator's convention for# source-level credentials. Use the sourceId printed by `source add`.kici-admin secret set <orgId> "__source__/<sourceId>" pat --value "<your-forgejo-pat>"Finally, configure the forge to deliver webhooks to:
https://<platform-host>/webhook/<orgId>/generic/<source-name>with the same secret you passed to --secret.
SSH deploy key
Section titled “SSH deploy key”For SSH instead of HTTPS:
-
Generate an Ed25519 deploy key. Ed25519 is the recommended default.
Terminal window ssh-keygen -t ed25519 -N '' -C 'kici-forgejo-deploy-key' -f ~/.ssh/forgejo-deploy-keyThis produces
~/.ssh/forgejo-deploy-key(private, OpenSSH PEM) and~/.ssh/forgejo-deploy-key.pub(public). -
Register the public key as a deploy key on the forge. On Forgejo / Gitea this is Repository -> Settings -> Deploy Keys -> Add Key (paste the
.pubcontents). On GitLab it’s Settings -> Repository -> Deploy keys. On plain GitHub it’s Settings -> Deploy keys. Read-only access is enough — KiCI only clones. -
Capture the forge’s host keys (needed only for
--ssh-host-key-policy pinned):Terminal window ssh-keyscan -t ed25519,rsa forgejo.example.com > forgejo.known_hostsInspect the file before trusting it (compare against what the forge publishes in its docs) — this is your one chance to pin the key out-of-band rather than trust-on-first-use.
-
Create the source:
Terminal window kici-admin source add generic \--org <orgId> \--name forgejo-ssh \--verification hmac_sha256 \--secret <random-hex> \--preset forgejo \--git-url-template 'ssh://git@forgejo.example.com:22/{owner}/{name}.git' \--credential-ref deploy-key \--credential-type ssh \--ssh-host-key-policy pinned \--ssh-known-hosts-pem "@/path/to/forgejo.known_hosts"The
@prefix on--ssh-known-hosts-pemtells the CLI to read the file contents. -
Store the private key PEM under the source scope:
Terminal window kici-admin secret set <orgId> "__source__/<sourceId>" deploy-key \--value "$(cat ~/.ssh/forgejo-deploy-key)"The orchestrator materialises this PEM into a tempfile (mode
0600) at every clone and drivesgitwith a purpose-builtGIT_SSH_COMMAND(IdentitiesOnly=yes,BatchMode=yes, plus the host-key flags below). The tempdir is cleaned up as soon as the clone finishes.
Host-key policy: accept-new (default) auto-trusts the forge on
first connection (TOFU) and logs a one-time warning. pinned sets
StrictHostKeyChecking=yes with UserKnownHostsFile=<the PEM you supplied> and rejects any host key that doesn’t match — use this for
production supply-chain hardening. pinned requires
--ssh-known-hosts-pem (or the equivalent sshKnownHostsPem field on
update); the CLI rejects the request otherwise.
Updating an existing source: use kici-admin source update-generic <id> with the same flags to switch an HTTPS/PAT source to SSH, rotate
the host-key policy, or flip presets. Pass --clear-git-config to
revert the source back to a payload-only generic webhook.
Credential rotation
Section titled “Credential rotation”To rotate a PAT or SSH key, overwrite the value under the same scope + key and the next clone picks it up:
kici-admin secret set <orgId> "__source__/<sourceId>" pat --value "<new-pat>"The orchestrator re-reads the secret at each clone. No source update needed.
Global workflows
Section titled “Global workflows”Universal-git sources participate in the org-wide global-workflow model exactly like GitHub App sources — a global workflow authored in one source can dispatch against pushes from a different source in the same org (including across forges), with each clone using its own bundle’s credentials.
Enable and tune the policy via the org-settings CLI. Settings are
org-scoped (one row per customer_id); each list entry can optionally
pin to a specific source via --source <routingKey>:
# Enable global workflows for the orgkici-admin org-settings global-workflows set-enabled true \ --customer-id <orgId>
# Allow authors from any source in the orgkici-admin org-settings global-workflows allow-add \ 'forgejo.example.com/ci-workflows/*' \ --customer-id <orgId>
# Allow authors only when the workflow lives on a specific sourcekici-admin org-settings global-workflows allow-add \ 'forgejo.example.com/ci-workflows/*' \ --customer-id <orgId> \ --source "generic:<orgId>:<sourceId>"
# Forbid events from a specific source from firing any global workflowkici-admin org-settings global-workflows deny-add \ 'forgejo.example.com/untrusted/*' \ --customer-id <orgId> \ --source "generic:<orgId>:<sourceId>"See Global workflows for the
policy model (isWorkflowRepoAllowed + isSourceRepoAllowed +
isElevatedAccessAllowed) and the cross-provider dispatch contract.
Routing-key collisions
Section titled “Routing-key collisions”When a user has both a GitHub App source and a universal-git source
targeting the same owner/repo, each creates its own registration and
each fires its own run on a matching push. This is intentional: the two
sources are independently authenticated and may resolve different lock
files. If you want deduplication, either:
- constrain one side via
global_workflow_denied_repos, or - don’t create both sources.
Troubleshooting
Section titled “Troubleshooting”The webhook hits the orchestrator but no run fires. Check the
orchestrator log for Skipping global workflow dispatch or
no registrations for event. Most common cause: the webhook event
header doesn’t match the preset’s eventMapping. For custom sources,
make sure the eventMapping array includes every value the forge
actually sends (they can vary by event type).
Clone fails with 401. The source-scoped secret is missing or wrong. Verify with:
kici-admin secret list <orgId> "__source__/<sourceId>"Clone fails with 403 default branch fetch. The PAT lacks
read-access to the repo or the SSH deploy key isn’t registered on it.
SSH clone fails with host-key rejection. If you set
sshHostKeyPolicy: pinned, verify the known-hosts PEM matches the
forge’s current key. If you’re still using accept-new, the orch’s
~/.ssh/known_hosts has a stale entry — clear it or flip to pinned
with the right PEM.