Skip to content

feat(preview): add Gitea/Forgejo support for preview deployments - #1

Closed
ankit8697 wants to merge 2 commits into
canaryfrom
claude/dokploy-issue-3828-omx8un
Closed

feat(preview): add Gitea/Forgejo support for preview deployments#1
ankit8697 wants to merge 2 commits into
canaryfrom
claude/dokploy-issue-3828-omx8un

Conversation

@ankit8697

@ankit8697 ankit8697 commented Aug 20, 2026

Copy link
Copy Markdown
Owner

What is this PR about?

Preview deployments were wired exclusively to GitHub. pages/api/deploy/github.ts was the only handler that reacted to pull_request events, createPreviewDeployment threw "Github Account not configured correctly" unless application.githubId was set and then called octokit directly, and deployPreviewApplication only cloned when sourceType === "github" — for any other source type it skipped the build entirely yet still posted a ✅ "success" comment.

This PR makes preview deployments work for Gitea and Forgejo repositories. Forgejo's API is Gitea-compatible and Dokploy's existing "Gitea" provider already serves Forgejo users, so one implementation covers both.

Approach. The issue offered two options — extend the deployment webhook to handle pull request events, or add a separate pull request webhook. This takes the first. The existing /api/deploy/{refreshToken} URL is already per-application, so a pull request event resolves to exactly one application with no installation-id lookup; a Gitea webhook delivers all of its selected events to one URL, so a second webhook would be pure friction; and no new column is needed, hence no migration. Users only tick the pull request events on the webhook they already created for push auto-deploy.

Changes

Gitea REST helpers (packages/server/src/utils/providers/gitea.ts)

  • giteaApiRequest, an authenticated fetch wrapper that resolves giteaInternalUrl ?? giteaUrl and refreshes the token first.
  • Issue comment create / update / get / list, and checkGiteaUserRepositoryPermissions.
  • These read the token through findGiteaById, not application.gitea, because findApplicationById redacts accessToken.

Provider-agnostic preview comments (packages/server/src/services/preview-comment.ts, new)

  • getPreviewCommentContext(application) resolves the pull request coordinates for a GitHub or Gitea application, and returns null for source types that cannot host previews.
  • createPreviewComment / updatePreviewComment / ensurePreviewComment / createPreviewSecurityBlockedComment / checkPreviewAuthorPermissions dispatch on the provider. The GitHub branches delegate to the existing services/github.ts functions, so GitHub behaviour is unchanged.

Preview lifecycle (services/preview-deployment.ts, services/application.ts)

  • createPreviewDeployment no longer hard-requires githubId.
  • deployPreviewApplication and rebuildPreviewApplication write their status comment through the dispatch layer, and deployPreviewApplication clones Gitea repositories.
  • Removed createPreviewDeploymentComment, which became unused once the callers moved to ensurePreviewComment — that also drops the github.tspreview-deployment.ts import cycle.

Webhook handling (apps/dokploy/server/utils/gitea-preview.ts, new, wired into pages/api/deploy/[refreshToken].ts)

  • Pull request deliveries are handled before the autoDeploy gate, because previews are a separate feature from push auto-deploy and must work with auto-deploy off.
  • Gitea's action names are mapped onto the existing behaviour: synchronized (not GitHub's synchronize), and label_updated / label_cleared instead of labeled / unlabeled. Collaborator check, previewLabels filter, and the previewLimit cap (still only for new previews, per 98dbc59) all carry over.
  • Matching is an exact comparison on X-Gitea-Event / X-Forgejo-Event. Gitea folds every pull request sub-event into the single name pull_request and keeps the specific one in X-Gitea-Event-Type, so the generic header alone is sufficient — and a prefix match on the event type would be wrong, see the verification notes below. The GitHub compatibility headers Gitea also sends are deliberately ignored so GitHub deliveries keep taking the existing path.

Drive-by fixes in the code being touched

  • deployPreviewApplication now throws for source types it cannot build, instead of reporting success without building.
  • cloneGiteaRepository uses its (previously dead) getErrorCloneRequirements guard and checks the access token, so a half-configured provider gets a clear error instead of git clone .../null/null.git.
  • A failing status comment on the error path can no longer mask the real build error.

Hardening beyond parity with the GitHub handler

The GitHub handler derives its application set from the payload (repository, owner, branch, githubId are all in the where). Here the application comes from the URL, so the payload needs checking:

  • Repository identityrepository.name and the repository owner must match giteaRepository / giteaOwner, case-insensitively. Without this a webhook on any repository could deploy an arbitrary branch of the configured one.
  • Fork pull requests are skipped with an explicit message; cloneGiteaRepository always clones the configured repository, so a fork-only branch would just produce a failing build.
  • closed cleanup is scoped to this application. Gitea pull request ids are per-instance auto-increments — the live instance below issued id 1 for the first PR — so the installation-wide findPreviewDeploymentsByPullRequestId lookup used for GitHub (whose ids are globally unique) would collide across two Gitea instances.
  • Repository owners short-circuit the permission lookup. Gitea only answers /collaborators/{u}/permission for site admins, repository admins, or users asking about themselves — everyone else gets a 403. A 403 is therefore reported as unverified and skips the deployment without posting the "you lack access" comment, since it means the Dokploy-side account lacks repository admin, not that the author is untrusted.
  • owner is allow-listed alongside write and admin (Gitea returns owner as a distinct role and has no maintain level, so the "Required Level" line in the blocked-comment is now per-provider).

Verification

pnpm typecheck and pnpm server:build pass. 47 new tests pass alongside the existing suite — 908 passing overall, with the only 5 failures (application.real.test.ts, env-file-literals.test.ts) reproducing identically on a clean canary checkout because they need real nixpacks/Docker.

Rather than rely on the API docs, I stood up a real Gitea 1.24.3 instance (SQLite, four users: repo owner, write collaborator, read collaborator, outsider), opened a real pull request from the write collaborator, pushed a second commit to it, added a label and cleared the labels, and pointed a repository webhook at a capture server. Verified against that instance:

Check Result
X-Gitea-Event for opened / sync / label add / label clear pull_request in all four cases; specific type in X-Gitea-Event-Type
Action names opened, synchronized, label_updated, label_cleared
Payload fields (pull_request.id/number/title/html_url/user.login/base.ref/head.ref/head.sha/head.repo.owner.login, repository.name/owner.login) all present as assumed
Issue-comment create / get / update / list through the new helpers 201 / 200 / 200 / 200
/collaborators/{u}/permission owner for the repo owner, write, read; 403 when the connected account is not a repo admin
Issue-comment write with an OAuth token issued using Dokploy's current scope string 201 — existing providers need no re-authorization

Two things that testing corrected, both now fixed and covered by fixtures captured from that instance (__test__/deploy/fixtures/gitea-pull-request-deliveries.json):

  1. Comments on a pull request arrive as X-Gitea-Event: issue_comment with X-Gitea-Event-Type: pull_request_comment. Dokploy posts preview status comments itself, so those deliveries come straight back to the same webhook. The exact match on the generic event name rejects them correctly; a prefix match on the event type — which I had considered — would have routed them into the pull request handler.
  2. On a public repository Gitea reports a non-collaborator as read, not 404. Harmless for behaviour (still blocked, with an accurate comment) but a code comment claimed otherwise.

What is still not verified: a full end-to-end preview build and deploy from a running Dokploy instance, which needs Postgres, Redis, Docker Swarm, Traefik and wildcard DNS that I could not stand up here. The Gitea-specific surface this PR changes is covered above; the build/deploy machinery downstream of it is provider-independent and untouched apart from the one new cloneGiteaRepository call, whose emitted command is unit-tested. Please do give it one real end-to-end pass before merging.

Other notes for reviewers

  • No signature verification, deliberately. Gitea sends X-Gitea-Signature and X-Hub-Signature-256 on every delivery (the live instance sent both even with no secret configured), and it is tempting to verify one against the refreshToken. Verifying only when the header is present buys nothing — an attacker holding the token simply omits it — and making it mandatory would break every existing push webhook whose secret is something else. The trust model for this endpoint is unchanged: the refreshToken in the URL is the shared secret, and it is rotatable from the UI. Anyone who needs real HMAC verification wants a provider-level Gitea webhook with a stored secret, which the preview-comment.ts abstraction added here makes cheap to add later.
  • One repository mapped to N applications needs N webhooks, since the URL is per-application. That is already true for push auto-deploy on Gitea.
  • Custom git source type pointing at a Gitea instance stays unsupported — there is no giteaId to authenticate comment writes with. Such a delivery now returns a message saying so rather than a bare "Branch Not Match".
  • openapi.json needs no regeneration (no new tRPC procedures), no audit-log entry is added (webhooks don't audit today for any provider), and the preview components have no t() calls so there are no i18n keys to add.
  • Docs live in Dokploy/website and need a follow-up note that previews support GitHub and Gitea/Forgejo, including which webhook events to enable.

Checklist

Issues related

closes Dokploy#3828

Screenshots

The only UI change is a provider-aware icon on the "Pull Request" link and an info block explaining which Gitea webhook events previews need:

Gitea / Forgejo: preview deployments are driven by the webhook you added for this application (its URL is shown in the Deployments tab). In the repository webhook settings, choose Custom Events and enable Pull Request and Pull Request Synchronized — without the latter, previews are created but never updated when new commits are pushed. Enable Pull Request Label as well if you use the preview labels filter.

That last detail is not cosmetic: Gitea exposes those as separate checkboxes, and enabling only "Pull Request" is the failure mode where previews appear but never refresh.

claude added 2 commits August 20, 2026 18:02
Preview deployments were wired exclusively to GitHub: only
`/api/deploy/github` reacted to `pull_request` events,
`createPreviewDeployment` refused to run without an `application.githubId`
and talked to octokit directly, and `deployPreviewApplication` only cloned
when `sourceType === "github"` - silently reporting success without
building for every other source type.

Gitea/Forgejo repositories now get the same feature, driven by the
per-application webhook that already exists for push auto deployments
(`/api/deploy/{refreshToken}`), so users only have to enable the pull
request events on the webhook they already created.

- Add the Gitea REST helpers preview deployments need: issue comment
  create/update/get/list and the collaborator permission lookup, all
  going through `findGiteaById` because `findApplicationById` redacts the
  access token.
- Introduce `services/preview-comment.ts`, a provider-agnostic layer that
  resolves the pull request coordinates of an application and dispatches
  comment and permission calls to GitHub or Gitea. The GitHub branches
  delegate to the existing functions, so GitHub behaviour is unchanged.
- Teach `createPreviewDeployment`, `deployPreviewApplication` and
  `rebuildPreviewApplication` to use that layer, and clone Gitea
  repositories for previews.
- Handle Gitea/Forgejo `pull_request` deliveries before the `autoDeploy`
  gate, mapping Gitea's action names (`synchronized`, `label_updated`,
  `label_cleared`) onto the existing create/redeploy/remove behaviour and
  preserving the collaborator check, preview labels and preview limit.
- Validate that the payload repository matches the one the application is
  configured for, skip pull requests from forks, and short circuit the
  permission lookup for the repository owner (Gitea only answers that
  endpoint for repository admins).
- Fail explicitly instead of reporting a successful preview deployment for
  source types that cannot build one, and guard the Gitea clone against a
  missing owner, repository, branch or access token.
- Show the icon of the configured provider on the pull request link and
  explain which Gitea webhook events previews need.
Ran the handler against webhook deliveries captured from a live Gitea
1.24.3 instance (pull request opened by a write collaborator, a second
commit pushed to it, a label added, then all labels cleared, plus a
comment on the same pull request) and added those payloads as fixtures.

Two things the live run corrected:

- Gitea sends pull request *comment* deliveries as
  `X-Gitea-Event: issue_comment` with `X-Gitea-Event-Type:
  pull_request_comment`. Dokploy posts preview status comments itself, so
  those deliveries come straight back to the same webhook and must not be
  treated as pull request events - which the exact match on the generic
  event name already does, and a prefix match on the event type would not.
- On a public repository Gitea reports a non-collaborator as `read`, not as
  a 404, so the comment claiming otherwise was wrong.

Also confirmed live: `owner` is returned as a distinct permission for the
repository owner, and the permission endpoint answers 403 when the
connected account is not a repository admin, which is the case the handler
reports as unverified instead of blaming the pull request author.

Copy link
Copy Markdown
Owner Author

Superseded by the upstream pull request: Dokploy#5149

This one existed only to stage the branch and draft the description. The branch (claude/dokploy-issue-3828-omx8un) is unchanged and is the head of the upstream PR, so closing this does not affect it. Review discussion belongs upstream.


Generated by Claude Code

@ankit8697 ankit8697 closed this Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Gitea/Forgejo support for Preview Deployments

2 participants