Skip to content

feat(gitea): add per-project issue synchronization modes - #1899

Closed
gabrielrinaldi wants to merge 4 commits into
usekaneo:mainfrom
gabrielrinaldi:feat/gitea-issue-sync-modes
Closed

gabrielrinaldi wants to merge 4 commits into
usekaneo:mainfrom
gabrielrinaldi:feat/gitea-issue-sync-modes

Conversation

@gabrielrinaldi

@gabrielrinaldi gabrielrinaldi commented Oct 2, 2026 •

Copy link
Copy Markdown

What changed?

This is a proposal PR. I recently installed Kaneo and I love it. Also, kudos for having Gitea support!

I would like more control over issue synchronization with Gitea, since I don't want every Kaneo task duplicated as a Gitea issue or to maintain the source of truth in two places.

This adds three issue synchronization modes:

  • Two-way sync: synchronize issues between Kaneo and Gitea.
  • Ingest only: bring Gitea issues and updates into Kaneo without
    writing issue changes back to Gitea.
  • Off: disable issue synchronization while keeping the repository
    connection.

Branch, push, and pull-request linking and workflow automation remain available in all three modes. Changing modes preserves existing tasks and links and doesn't automatically export existing tasks.

Related issue

None.

How did you check it?

I used AI to help with the implementation, since TypeScript isn't a language I work with every day. I have reviewed the diff and tested the changes.

Verification included:

  • 1,637 unit tests and 1,157 PostgreSQL integration tests passed. Integration tests ran with TZ=UTC to match the test database.
  • Typechecks, lint, locale consistency, and generated OpenAPI checks passed.
  • Local browser testing against disposable Gitea and PostgreSQL instances covered all three modes, branch/PR automation, switching modes, and credential updates without accidentally changing the selected mode.

Non-English locales retain English placeholders, following the contribution guidelines.

Fixes #1985

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

PR Summary by Qodo

Add per-project Gitea issue synchronization modes

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add two-way, ingest-only, and off modes while retaining branch and pull-request automation.
• Enforce mode transitions across writes, webhooks, imports, queued repairs, and sync rules.
• Expose the setting in project settings and document and test its behavior.
Diagram

graph TD
  Settings["Project settings"] --> API["Gitea API"] --> Config["Saved mode"] --> Gate{"Issue operation?"}
  Gate -->|Outbound allowed| Fence["Write fence"] --> Gitea["Gitea repository"]
  Gitea --> Webhook["Signed webhooks"] --> Gate
  Gate -->|Inbound allowed| Tasks["Kaneo tasks"]
  Gate -->|Off| Config
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Distributed write lease instead of transaction-held fence
  • ➕ Avoids holding a database transaction during provider HTTP requests.
  • ➖ Requires a reliable cross-instance lease and fencing protocol to prevent writes after mode changes.
  • ➖ Introduces lease-expiration and failure cases into a race-sensitive path.

Recommendation: The transaction-held fence is defensible for guaranteeing that a saved read-only mode admits no subsequent issue write. Its four-write admission limit and overall request deadline bound pool use; reviewers should examine those operational limits and cross-instance races.

Files changed (99) +5673 / -825

Enhancement (30) +932 / -189
create-gitea-integration.tsPreserve and verify mode when connecting Gitea +95/-49

Preserve and verify mode when connecting Gitea

• Accepts a mode, preserves saved configuration on reconnect, verifies permissions for the target mode, and retires writes when switching away from two-way sync.

apps/api/src/gitea-integration/controllers/create-gitea-integration.ts

get-gitea-integration.tsReturn the effective issue mode +2/-0

Return the effective issue mode

• Includes the mode in integration responses, treating legacy configurations as two-way sync.

apps/api/src/gitea-integration/controllers/get-gitea-integration.ts

import-gitea-issues.tsReject manual imports in off mode +10/-1

Reject manual imports in off mode

• Prevents issue import when the project's issue synchronization is off.

apps/api/src/gitea-integration/controllers/import-gitea-issues.ts

resolve-verification-context.tsResolve credentials and mode for verification +85/-0

Resolve credentials and mode for verification

• Uses explicit values or compatible saved credentials and mode without forwarding a stored token to a changed destination.

apps/api/src/gitea-integration/controllers/resolve-verification-context.ts

update-gitea-integration.tsSave mode changes with transition safeguards +125/-0

Save mode changes with transition safeguards

• Checks write permission before enabling two-way sync, detects concurrent settings changes, retires pending writes, and reconciles existing links.

apps/api/src/gitea-integration/controllers/update-gitea-integration.ts

verify-gitea-access.tsMake permission checks mode-aware +12/-4

Make permission checks mode-aware

• Requires issue-write access for two-way sync but not for ingest-only or off.

apps/api/src/gitea-integration/controllers/verify-gitea-access.ts

index.tsWire mode-aware Gitea routes +22/-73

Wire mode-aware Gitea routes

• Passes modes through verification and connection routes, delegates settings updates, and limits reconnect reconciliation to existing links.

apps/api/src/gitea-integration/index.ts

response.tsExpose mode in the response contract +2/-0

Expose mode in the response contract

• Adds the issue mode to the Gitea integration response schema.

apps/api/src/gitea-integration/response.ts

schema.tsValidate mode in Gitea requests +4/-0

Validate mode in Gitea requests

• Allows the three defined modes in verification, creation, and update bodies.

apps/api/src/gitea-integration/schema.ts

preview-rules.tsSuppress export counts outside two-way sync +10/-1

Suppress export counts outside two-way sync

• Reports zero prospective Gitea task exports when the mode cannot write issues.

apps/api/src/integration-sync/controllers/preview-rules.ts

config.tsDefine issue modes and legacy default +17/-0

Define issue modes and legacy default

• Adds the three-mode schema and helpers; configurations without a stored mode continue to mean two-way sync.

apps/api/src/plugins/gitea/config.ts

task-comment-created.tsGate outbound task comments +2/-2

Gate outbound task comments

• Stops task comments from being sent to Gitea outside two-way issue sync.

apps/api/src/plugins/gitea/events/task-comment-created.ts

task-description-changed.tsGate description updates +15/-15

Gate description updates

• Skips outbound description changes outside two-way mode and uses the shared write-result contract.

apps/api/src/plugins/gitea/events/task-description-changed.ts

task-priority-changed.tsGate priority-label changes +2/-2

Gate priority-label changes

• Prevents task priority changes from writing Gitea labels outside two-way sync.

apps/api/src/plugins/gitea/events/task-priority-changed.ts

task-title-changed.tsGate title updates +15/-15

Gate title updates

• Skips outbound title changes outside two-way mode and uses the shared write-result contract.

apps/api/src/plugins/gitea/events/task-title-changed.ts

integration-lookup.tsFilter off-mode issue recipients +6/-2

Filter off-mode issue recipients

• Excludes off-mode integrations from issue-event lookups without excluding other repository automation.

apps/api/src/plugins/gitea/services/integration-lookup.ts

outbound-fence.tsSerialize issue writes with settings saves +232/-0

Serialize issue writes with settings saves

• Bounds concurrent Gitea mutations and rechecks active configuration, links, rules, tasks, and pending intents before dispatch.

apps/api/src/plugins/gitea/services/outbound-fence.ts

retire-issue-edits.tsRetire writes during mode transitions +70/-0

Retire writes during mode transitions

• Cancels pending outbound intents and queued repairs while preserving eligible inbound deferred fields for ingest-only mode.

apps/api/src/plugins/gitea/services/retire-issue-edits.ts

sync-label-to-gitea.tsRespect mode for direct label sync +3/-3

Respect mode for direct label sync

• Requires an active integration in two-way mode before syncing task labels to Gitea.

apps/api/src/plugins/gitea/utils/sync-label-to-gitea.ts

webhook-handler.tsIgnore off-mode issue webhooks +9/-1

Ignore off-mode issue webhooks

• Acknowledges issue-related deliveries without processing them when mode is off; branch and pull-request events remain available.

apps/api/src/plugins/gitea/webhook-handler.ts

issue-closed.tsScope closed-issue lookups +1/-0

Scope closed-issue lookups

• Marks repository lookup as issue-related so off-mode connections are excluded.

apps/api/src/plugins/gitea/webhooks/issue-closed.ts

issue-comment-created.tsScope issue-comment lookups +1/-0

Scope issue-comment lookups

• Excludes off-mode connections from inbound issue-comment handling.

apps/api/src/plugins/gitea/webhooks/issue-comment-created.ts

issue-edited.tsScope edited-issue lookups +1/-0

Scope edited-issue lookups

• Excludes off-mode connections from inbound issue edits.

apps/api/src/plugins/gitea/webhooks/issue-edited.ts

issue-labeled.tsScope issue-label lookups +1/-0

Scope issue-label lookups

• Excludes off-mode connections from inbound issue-label handling.

apps/api/src/plugins/gitea/webhooks/issue-labeled.ts

issue-opened.tsIngest new issues without read-only backlinks +22/-8

Ingest new issues without read-only backlinks

• Creates eligible tasks in ingest-only mode but skips outbound labels and task-link comments; handles refused label writes.

apps/api/src/plugins/gitea/webhooks/issue-opened.ts

issue-reopened.tsScope reopened-issue lookups +1/-0

Scope reopened-issue lookups

• Excludes off-mode connections from inbound issue reopen events.

apps/api/src/plugins/gitea/webhooks/issue-reopened.ts

label-created.tsScope created-label lookups +1/-0

Scope created-label lookups

• Excludes off-mode connections from inbound issue-label creation.

apps/api/src/plugins/gitea/webhooks/label-created.ts

reconcile.tsReconcile existing issue links without exporting +17/-1

Reconcile existing issue links without exporting

• Adds an existing-links-only filter for project reconciliation, including a Gitea-specific option.

apps/api/src/plugins/sync/reconcile.ts

gitea-integration-settings.tsxAdd project issue-mode selector +148/-12

Add project issue-mode selector

• Provides mode descriptions, saves changes on connected projects, includes mode in verification, and disables unavailable imports and backlinks.

apps/web/src/components/project/gitea-integration-settings.tsx

create-gitea-integration.tsPass mode when creating a connection +1/-0

Pass mode when creating a connection

• Extends the browser request type to include the selected issue mode.

apps/web/src/fetchers/gitea-integration/create-gitea-integration.ts

Bug fix (13) +358 / -184
lock-resume-scope.tsExpose resume binding to the caller +1/-0

Expose resume binding to the caller

• Returns the locked integration binding so resume can enforce the current Gitea mode.

apps/api/src/integration-sync/controllers/lock-resume-scope.ts

resume-sync.tsEnforce mode during paused-link resume +37/-3

Enforce mode during paused-link resume

• Rejects disallowed resume choices and fences Gitea writes after releasing initial task and link locks.

apps/api/src/integration-sync/controllers/resume-sync.ts

task-created.tsFence Gitea issue creation +15/-7

Fence Gitea issue creation

• Creates issues only in two-way mode and checks current integration and task scope before sending.

apps/api/src/plugins/gitea/events/task-created.ts

task-status-changed.tsGate state and status-label writes +15/-4

Gate state and status-label writes

• Restricts outbound status synchronization to two-way mode and distinguishes refused label writes from provider failures.

apps/api/src/plugins/gitea/events/task-status-changed.ts

gitea-api.tsBound the entire Gitea request deadline +87/-82

Bound the entire Gitea request deadline

• Applies one timeout across destination checks, transport, and response reading so delayed requests cannot start after a write fence expires.

apps/api/src/plugins/gitea/utils/gitea-api.ts

labels.tsFence and batch label mutations +30/-15

Fence and batch label mutations

• Skips label writes outside two-way mode, dispatches bounded batches separately, and propagates scope refusals.

apps/api/src/plugins/gitea/utils/labels.ts

deferred-issue-edits.tsMake deferred replay mode-aware +72/-39

Make deferred replay mode-aware

• In ingest-only mode, applies queued inbound fields without scheduling outbound repairs; off mode discards issue work.

apps/api/src/plugins/github/services/deferred-issue-edits.ts

link-manager.tsRequire live intent for receipt updates +10/-1

Require live intent for receipt updates

• Prevents a retired outbound intent from being restored by a late completion or repair.

apps/api/src/plugins/github/services/link-manager.ts

sync-latest-task-value.tsCorrelate dispatched writes with live intents +41/-21

Correlate dispatched writes with live intents

• Uses explicit sent results, checks Gitea mode, and retires refused intents instead of recording successful writes.

apps/api/src/plugins/github/services/sync-latest-task-value.ts

registry.tsLimit automatic Gitea reconciliation +12/-7

Limit automatic Gitea reconciliation

• Propagates existing-links-only mode-change events and avoids exporting unlinked Gitea tasks on workspace label changes.

apps/api/src/plugins/registry.ts

dispatch-issue-write.tsRoute Gitea mutations through the fence +31/-1

Route Gitea mutations through the fence

• Checks current Gitea write eligibility and pending intents before provider dispatch while retaining the existing GitHub path.

apps/api/src/plugins/sync/dispatch-issue-write.ts

initialize-task-text.tsHandle refused initial text writes +4/-2

Handle refused initial text writes

• Uses shared write results and stops initialization when a scope change refuses a write.

apps/api/src/plugins/sync/initialize-task-text.ts

sync-task-field-labels.tsPreserve label-write scope refusals +3/-2

Preserve label-write scope refusals

• Propagates the distinct refusal error so callers can stop stale label synchronization.

apps/api/src/plugins/sync/sync-task-field-labels.ts

Refactor (4) +4 / -4
task-description-changed.tsAdapt GitHub description writer +1/-1

Adapt GitHub description writer

• Returns the shared sent-and-timestamp result expected by task-value synchronization.

apps/api/src/plugins/github/events/task-description-changed.ts

task-status-changed.tsAdapt GitHub state writer +1/-1

Adapt GitHub state writer

• Returns the shared sent-and-timestamp result expected by task-value synchronization.

apps/api/src/plugins/github/events/task-status-changed.ts

task-title-changed.tsAdapt GitHub title writer +1/-1

Adapt GitHub title writer

• Returns the shared sent-and-timestamp result expected by task-value synchronization.

apps/api/src/plugins/github/events/task-title-changed.ts

initialize-task-state.tsAdapt initial state synchronization +1/-1

Adapt initial state synchronization

• Returns the shared write-result shape for initial provider state writes.

apps/api/src/plugins/sync/initialize-task-state.ts

Tests (28) +3962 / -419
gitea-integration-settings.test.tsxTest issue-mode settings interactions +424/-39

Test issue-mode settings interactions

• Covers selection, immediate saves, verification invalidation, permissions, disabled controls, and credential updates.

apps/web/src/components/project/gitea-integration-settings.test.tsx

deferred-issue-edits.test.tsTest deferred delivery across mode transitions +308/-19

Test deferred delivery across mode transitions

• Exercises queued inbound fields, old outbound echoes, and repair behavior when Gitea switches to ingest-only.

tests/api-integration/deferred-issue-edits.test.ts

gitea-issue-sync-modes.test.tsCover Gitea modes end to end +1209/-0

Cover Gitea modes end to end

• Tests inbound and outbound behavior, legacy defaults, off-mode automation, imports, permissions, reconnects, and stale saves.

tests/api-integration/gitea-issue-sync-modes.test.ts

gitea-outbound-fence.test.tsTest concurrent write fencing +786/-0

Test concurrent write fencing

• Covers writes racing mode saves, label batches, pool admission, lock release, and retired intents.

tests/api-integration/gitea-outbound-fence.test.ts

gitea-task-numbering.test.tsAdjust Gitea task-numbering coverage +19/-15

Adjust Gitea task-numbering coverage

• Adapts integration assertions and setup to mode-aware connection behavior.

tests/api-integration/gitea-task-numbering.test.ts

gitea-verification.test.tsTest mode-aware access verification +40/-6

Test mode-aware access verification

• Covers stored credentials and permissions when verifying Gitea connections.

tests/api-integration/gitea-verification.test.ts

inbound-state-echo.test.tsAdapt inbound state-echo coverage +7/-3

Adapt inbound state-echo coverage

• Updates expectations for the changed write-result and mode-aware synchronization paths.

tests/api-integration/inbound-state-echo.test.ts

integration-label-dispatch.test.tsAdapt label-dispatch coverage +15/-11

Adapt label-dispatch coverage

• Checks label dispatch against revised Gitea write admission behavior.

tests/api-integration/integration-label-dispatch.test.ts

integration-sync-field-labels.test.tsTest fenced field-label synchronization +45/-28

Test fenced field-label synchronization

• Adjusts field-label assertions for scoped Gitea writes and refusal handling.

tests/api-integration/integration-sync-field-labels.test.ts

integration-sync-imported-link.test.tsAdapt imported-link sync coverage +10/-6

Adapt imported-link sync coverage

• Updates imported-link expectations for mode-aware issue synchronization.

tests/api-integration/integration-sync-imported-link.test.ts

integration-sync-issue-opened-writes.test.tsTest issue-opened outbound safeguards +21/-6

Test issue-opened outbound safeguards

• Updates checks for label and backlink writes during inbound issue creation.

tests/api-integration/integration-sync-issue-opened-writes.test.ts

integration-sync-project-moved.test.tsAdapt moved-project sync tests +4/-5

Adapt moved-project sync tests

• Updates assertions for revised integration synchronization behavior.

tests/api-integration/integration-sync-project-moved.test.ts

integration-sync-providers.test.tsTest provider dispatch and concurrency +328/-113

Test provider dispatch and concurrency

• Expands coverage of Gitea write locks, provider failures, task responsiveness, and creation races.

tests/api-integration/integration-sync-providers.test.ts

integration-sync-reconnect.test.tsTest reconnect reconciliation +9/-5

Test reconnect reconciliation

• Updates reconnect coverage for saved modes and existing-link-only reconciliation.

tests/api-integration/integration-sync-reconnect.test.ts

integration-sync-rules.test.tsTest mode-aware rules and resume races +293/-15

Test mode-aware rules and resume races

• Covers scope conflicts during resume, provider failures, and rule behavior under concurrent settings changes.

tests/api-integration/integration-sync-rules.test.ts

integration-sync-settings.test.tsTest mode-aware settings and previews +37/-11

Test mode-aware settings and previews

• Updates assertions for mode-dependent reconciliation and export previews.

tests/api-integration/integration-sync-settings.test.ts

integration-sync-task-moved.test.tsAdapt moved-task sync tests +7/-3

Adapt moved-task sync tests

• Adjusts moved-task expectations to the updated provider synchronization contract.

tests/api-integration/integration-sync-task-moved.test.ts

outbound-intent-correlation.test.tsTest late outbound intent correlation +62/-20

Test late outbound intent correlation

• Expands assertions around completion receipts and canceled intents when write scope changes.

tests/api-integration/outbound-intent-correlation.test.ts

pull-request-task-links.test.tsPreserve pull-request link coverage +9/-5

Preserve pull-request link coverage

• Adapts integration assertions to confirm repository linking remains functional.

tests/api-integration/pull-request-task-links.test.ts

create-gitea-integration.test.tsUpdate connection controller tests +20/-27

Update connection controller tests

• Adjusts creation and credential-reuse expectations for combined verification context.

tests/api/gitea-integration/create-gitea-integration.test.ts

verify-gitea-access.test.tsUpdate access verification tests +11/-7

Update access verification tests

• Tests mode-sensitive permission results alongside repository verification.

tests/api/gitea-integration/verify-gitea-access.test.ts

gitea-api-timeout.test.tsTest bounded Gitea requests +115/-0

Test bounded Gitea requests

• Checks DNS delays, uncooperative transports, response deadlines, and caller cancellation.

tests/api/plugins/gitea-api-timeout.test.ts

comment-sync.test.tsAdapt Gitea comment-sync tests +30/-18

Adapt Gitea comment-sync tests

• Updates comment dispatch expectations for mode-gated issue writes.

tests/api/plugins/gitea/comment-sync.test.ts

rapid-text-sync.test.tsAdapt rapid Gitea text-sync tests +19/-11

Adapt rapid Gitea text-sync tests

• Updates rapid-edit coverage for correlated, mode-aware outbound writes.

tests/api/plugins/gitea/rapid-text-sync.test.ts

issue-opened.test.tsTest issue-opened mode handling +16/-6

Test issue-opened mode handling

• Updates webhook tests for conditional label and backlink writes.

tests/api/plugins/gitea/webhooks/issue-opened.test.ts

outbound-intent.test.tsTest shared outbound-intent changes +59/-5

Test shared outbound-intent changes

• Covers cancellation and receipt behavior after scoped dispatch is refused.

tests/api/plugins/github/outbound-intent.test.ts

rapid-state-sync.test.tsAdapt rapid GitHub state-sync tests +34/-18

Adapt rapid GitHub state-sync tests

• Updates state synchronization tests for the shared sent-result contract.

tests/api/plugins/github/rapid-state-sync.test.ts

rapid-text-sync.test.tsAdapt rapid GitHub text-sync tests +25/-17

Adapt rapid GitHub text-sync tests

• Updates text synchronization tests for correlated writes and the shared result contract.

tests/api/plugins/github/rapid-text-sync.test.ts

Documentation (3) +108 / -9
setup.mdxDocument issue modes and transitions +50/-6

Document issue modes and transitions

• Explains permissions, imports, rules, queued work, write fencing, resume choices, and unaffected repository automation.

apps/docs/core/integrations/gitea/setup.mdx

openapi.jsonPublish mode-aware Gitea API contracts +45/-2

Publish mode-aware Gitea API contracts

• Regenerates request, response, and error documentation for Gitea integration routes.

apps/docs/openapi.json

en-US.jsonDefine English issue-mode copy +13/-1

Define English issue-mode copy

• Adds mode names, behavior hints, transition guidance, and mode-dependent token guidance.

i18n/en-US.json

Other (21) +309 / -20
az-AZ.jsonAdd Azerbaijani mode message keys +13/-1

Add Azerbaijani mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/az-AZ.json

de-DE.jsonAdd German mode message keys +13/-1

Add German mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/de-DE.json

el-GR.jsonAdd Greek mode message keys +13/-1

Add Greek mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/el-GR.json

es-ES.jsonAdd Spanish mode message keys +13/-1

Add Spanish mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/es-ES.json

fr-FR.jsonAdd French mode message keys +13/-1

Add French mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/fr-FR.json

hi-IN.jsonAdd Hindi mode message keys +13/-1

Add Hindi mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/hi-IN.json

id-ID.jsonAdd Indonesian mode message keys +13/-1

Add Indonesian mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/id-ID.json

it-IT.jsonAdd Italian mode message keys +13/-1

Add Italian mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/it-IT.json

ja-JP.jsonAdd Japanese mode message keys +13/-1

Add Japanese mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/ja-JP.json

ko-KR.jsonAdd Korean mode message keys +13/-1

Add Korean mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/ko-KR.json

mk-MK.jsonAdd Macedonian mode message keys +13/-1

Add Macedonian mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/mk-MK.json

nl-NL.jsonAdd Dutch mode message keys +13/-1

Add Dutch mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/nl-NL.json

pl-PL.jsonAdd Polish mode message keys +13/-1

Add Polish mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/pl-PL.json

pt-BR.jsonAdd Brazilian Portuguese mode message keys +13/-1

Add Brazilian Portuguese mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/pt-BR.json

ru-RU.jsonAdd Russian mode message keys +13/-1

Add Russian mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/ru-RU.json

schema.jsonDescribe issue-mode translation keys +49/-0

Describe issue-mode translation keys

• Extends the locale schema for mode labels, explanations, and availability hints.

i18n/schema.json

tr-TR.jsonAdd Turkish mode message keys +13/-1

Add Turkish mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/tr-TR.json

uk-UA.jsonAdd Ukrainian mode message keys +13/-1

Add Ukrainian mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/uk-UA.json

vi-VN.jsonAdd Vietnamese mode message keys +13/-1

Add Vietnamese mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/vi-VN.json

zh-CN.jsonAdd Simplified Chinese mode message keys +13/-1

Add Simplified Chinese mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/zh-CN.json

zh-TW.jsonAdd Traditional Chinese mode message keys +13/-1

Add Traditional Chinese mode message keys

• Adds issue-mode labels and guidance with English placeholders.

i18n/zh-TW.json

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Mode changes live inside the API handler ✓ Resolved
Description
The Gitea update handler now saves the mode and retires queued issue edits directly inside its route
callback instead of delegating that behavior to a controller. A settings change therefore requires
the route to coordinate database transactions and issue-edit lifecycle rules alongside request
handling.
Code

apps/api/src/gitea-integration/index.ts[R348-349]

+      if (getGiteaIssueSyncMode(config) !== "sync") {
+        await retireGiteaIssueEdits(row.id, tx, getGiteaIssueSyncMode(config));
Evidence
Rule 6 calls for thin route handlers with controller-owned domain behavior. The changed route
performs the transaction and invokes issue-edit retirement itself.

AGENTS.md: Follow API Route and Controller Conventions: AGENTS.md: Follow API Route and Controller Conventions: AGENTS.md: Follow API Route and Controller Conventions: AGENTS.md: Follow API Route and Controller Conventions
apps/api/src/gitea-integration/index.ts[320-350]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Gitea update route performs mode persistence and queued-edit retirement instead of delegating domain behavior to a controller.
## Fix Focus Areas
- apps/api/src/gitea-integration/index.ts[320-350]
## Recommended Fix
Move the configuration update and edit-retirement transaction into a Gitea integration controller. Keep the route responsible for validated input, delegation, and the response.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Verification crashes on invalid settings ✓ Resolved
Description
The verify route parses saved.config without guarding against malformed JSON when issueSyncMode
is omitted. If the caller supplies an access token, resolveVerificationToken returns before its
guarded parse, so malformed stored configuration throws an unhandled 500 instead of the controlled
400 reconnect error.
Code

apps/api/src/gitea-integration/index.ts[R249-251]

+      issueSyncMode = saved
+        ? getGiteaIssueSyncMode(JSON.parse(saved.config) as GiteaConfig)
+        : "sync";
Evidence
The verify route’s new JSON.parse(saved.config) has no error handler. resolveVerificationToken
returns early for a supplied token; when it does parse saved configuration, it catches malformed
JSON and returns a 400 reconnect error. A supplied token therefore bypasses that protection and
reaches the unguarded route parse.

AGENTS.md: Follow API Route and Controller Conventions: AGENTS.md: Follow API Route and Controller Conventions: AGENTS.md: Follow API Route and Controller Conventions: AGENTS.md: Follow API Route and Controller Conventions
apps/api/src/gitea-integration/index.ts[238-257]
apps/api/src/gitea-integration/controllers/resolve-verification-token.ts[19-33]
apps/api/src/gitea-integration/controllers/resolve-verification-token.ts[21-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The verify route parses saved configuration without handling malformed JSON, particularly when a caller supplies an access token and omits `issueSyncMode`.
## Fix Focus Areas
- apps/api/src/gitea-integration/index.ts[241-252]
- apps/api/src/gitea-integration/controllers/resolve-verification-token.ts[25-33]
## Recommended Fix
Keep mode resolution in the controller so the route does not need to parse stored configuration. Handle malformed JSON as an expected configuration error, consistent with saved-token resolution: throw `HTTPException(400, "Invalid saved Gitea configuration. Reconnect the integration.")` rather than allowing an unhandled parse error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Mode changes can precede an old repair ✓ Resolved
Description
retireGiteaIssueEdits cancels queued jobs and outbound intents, but a repair already in
syncLatestTaskValue checks its binding separately from its provider write. If the mode update
commits between that final check and write(value), the repair sends an issue change after
ingest-only or off has been saved.
Code

apps/api/src/gitea-integration/index.ts[R348-349]

+      if (getGiteaIssueSyncMode(config) !== "sync") {
+        await retireGiteaIssueEdits(row.id, tx, getGiteaIssueSyncMode(config));
Evidence
Replay starts repairs outside its guarded transaction. The outbound helper checks the binding, then
calls the provider writer as a separate operation; the new mode-update transaction can commit
between those steps, while retirement only changes stored job and intent metadata.

apps/api/src/plugins/github/services/deferred-issue-edits.ts[363-380]
apps/api/src/plugins/github/services/sync-latest-task-value.ts[76-91]
apps/api/src/gitea-integration/index.ts[331-350]
apps/api/src/plugins/gitea/services/retire-issue-edits.ts[48-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Retiring queued edits does not prevent a previously started repair from writing after a mode switch commits.
## Fix Focus Areas
- apps/api/src/gitea-integration/index.ts[331-350]
- apps/api/src/plugins/gitea/services/retire-issue-edits.ts[10-16]
- apps/api/src/plugins/github/services/sync-latest-task-value.ts[76-91]
## Recommended Fix
Coordinate outbound provider writes with mode transitions so a write cannot begin under an obsolete mode after the transition commits; cover the interval between the final binding check and the provider call.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Read-only settings saves scan every issue ✓ Resolved
Description
The Gitea update route calls retireGiteaIssueEdits whenever the resulting mode is not sync, even
when the request leaves that mode unchanged. Saving an unrelated setting on a project with many
linked issues therefore scans every issue link inside the settings transaction and may lock links
with pending jobs or intents.
Code

apps/api/src/gitea-integration/index.ts[R348-349]

+      if (getGiteaIssueSyncMode(config) !== "sync") {
+        await retireGiteaIssueEdits(row.id, tx, getGiteaIssueSyncMode(config));
Evidence
Both existing-connection save paths select retirement from the resulting mode alone. Retirement
fetches every issue link and processes its metadata, with updates for links carrying deferred jobs
or pending intents.

apps/api/src/gitea-integration/index.ts[320-350]
apps/api/src/gitea-integration/controllers/create-gitea-integration.ts[141-172]
apps/api/src/plugins/gitea/services/retire-issue-edits.ts[17-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Unchanged ingest-only and off settings saves unnecessarily traverse every linked issue.
## Fix Focus Areas
- apps/api/src/gitea-integration/index.ts[320-350]
- apps/api/src/gitea-integration/controllers/create-gitea-integration.ts[141-172]
- apps/api/src/plugins/gitea/services/retire-issue-edits.ts[17-23]
## Recommended Fix
Compare the previously stored mode with the requested mode and invoke retirement only when a transition away from sync requires it; apply the same check to existing-connection saves.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Switching to two-way sync skips the permission check ✓ Resolved
Description
The Gitea integration PATCH handler saves issueSyncMode: "sync" after schema-only validation,
without checking whether the saved token has the repository write permissions required for sync. A
project connected with a read-only token in ingest-only mode can switch modes through the settings
UI, leaving subsequent outbound issue, comment, and label writes to fail at Gitea.
Code

apps/api/src/gitea-integration/index.ts[R320-322]

+    if (body.issueSyncMode !== undefined) {
+      config = { ...config, issueSyncMode: body.issueSyncMode };
+    }
Evidence
The PATCH route updates the mode after validating the configuration’s shape, while access
verification requires write permissions for sync but not for read-only modes. The integration test
demonstrates a token that passes ingest-only verification but fails sync verification, and the
settings UI calls PATCH directly when the mode changes, with no verification step.

apps/api/src/gitea-integration/controllers/verify-gitea-access.ts[56-58]
apps/api/src/gitea-integration/index.ts[320-330]
apps/web/src/components/project/gitea-integration-settings.tsx[409-412]
apps/api/src/gitea-integration/index.ts[320-329]
apps/api/src/plugins/gitea/config.ts[42-59]
apps/api/src/gitea-integration/controllers/verify-gitea-access.ts[49-68]
tests/api-integration/gitea-issue-sync-modes.test.ts[693-718]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A mode-only PATCH can enable two-way issue sync for a saved token that lacks repository write permissions. Access verification applies different permission requirements by mode, but the settings UI saves mode changes immediately and the PATCH handler performs only schema validation.
## Fix Focus Areas
- apps/api/src/gitea-integration/index.ts[320-329]
- apps/api/src/gitea-integration/controllers/verify-gitea-access.ts[49-68]
- apps/web/src/components/project/gitea-integration-settings.tsx[402-420]
## Recommended Fix
Before accepting a change from another mode to `issueSyncMode: "sync"`, call `verifyGiteaAccess` with the saved base URL, token, owner, and repository, passing `issueSyncMode: "sync"`. If `hasRequiredPermissions` is false, throw `HTTPException(400)` before updating the saved mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/api/src/gitea-integration/index.ts Outdated
Comment thread apps/api/src/gitea-integration/index.ts Outdated
Comment thread apps/api/src/gitea-integration/index.ts Outdated
Comment thread apps/api/src/gitea-integration/index.ts Outdated
Comment thread apps/api/src/gitea-integration/index.ts Outdated
@gabrielrinaldi

Copy link
Copy Markdown
Author

Addressed the five Qodo findings in 8d7e885:

  1. Moved settings persistence and queued-edit retirement into a controller; the PATCH route now delegates.
  2. Replaced token-only verification resolution with a guarded token-and-mode context. Corrupt saved configuration produces the controlled 400 reconnect error when saved values are needed. Explicit credentials and mode still allow reconnection.
  3. Coordinated every outbound Gitea issue/comment/label mutation with settings changes using an integration-row shared lock. Stale or cancelled intents are skipped explicitly, late completions cannot revive retired intents, and the request deadline covers destination validation as well as HTTP. Task/link rows are not locked across network calls.
  4. Unchanged-mode saves preserve queued work without retirement scans. Actual transitions to restricted modes still retire the appropriate work, including Ingest only → Off.
  5. Enabling Two-way sync verifies repository write permission before saving. The transaction rejects concurrent configuration changes with 409 rather than saving settings verified against obsolete credentials.

The request-coordination change also preserves best-effort label behavior: a provider label failure does not prevent the independent close/reopen or backlink operation, while a refused write still stops subsequent mutations. Added regression coverage and updated the setup guide and generated OpenAPI reference.

Verification:

  • 1,207 PostgreSQL integration tests passed across 118 files, with TZ=UTC.
  • Unit suites passed: 851 API, 653 frontend, and 135 package tests. The frontend suite was rerun with four workers after three timeouts in the default parallel run; no timeout thresholds or frontend code were changed.
  • Typechecks, lint, locale consistency, and OpenAPI checks passed.
  • Standalone smoke with real PostgreSQL and localhost HTTP exercised the actual controllers and transport: an admitted request blocked a mode save; stale/cancelled work sent no later request; permission denial preserved the saved mode; corrupt configuration returned 400; and real HTTP 503 label responses still allowed issue close/reopen while Off suppressed further writes.

No production configuration changes or deployment. Disposable verification services and scripts were removed.

@gabrielrinaldi
gabrielrinaldi force-pushed the feat/gitea-issue-sync-modes branch from 8d7e885 to c8bf547 Compare October 3, 2026 17:53
@github-actions
github-actions Bot marked this pull request as draft October 6, 2026 01:41
@gabrielrinaldi
gabrielrinaldi force-pushed the feat/gitea-issue-sync-modes branch 2 times, most recently from b251dc6 to ccf0f50 Compare October 9, 2026 20:05
@gabrielrinaldi
gabrielrinaldi force-pushed the feat/gitea-issue-sync-modes branch from ccf0f50 to e32c166 Compare October 9, 2026 22:44
@gabrielrinaldi
gabrielrinaldi marked this pull request as ready for review October 9, 2026 22:44
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Hello there!

Your pull request was automatically converted to a draft.

Since October 5, 2026, contributors who have not been vouched for must link their pull requests to an open issue in this repository labeled ready-for-contribution.

Please open a bug report or feature request first, then wait for a maintainer to approve the scope and add the label. If an approved issue already exists, link it in your pull request description with Fixes #123 or Closes #123. Once the contribution eligibility check passes, you can mark your pull request ready for review.

Read more: https://github.com/usekaneo/kaneo?tab=contributing-ov-file#contribution-eligibility

@github-actions
github-actions Bot marked this pull request as draft October 9, 2026 22:44
@qodo-code-review

qodo-code-review Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Incoming issue edits miss paused tasks ✓ Resolved
Description
replayClaimedIssueEdits recognizes ingest-only jobs but still gates them through canSyncTask,
which rejects links whose tasks do not match the outgoing label rule. When an incoming Gitea edit is
deferred for a linked task without that label, replay skips the provider read and task update on
every retry, even though the direct inbound handler accepts edits to that link.
Code

apps/api/src/plugins/github/services/deferred-issue-edits.ts[R145-151]

+        const issueSyncMode =
+          integration?.type === "gitea"
+            ? getGiteaIssueSyncMode(
+                JSON.parse(integration.config) as GiteaConfig,
+              )
+            : "sync";
+        const ingestOnly = issueSyncMode === "ingest-only";
Relevance

●●● Strong

Inbound deferred edits should bypass outgoing eligibility; recent sync fixes favor preserving and
replaying deferred provider changes.

PR-#1867
PR-#1874

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new ingest-only branch is followed by an unchanged canSyncTask gate; that gate returns false
for an outgoing-rule mismatch and marks the link paused. The Gitea edit handler instead finds the
linked issue without that gate and queues a deferred edit when echo confirmation cannot complete,
making replay the path that must apply it.

apps/api/src/plugins/github/services/deferred-issue-edits.ts[145-175]
apps/api/src/plugins/sync/eligibility.ts[63-93]
apps/api/src/plugins/gitea/webhooks/issue-edited.ts[58-70]
apps/api/src/plugins/gitea/webhooks/issue-edited.ts[192-229]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Ingest-only deferred issue edits are skipped when the linked task fails the outgoing synchronization rule, leaving incoming updates unapplied.
## Fix Focus Areas
- apps/api/src/plugins/github/services/deferred-issue-edits.ts[145-175]
- apps/api/src/plugins/sync/eligibility.ts[63-93]
## Recommended Fix
For ingest-only replay, validate that the integration and linked issue remain active and in scope without requiring the task to match the outgoing rule. Keep outgoing eligibility checks for paths that write to the provider, and add a test with a linked task that lacks the configured outgoing label.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reconnecting can break the Gitea webhook signature ✓ Resolved
Description
createGiteaIntegration now runs the saved config through v.parse(giteaConfigSchema, …) and only
then reuses previousConfig.webhookSecret. If the stored JSON fails any schema rule (for example a
syncRules value that no longer passes the custom validator), parsing throws and a fresh random
secret is saved. This is exactly the situation where updateGiteaIntegration tells users to
"Reconnect the integration", so the advised reconnect silently invalidates the secret configured in
Gitea and every later delivery fails signature verification.
Code

apps/api/src/gitea-integration/controllers/create-gitea-integration.ts[R106-109]

+      const savedConfig: unknown = JSON.parse(existingIntegration.config);
+      previousConfig = v.parse(giteaConfigSchema, savedConfig);
      webhookSecret = previousConfig.webhookSecret ?? webhookSecret;
+      previousBaseUrl = normalizeGiteaBaseUrl(previousConfig.baseUrl);
Relevance

●●● Strong

Recent Gitea reconnect precedents accept preserving prior configuration and safely handling
malformed saved configs.

PR-#1908
PR-#1119

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
webhookSecret is read from the strictly parsed object, so any schema failure skips that line and
leaves the newly generated randomBytes secret. Before this PR, a plain JSON.parse kept the
secret for any valid JSON. The update controller throws "Invalid saved Gitea configuration.
Reconnect the integration." on the same strict parse. The webhook handler rejects deliveries whose
signature does not match the stored secret.

apps/api/src/gitea-integration/controllers/create-gitea-integration.ts[101-116]
apps/api/src/gitea-integration/controllers/update-gitea-integration.ts[17-26]
apps/api/src/plugins/gitea/webhook-handler.ts[93-102]
apps/api/src/plugins/gitea/config.ts[10-30]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When the saved Gitea config is valid JSON but fails `giteaConfigSchema`, reconnecting generates a new webhook secret, which breaks the webhook already configured in Gitea.

## Fix Focus Areas
- apps/api/src/gitea-integration/controllers/create-gitea-integration.ts[101-116]

## Recommended Fix
First `JSON.parse` the saved config. If the result is an object with a string `webhookSecret`, reuse that secret regardless of schema validity. Then do the strict `v.parse` separately to populate `previousConfig` (and leave it undefined if that fails), so a reconnect repairs the config without rotating the secret.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Slow Gitea requests stall webhooks and settings saves ✓ Resolved
Description
withGiteaOutboundWrite keeps a transaction open with FOR SHARE locks on the integration row (and
on rule label rows) while write() performs the Gitea HTTP request, for up to 10 seconds per call.
Meanwhile, inbound issue opened webhooks (which take FOR UPDATE on the integration), settings
and sync-rule saves, and label updates or deletes all wait for every in-flight Gitea write, each
holding its own pooled connection.
Code

apps/api/src/plugins/gitea/services/outbound-fence.ts[R120-125]

+    return await db.transaction(async (tx): Promise<GiteaWriteResult<T>> => {
+      const [integration] = await tx
+        .select()
+        .from(integrationTable)
+        .where(eq(integrationTable.id, binding.integrationId))
+        .for("share");
Relevance

●●● Strong

Recent integration fixes accepted preventing provider latency and concurrent webhook work from
occupying transaction-held connections.

PR-#1867
PR-#1119

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The second fence transaction locks the integration row FOR SHARE and keeps it through `await
write(). The existing generic dispatchIssueWrite path starts send()` inside the transaction but
waits for the result only after commit, specifically so provider latency never holds row locks. The
Gitea issue opened handler locks the same integration row FOR UPDATE, so it blocks behind any
outbound write. The label FOR SHARE lock likewise blocks label updates. The 4-slot semaphore
limits how many connections are used, but not how long others wait on the locks.

apps/api/src/plugins/gitea/services/outbound-fence.ts[120-219]
apps/api/src/plugins/sync/dispatch-issue-write.ts[80-108]
apps/api/src/plugins/gitea/webhooks/issue-opened.ts[96-106]
apps/api/src/plugins/gitea/utils/gitea-api.ts[72-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Gitea outbound fence holds integration and label row locks, plus a DB connection, for the full duration of each Gitea HTTP request. Inbound webhooks, settings saves and label edits block behind slow Gitea servers.

## Fix Focus Areas
- apps/api/src/plugins/gitea/services/outbound-fence.ts[120-219]
- apps/api/src/gitea-integration/controllers/update-gitea-integration.ts[59-102]

## Recommended Fix
Follow the `dispatchIssueWrite` pattern: run all eligibility checks inside the transaction, start `write()` before commit, and await it only after the transaction releases its locks. To stop stale writes after a mode change, use a cheaper coordination mechanism (for example an in-process or advisory counter that the settings update waits on), or record a generation number that completion paths re-check. Do not keep row locks open across HTTP.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
4. New two-way connections no longer export existing tasks 🐞 Bug ≡ Correctness
Description
The create route now always publishes integration.sync_rules_changed with `existingLinksOnly:
true, so reconcileProjectSync` only processes tasks that already have a Gitea issue link, even
when the new connection is in sync mode. Previously (and still for GitHub and GitLab) connecting a
repository exported every eligible unlinked task; first-time Gitea users in the default two-way mode
now get no issues created for their existing backlog.
Code

apps/api/src/gitea-integration/index.ts[R267-271]

      await publishEvent("integration.sync_rules_changed", {
        projectId,
        integrationId: integration.id,
+        existingLinksOnly: true,
      });
Relevance

●● Moderate

The PR explicitly avoids automatic exports during mode changes, but whether this includes first
connections is ambiguous.

PR-#1908

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
reconcileProjectSync adds an exists(externalLink…) filter whenever existingLinksOnly === true,
so unlinked tasks are never passed to onTaskCreated/createTaskIssue. The Gitea create route
hard-codes true no matter which mode is chosen. The GitHub and GitLab create routes publish the
same event without the flag, so they still export on connect. Only the update route keeps the export
path, and only when reactivating an integration in sync mode.

apps/api/src/gitea-integration/index.ts[251-272]
apps/api/src/plugins/sync/reconcile.ts[91-135]
apps/api/src/gitlab-integration/index.ts[257-277]
apps/api/src/github-integration/index.ts[301-317]
apps/api/src/gitea-integration/controllers/update-gitea-integration.ts[103-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Gitea create route always publishes `integration.sync_rules_changed` with `existingLinksOnly: true`, so a brand-new two-way connection never exports existing eligible tasks. That is a regression from previous Gitea behaviour and differs from GitHub/GitLab.

## Fix Focus Areas
- apps/api/src/gitea-integration/index.ts[251-272]
- apps/api/src/gitea-integration/controllers/create-gitea-integration.ts[141-150]

## Recommended Fix
Have `createGiteaIntegration` return whether this is a new connection (or a new target) in `sync` mode. Set `existingLinksOnly: true` only when the mode is not `sync`, or when only credentials changed on the same target in the same mode. Otherwise omit the flag so a fresh two-way connection exports eligible tasks as before.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗



Informational

5. Changing the mode on large projects briefly blocks syncing 🐞 Bug ➹ Performance
Description
retireGiteaIssueEdits loads every issue link for the integration and, for each one, runs up to
five sequential updateExternalLink savepoint transactions with per-link FOR UPDATE locks. All of
this happens inside the settings transaction that holds the integration row FOR UPDATE. For
projects with thousands of linked issues, a mode change keeps webhooks and outbound fences for that
integration waiting until the loop finishes.
Code

apps/api/src/plugins/gitea/services/retire-issue-edits.ts[R17-23]

+  const links = await database.query.externalLinkTable.findMany({
+    where: and(
+      eq(externalLinkTable.integrationId, integrationId),
+      eq(externalLinkTable.resourceType, "issue"),
+    ),
+  });
+  for (const link of links) {
Relevance

●● Moderate

The lock duration concern is plausible, but no close precedent confirms rejecting this required
mode-transition cleanup.

PR-#1867

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both the update and create controllers call the function inside the transaction that locked the
integration row. The function walks every link without filtering for links that actually carry
deferred jobs or pending/uncertain intents.

apps/api/src/plugins/gitea/services/retire-issue-edits.ts[12-70]
apps/api/src/gitea-integration/controllers/update-gitea-integration.ts[59-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A mode change iterates every issue link with several sequential updates while holding the integration lock.

## Fix Focus Areas
- apps/api/src/plugins/gitea/services/retire-issue-edits.ts[17-69]

## Recommended Fix
Restrict the query to links whose metadata contains `deferredIssueEdit` or pending/uncertain outbound intents (for example with a LIKE/JSONB filter, as `replayClaimedIssueEdits` does). Merge the per-field `retireOutboundIntents` calls into a single `updateExternalLink` per link.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Grey Divider

Context sources
✅ Compliance rules (platform): 6 rules
✅ Cross-repo context — repo relationships
  Explored: repo: usekaneo/drim (sha: f1e40491) — View relationship
Review mode: Auto: 🧠 Deep: Large, cross-cutting synchronization changes span many independent logic paths.

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/api/src/plugins/github/services/deferred-issue-edits.ts
Comment on lines 267 to 271
await publishEvent("integration.sync_rules_changed", {
projectId,
integrationId: integration.id,
existingLinksOnly: true,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. New two-way connections no longer export existing tasks 🐞 Bug ≡ Correctness

The create route now always publishes integration.sync_rules_changed with `existingLinksOnly:
true, so reconcileProjectSync` only processes tasks that already have a Gitea issue link, even
when the new connection is in sync mode. Previously (and still for GitHub and GitLab) connecting a
repository exported every eligible unlinked task; first-time Gitea users in the default two-way mode
now get no issues created for their existing backlog.
Agent Prompt
## Issue description
The Gitea create route always publishes `integration.sync_rules_changed` with `existingLinksOnly: true`, so a brand-new two-way connection never exports existing eligible tasks. That is a regression from previous Gitea behaviour and differs from GitHub/GitLab.

## Fix Focus Areas
- apps/api/src/gitea-integration/index.ts[251-272]
- apps/api/src/gitea-integration/controllers/create-gitea-integration.ts[141-150]

## Recommended Fix
Have `createGiteaIntegration` return whether this is a new connection (or a new target) in `sync` mode. Set `existingLinksOnly: true` only when the mode is not `sync`, or when only credentials changed on the same target in the same mode. Otherwise omit the flag so a fresh two-way connection exports eligible tasks as before.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment thread apps/api/src/plugins/gitea/services/outbound-fence.ts
Comment on lines +17 to +23
const links = await database.query.externalLinkTable.findMany({
where: and(
eq(externalLinkTable.integrationId, integrationId),
eq(externalLinkTable.resourceType, "issue"),
),
});
for (const link of links) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

5. Changing the mode on large projects briefly blocks syncing 🐞 Bug ➹ Performance

retireGiteaIssueEdits loads every issue link for the integration and, for each one, runs up to
five sequential updateExternalLink savepoint transactions with per-link FOR UPDATE locks. All of
this happens inside the settings transaction that holds the integration row FOR UPDATE. For
projects with thousands of linked issues, a mode change keeps webhooks and outbound fences for that
integration waiting until the loop finishes.
Agent Prompt
## Issue description
A mode change iterates every issue link with several sequential updates while holding the integration lock.

## Fix Focus Areas
- apps/api/src/plugins/gitea/services/retire-issue-edits.ts[17-69]

## Recommended Fix
Restrict the query to links whose metadata contains `deferredIssueEdit` or pending/uncertain outbound intents (for example with a LIKE/JSONB filter, as `replayClaimedIssueEdits` does). Merge the per-field `retireOutboundIntents` calls into a single `updateExternalLink` per link.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

@gabrielrinaldi
gabrielrinaldi deleted the feat/gitea-issue-sync-modes branch October 10, 2026 01:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Archived tasks show as open in Gitea

1 participant