@wascartech/devbot 0.0.0-stage → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +638 -2
- package/dist/access-grant-store.js +99 -0
- package/dist/access-grant-store.js.map +1 -0
- package/dist/access-resolve.js +11 -0
- package/dist/access-resolve.js.map +1 -0
- package/dist/access-rule-store.js +99 -0
- package/dist/access-rule-store.js.map +1 -0
- package/dist/admin/access-routes.js +73 -0
- package/dist/admin/access-routes.js.map +1 -0
- package/dist/admin/access-rule-routes.js +107 -0
- package/dist/admin/access-rule-routes.js.map +1 -0
- package/dist/admin/custom-repos-routes.js +93 -0
- package/dist/admin/custom-repos-routes.js.map +1 -0
- package/dist/admin/env-routes.js +74 -0
- package/dist/admin/env-routes.js.map +1 -0
- package/dist/admin/global-settings-routes.js +254 -0
- package/dist/admin/global-settings-routes.js.map +1 -0
- package/dist/admin/mcp-cleanup.js +12 -0
- package/dist/admin/mcp-cleanup.js.map +1 -0
- package/dist/admin/mcp-routes.js +169 -0
- package/dist/admin/mcp-routes.js.map +1 -0
- package/dist/admin/query-params.js +80 -0
- package/dist/admin/query-params.js.map +1 -0
- package/dist/admin/routes.js +150 -0
- package/dist/admin/routes.js.map +1 -0
- package/dist/admin/run-history-routes.js +44 -0
- package/dist/admin/run-history-routes.js.map +1 -0
- package/dist/admin/run-routes.js +99 -0
- package/dist/admin/run-routes.js.map +1 -0
- package/dist/admin/settings-routes.js +119 -0
- package/dist/admin/settings-routes.js.map +1 -0
- package/dist/admin/stats-routes.js +51 -0
- package/dist/admin/stats-routes.js.map +1 -0
- package/dist/admin/task-routes.js +213 -0
- package/dist/admin/task-routes.js.map +1 -0
- package/dist/admin/test-routes.js +38 -0
- package/dist/admin/test-routes.js.map +1 -0
- package/dist/admin/volume-routes.js +82 -0
- package/dist/admin/volume-routes.js.map +1 -0
- package/dist/admin/webhook-routes.js +174 -0
- package/dist/admin/webhook-routes.js.map +1 -0
- package/dist/agents.js +48 -0
- package/dist/agents.js.map +1 -0
- package/dist/assignee-value.js +6 -0
- package/dist/assignee-value.js.map +1 -0
- package/dist/attachments.js +76 -0
- package/dist/attachments.js.map +1 -0
- package/dist/auto-commands.js +88 -0
- package/dist/auto-commands.js.map +1 -0
- package/dist/auto-features.js +32 -0
- package/dist/auto-features.js.map +1 -0
- package/dist/base-branch-store.js +40 -0
- package/dist/base-branch-store.js.map +1 -0
- package/dist/base-branches.js +9 -0
- package/dist/base-branches.js.map +1 -0
- package/dist/build-workflow.js +116 -0
- package/dist/build-workflow.js.map +1 -0
- package/dist/change-request-lifecycle.js +16 -0
- package/dist/change-request-lifecycle.js.map +1 -0
- package/dist/change-request-store.js +72 -0
- package/dist/change-request-store.js.map +1 -0
- package/dist/ci-failure.js +80 -0
- package/dist/ci-failure.js.map +1 -0
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -0
- package/dist/conflict-fix.js +178 -0
- package/dist/conflict-fix.js.map +1 -0
- package/dist/cookie.js +70 -0
- package/dist/cookie.js.map +1 -0
- package/dist/crypto-util.js +40 -0
- package/dist/crypto-util.js.map +1 -0
- package/dist/custom-repos-store.js +90 -0
- package/dist/custom-repos-store.js.map +1 -0
- package/dist/custom-repos-sync.js +118 -0
- package/dist/custom-repos-sync.js.map +1 -0
- package/dist/db.js +12 -0
- package/dist/db.js.map +1 -0
- package/dist/devbot-scripts.js +18 -0
- package/dist/devbot-scripts.js.map +1 -0
- package/dist/devcontainer.js +141 -0
- package/dist/devcontainer.js.map +1 -0
- package/dist/env-config-store.js +132 -0
- package/dist/env-config-store.js.map +1 -0
- package/dist/error-log-store.js +193 -0
- package/dist/error-log-store.js.map +1 -0
- package/dist/git.js +605 -0
- package/dist/git.js.map +1 -0
- package/dist/global-queue.js +127 -0
- package/dist/global-queue.js.map +1 -0
- package/dist/global-settings-store.js +307 -0
- package/dist/global-settings-store.js.map +1 -0
- package/dist/help.js +41 -0
- package/dist/help.js.map +1 -0
- package/dist/http-csp.js +15 -0
- package/dist/http-csp.js.map +1 -0
- package/dist/http-util.js +40 -0
- package/dist/http-util.js.map +1 -0
- package/dist/idempotency.js +50 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/lifecycle.js +234 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/logger.js +33 -0
- package/dist/logger.js.map +1 -0
- package/dist/lsp-servers.js +81 -0
- package/dist/lsp-servers.js.map +1 -0
- package/dist/mcp/auth.js +62 -0
- package/dist/mcp/auth.js.map +1 -0
- package/dist/mcp/effective-mcp.js +42 -0
- package/dist/mcp/effective-mcp.js.map +1 -0
- package/dist/mcp/oauth-refresh.js +138 -0
- package/dist/mcp/oauth-refresh.js.map +1 -0
- package/dist/mcp/peer-sync.js +109 -0
- package/dist/mcp/peer-sync.js.map +1 -0
- package/dist/mcp/registry.js +54 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/runner-bridge.js +93 -0
- package/dist/mcp/runner-bridge.js.map +1 -0
- package/dist/mcp/server.js +173 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/_helpers.js +88 -0
- package/dist/mcp/tools/_helpers.js.map +1 -0
- package/dist/mcp/tools/attachments.js +117 -0
- package/dist/mcp/tools/attachments.js.map +1 -0
- package/dist/mcp/tools/cicd.js +105 -0
- package/dist/mcp/tools/cicd.js.map +1 -0
- package/dist/mcp/tools/discovery.js +34 -0
- package/dist/mcp/tools/discovery.js.map +1 -0
- package/dist/mcp/tools/issues.js +56 -0
- package/dist/mcp/tools/issues.js.map +1 -0
- package/dist/mcp/tools/merge_requests.js +67 -0
- package/dist/mcp/tools/merge_requests.js.map +1 -0
- package/dist/mcp/tools/metadata.js +127 -0
- package/dist/mcp/tools/metadata.js.map +1 -0
- package/dist/mcp/tools/peer_sync.js +43 -0
- package/dist/mcp/tools/peer_sync.js.map +1 -0
- package/dist/mcp/tools/repository.js +178 -0
- package/dist/mcp/tools/repository.js.map +1 -0
- package/dist/mcp/tools/work_tracking.js +125 -0
- package/dist/mcp/tools/work_tracking.js.map +1 -0
- package/dist/mcp/tools/workitems.js +30 -0
- package/dist/mcp/tools/workitems.js.map +1 -0
- package/dist/mcp/volume-files.js +187 -0
- package/dist/mcp/volume-files.js.map +1 -0
- package/dist/mcp-auth-store.js +134 -0
- package/dist/mcp-auth-store.js.map +1 -0
- package/dist/mcp-config-store.js +83 -0
- package/dist/mcp-config-store.js.map +1 -0
- package/dist/mention.js +512 -0
- package/dist/mention.js.map +1 -0
- package/dist/migrate.js +73 -0
- package/dist/migrate.js.map +1 -0
- package/dist/mr-assignee.js +71 -0
- package/dist/mr-assignee.js.map +1 -0
- package/dist/mr-content.js +90 -0
- package/dist/mr-content.js.map +1 -0
- package/dist/oauth-routes.js +54 -0
- package/dist/oauth-routes.js.map +1 -0
- package/dist/onboarding-allowlist.js +32 -0
- package/dist/onboarding-allowlist.js.map +1 -0
- package/dist/opencode.js +291 -0
- package/dist/opencode.js.map +1 -0
- package/dist/outbound-webhook-store.js +229 -0
- package/dist/outbound-webhook-store.js.map +1 -0
- package/dist/outbound-webhook.js +314 -0
- package/dist/outbound-webhook.js.map +1 -0
- package/dist/path-sandbox.js +200 -0
- package/dist/path-sandbox.js.map +1 -0
- package/dist/platform/git-auth.js +34 -0
- package/dist/platform/git-auth.js.map +1 -0
- package/dist/platform/gitealike/attachments.js +62 -0
- package/dist/platform/gitealike/attachments.js.map +1 -0
- package/dist/platform/gitealike/base.js +935 -0
- package/dist/platform/gitealike/base.js.map +1 -0
- package/dist/platform/gitealike/forgejo.js +16 -0
- package/dist/platform/gitealike/forgejo.js.map +1 -0
- package/dist/platform/gitealike/gitea.js +16 -0
- package/dist/platform/gitealike/gitea.js.map +1 -0
- package/dist/platform/gitealike/http.js +152 -0
- package/dist/platform/gitealike/http.js.map +1 -0
- package/dist/platform/gitealike/schemas.js +284 -0
- package/dist/platform/gitealike/schemas.js.map +1 -0
- package/dist/platform/github/attachments.js +52 -0
- package/dist/platform/github/attachments.js.map +1 -0
- package/dist/platform/github/http.js +153 -0
- package/dist/platform/github/http.js.map +1 -0
- package/dist/platform/github/index.js +936 -0
- package/dist/platform/github/index.js.map +1 -0
- package/dist/platform/github/schemas.js +308 -0
- package/dist/platform/github/schemas.js.map +1 -0
- package/dist/platform/github/zip.js +85 -0
- package/dist/platform/github/zip.js.map +1 -0
- package/dist/platform/gitlab/admin.js +105 -0
- package/dist/platform/gitlab/admin.js.map +1 -0
- package/dist/platform/gitlab/api.js +229 -0
- package/dist/platform/gitlab/api.js.map +1 -0
- package/dist/platform/gitlab/attachments.js +79 -0
- package/dist/platform/gitlab/attachments.js.map +1 -0
- package/dist/platform/gitlab/http.js +165 -0
- package/dist/platform/gitlab/http.js.map +1 -0
- package/dist/platform/gitlab/index.js +630 -0
- package/dist/platform/gitlab/index.js.map +1 -0
- package/dist/platform/gitlab/oauth.js +58 -0
- package/dist/platform/gitlab/oauth.js.map +1 -0
- package/dist/platform/gitlab/read.js +182 -0
- package/dist/platform/gitlab/read.js.map +1 -0
- package/dist/platform/gitlab/schemas-read.js +274 -0
- package/dist/platform/gitlab/schemas-read.js.map +1 -0
- package/dist/platform/gitlab/schemas.js +251 -0
- package/dist/platform/gitlab/schemas.js.map +1 -0
- package/dist/platform/gitlab/signature.js +52 -0
- package/dist/platform/gitlab/signature.js.map +1 -0
- package/dist/platform/index.js +38 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/port.js +26 -0
- package/dist/platform/port.js.map +1 -0
- package/dist/platform/types.js +80 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/podman.js +119 -0
- package/dist/podman.js.map +1 -0
- package/dist/project-sync.js +69 -0
- package/dist/project-sync.js.map +1 -0
- package/dist/prompt.js +47 -0
- package/dist/prompt.js.map +1 -0
- package/dist/queue-store.js +236 -0
- package/dist/queue-store.js.map +1 -0
- package/dist/queue.js +129 -0
- package/dist/queue.js.map +1 -0
- package/dist/redact.js +23 -0
- package/dist/redact.js.map +1 -0
- package/dist/run-files.js +297 -0
- package/dist/run-files.js.map +1 -0
- package/dist/run-filters.js +54 -0
- package/dist/run-filters.js.map +1 -0
- package/dist/run-history-store.js +229 -0
- package/dist/run-history-store.js.map +1 -0
- package/dist/run-log-sink.js +270 -0
- package/dist/run-log-sink.js.map +1 -0
- package/dist/run-log.js +97 -0
- package/dist/run-log.js.map +1 -0
- package/dist/run-usage.js +29 -0
- package/dist/run-usage.js.map +1 -0
- package/dist/run-view.js +112 -0
- package/dist/run-view.js.map +1 -0
- package/dist/runner.js +1094 -0
- package/dist/runner.js.map +1 -0
- package/dist/runtime-apply.js +26 -0
- package/dist/runtime-apply.js.map +1 -0
- package/dist/runtime-settings.js +71 -0
- package/dist/runtime-settings.js.map +1 -0
- package/dist/runtime-state.js +19 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/server.js +119 -0
- package/dist/server.js.map +1 -0
- package/dist/settings-store.js +320 -0
- package/dist/settings-store.js.map +1 -0
- package/dist/status.js +121 -0
- package/dist/status.js.map +1 -0
- package/dist/stop.js +56 -0
- package/dist/stop.js.map +1 -0
- package/dist/sweeps.js +87 -0
- package/dist/sweeps.js.map +1 -0
- package/dist/task-log.js +279 -0
- package/dist/task-log.js.map +1 -0
- package/dist/task-view.js +62 -0
- package/dist/task-view.js.map +1 -0
- package/dist/token-store.js +68 -0
- package/dist/token-store.js.map +1 -0
- package/dist/types/access.js +74 -0
- package/dist/types/access.js.map +1 -0
- package/dist/types/custom-repos.js +28 -0
- package/dist/types/custom-repos.js.map +1 -0
- package/dist/types/env-config.js +25 -0
- package/dist/types/env-config.js.map +1 -0
- package/dist/types/env.js +190 -0
- package/dist/types/env.js.map +1 -0
- package/dist/types/global-settings.js +178 -0
- package/dist/types/global-settings.js.map +1 -0
- package/dist/types/lsp-servers.js +145 -0
- package/dist/types/lsp-servers.js.map +1 -0
- package/dist/types/mcp-config.js +77 -0
- package/dist/types/mcp-config.js.map +1 -0
- package/dist/types/opencode.js +59 -0
- package/dist/types/opencode.js.map +1 -0
- package/dist/types/outbound-webhook.js +56 -0
- package/dist/types/outbound-webhook.js.map +1 -0
- package/dist/types/project-settings.js +114 -0
- package/dist/types/project-settings.js.map +1 -0
- package/dist/types/provider-registry.js +37 -0
- package/dist/types/provider-registry.js.map +1 -0
- package/dist/types/runtime-settings.js +78 -0
- package/dist/types/runtime-settings.js.map +1 -0
- package/dist/usage-stats.js +159 -0
- package/dist/usage-stats.js.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/dist/volume-gc.js +193 -0
- package/dist/volume-gc.js.map +1 -0
- package/dist/volume-manage.js +191 -0
- package/dist/volume-manage.js.map +1 -0
- package/dist/volume-store.js +122 -0
- package/dist/volume-store.js.map +1 -0
- package/dist/webhook.js +1106 -0
- package/dist/webhook.js.map +1 -0
- package/dist/work-item-auto-store.js +75 -0
- package/dist/work-item-auto-store.js.map +1 -0
- package/dist/work-item-branch-store.js +37 -0
- package/dist/work-item-branch-store.js.map +1 -0
- package/migrations/0001_initial.sql +59 -0
- package/migrations/0002_queue_status.sql +15 -0
- package/migrations/0003_work_item_base_branch.sql +14 -0
- package/migrations/0004_project_settings.sql +15 -0
- package/migrations/0005_global_settings.sql +34 -0
- package/migrations/0006_model_variants.sql +8 -0
- package/migrations/0007_container_error_logs.sql +21 -0
- package/migrations/0008_project_access_grants.sql +16 -0
- package/migrations/0009_drop_imported_legacy.sql +4 -0
- package/migrations/0010_container_error_log_model.sql +6 -0
- package/migrations/0011_container_error_log_title.sql +5 -0
- package/migrations/0012_pending_task_cancel.sql +6 -0
- package/migrations/0013_builder_image.sql +6 -0
- package/migrations/0014_build_workflow.sql +7 -0
- package/migrations/0015_access_rules.sql +24 -0
- package/migrations/0016_trigger_policy.sql +17 -0
- package/migrations/0017_attachment_upload.sql +8 -0
- package/migrations/0018_work_item_branch.sql +11 -0
- package/migrations/0019_auto_push_mr.sql +18 -0
- package/migrations/0020_outbound_webhooks.sql +47 -0
- package/migrations/0021_runtime_settings.sql +12 -0
- package/migrations/0022_auto_resolve_conflicts.sql +17 -0
- package/migrations/0023_ci_fail_auto_report.sql +27 -0
- package/migrations/0024_work_item_auto_override.sql +22 -0
- package/migrations/0025_run_history.sql +37 -0
- package/migrations/0026_run_history_task_seq.sql +6 -0
- package/migrations/0027_custom_repos.sql +11 -0
- package/migrations/0028_run_usage.sql +14 -0
- package/migrations/0029_drop_ocr_langs.sql +6 -0
- package/migrations/0030_mr_assignee.sql +9 -0
- package/migrations/0031_lsp_servers.sql +14 -0
- package/opencode-config/managed/devbot-agent-policy.md +61 -0
- package/opencode-config/managed/opencode.json +77 -0
- package/opencode-config/opencode.json +211 -0
- package/opencode-config/plugins/devbot-path-guard.js +224 -0
- package/package.json +157 -4
- package/public/assets/DashboardSidebarCollapse-rIuzCmVm.js +1 -0
- package/public/assets/DashboardToolbar-DFCUqZ4Z.js +1 -0
- package/public/assets/ErrorLogModal.vue_vue_type_script_setup_true_lang-BCyNIf1E.js +1 -0
- package/public/assets/ErrorLogsPage-DyYK2Rop.js +1 -0
- package/public/assets/LogSlideover.vue_vue_type_script_setup_true_lang-33xie7TE.js +1 -0
- package/public/assets/OverviewPage-CFfa0uwg.js +1 -0
- package/public/assets/PageGrid-CBROIU87.js +1 -0
- package/public/assets/PeriodRangePicker.vue_vue_type_script_setup_true_lang-DiFUNem7.js +1 -0
- package/public/assets/ProjectDetailPage-CreyvUit.js +1 -0
- package/public/assets/ProjectsPage-CFx8-elJ.js +1 -0
- package/public/assets/RunDetailSlideover.vue_vue_type_script_setup_true_lang-DopYYmCT.js +1 -0
- package/public/assets/RunsPage-CRnnXU4P.js +1 -0
- package/public/assets/Select-Bvx32Diz.js +1 -0
- package/public/assets/SelectMenu-BjMxc4VB.js +1 -0
- package/public/assets/SettingsPage-Bb6LM3wO.js +60 -0
- package/public/assets/TimeCell.vue_vue_type_script_setup_true_lang-DW_UB0Fu.js +1 -0
- package/public/assets/UsagePage-BLMDEsv-.js +332 -0
- package/public/assets/UsagePage-DodEeAjB.css +1 -0
- package/public/assets/VolumesPage-CxcLc3L2.js +1 -0
- package/public/assets/WebhookConfigPanel.vue_vue_type_script_setup_true_lang-B2V2GF6b.js +1 -0
- package/public/assets/WorkItemCell.vue_vue_type_script_setup_true_lang-BQn6vTjd.js +1 -0
- package/public/assets/index-BMbMibyb.js +4 -0
- package/public/assets/index-C5RBBrPj.js +30 -0
- package/public/assets/index-CgX29UgC.css +1 -0
- package/public/assets/usage-format-jsb31_O8.js +1 -0
- package/public/assets/useManagedProjects-8rtjRqVJ.js +1 -0
- package/public/assets/useRouteTab-DfwrcMic.js +1 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/index.html +15 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
// Shared Gitea/ForgeJo driver. Implements every capability against the /api/v1
|
|
2
|
+
// surface. ForgeJo and Gitea differ only in the platform id, webhook header
|
|
3
|
+
// names, and OAuth endpoint paths (see forgejo.ts / gitea.ts).
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
6
|
+
import { config } from "../../config.js";
|
|
7
|
+
import { log } from "../../logger.js";
|
|
8
|
+
import { PlatformHttpError, UnsupportedError, tailFileContent } from "../types.js";
|
|
9
|
+
import { gitExtraHeaderGitea } from "../git-auth.js";
|
|
10
|
+
import { findGiteaAttachmentRefs, downloadGiteaAttachment, uploadGiteaAttachment } from "./attachments.js";
|
|
11
|
+
import { GiteaActionJobSchema, GiteaActionRunSchema, GiteaBranchSchema, GiteaCollaboratorSchema, GiteaCommentSchema, GiteaCommitSchema, GiteaCompareSchema, GiteaContentSchema, GiteaIssueCommentPayloadSchema, GiteaIssuePayloadSchema, GiteaIssueSchema, GiteaLabelSchema, GiteaMilestoneSchema, GiteaPermissionsSchema, GiteaPullRequestPayloadSchema, GiteaPullRequestSchema, GiteaReleaseSchema, GiteaRepositorySchema, GiteaReviewCommentSchema, GiteaReviewSchema, GiteaTagSchema, GiteaUserSchema, GiteaWebhookSchema, GiteaWikiPageSchema, GiteaWorkflowRunPayloadSchema, } from "./schemas.js";
|
|
12
|
+
import { giteaRequest, giteaRequestArray, giteaFetchRaw, giteaVoid, pageParams } from "./http.js";
|
|
13
|
+
const BOT = { kind: "bot" };
|
|
14
|
+
// ---- Mappers ---------------------------------------------------------------
|
|
15
|
+
function splitFullName(fullName) {
|
|
16
|
+
const idx = fullName.indexOf("/");
|
|
17
|
+
if (idx === -1)
|
|
18
|
+
return { owner: "", repo: fullName };
|
|
19
|
+
return { owner: fullName.slice(0, idx), repo: fullName.slice(idx + 1) };
|
|
20
|
+
}
|
|
21
|
+
function mapRepo(r) {
|
|
22
|
+
return {
|
|
23
|
+
id: r.id,
|
|
24
|
+
path: r.full_name,
|
|
25
|
+
webUrl: r.html_url ?? "",
|
|
26
|
+
cloneUrl: r.clone_url || `${config.PLATFORM_URL.replace(/\/$/, "")}/${r.full_name}.git`,
|
|
27
|
+
defaultBranch: r.default_branch ?? null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function mapWorkItemState(state) {
|
|
31
|
+
return state;
|
|
32
|
+
}
|
|
33
|
+
function mapUser(u) {
|
|
34
|
+
if (!u)
|
|
35
|
+
return undefined;
|
|
36
|
+
return { id: u.id, username: u.login, name: u.full_name || u.login };
|
|
37
|
+
}
|
|
38
|
+
function mapIssue(i) {
|
|
39
|
+
const assignees = i.assignees && i.assignees.length > 0 ? i.assignees : i.assignee ? [i.assignee] : [];
|
|
40
|
+
return {
|
|
41
|
+
id: i.id,
|
|
42
|
+
number: i.number,
|
|
43
|
+
title: i.title,
|
|
44
|
+
description: i.body ?? "",
|
|
45
|
+
state: mapWorkItemState(i.state),
|
|
46
|
+
webUrl: i.html_url ?? "",
|
|
47
|
+
...(i.user ? { author: { id: i.user.id } } : {}),
|
|
48
|
+
...(assignees.length > 0 ? { assignees: assignees.map((a) => ({ id: a.id })) } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function mapIssueSummary(i) {
|
|
52
|
+
const who = mapUser(i.user);
|
|
53
|
+
return {
|
|
54
|
+
id: i.id,
|
|
55
|
+
number: i.number,
|
|
56
|
+
title: i.title,
|
|
57
|
+
state: mapWorkItemState(i.state),
|
|
58
|
+
webUrl: i.html_url ?? "",
|
|
59
|
+
...(i.body != null ? { description: i.body } : {}),
|
|
60
|
+
...(i.labels ? { labels: i.labels.map((l) => l.name) } : {}),
|
|
61
|
+
...(i.assignees
|
|
62
|
+
? { assignees: i.assignees.map((a) => ({ id: a.id, username: a.login, name: a.full_name || a.login })) }
|
|
63
|
+
: {}),
|
|
64
|
+
...(who ? { author: who } : {}),
|
|
65
|
+
...(i.created_at ? { createdAt: i.created_at } : {}),
|
|
66
|
+
...(i.due_date !== undefined ? { dueDate: i.due_date } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function mapComment(c) {
|
|
70
|
+
const who = mapUser(c.user);
|
|
71
|
+
return {
|
|
72
|
+
id: c.id,
|
|
73
|
+
body: c.body,
|
|
74
|
+
...(who ? { author: who } : {}),
|
|
75
|
+
...(c.created_at ? { createdAt: c.created_at } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function mapPrState(pr) {
|
|
79
|
+
if (pr.merged)
|
|
80
|
+
return "merged";
|
|
81
|
+
if (pr.state === "open")
|
|
82
|
+
return "open";
|
|
83
|
+
if (pr.state === "closed")
|
|
84
|
+
return "closed";
|
|
85
|
+
return pr.state;
|
|
86
|
+
}
|
|
87
|
+
function mapPr(pr) {
|
|
88
|
+
return {
|
|
89
|
+
id: pr.id,
|
|
90
|
+
number: pr.number,
|
|
91
|
+
title: pr.title,
|
|
92
|
+
state: mapPrState(pr),
|
|
93
|
+
sourceBranch: pr.head?.ref ?? "",
|
|
94
|
+
targetBranch: pr.base?.ref ?? "",
|
|
95
|
+
webUrl: pr.html_url ?? "",
|
|
96
|
+
...(pr.draft !== undefined ? { draft: pr.draft } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function mapPrSummary(pr) {
|
|
100
|
+
const who = mapUser(pr.user);
|
|
101
|
+
return {
|
|
102
|
+
id: pr.id,
|
|
103
|
+
number: pr.number,
|
|
104
|
+
title: pr.title,
|
|
105
|
+
state: mapPrState(pr),
|
|
106
|
+
webUrl: pr.html_url ?? "",
|
|
107
|
+
...(pr.body != null ? { description: pr.body } : {}),
|
|
108
|
+
...(pr.head ? { sourceBranch: pr.head.ref } : {}),
|
|
109
|
+
...(pr.base ? { targetBranch: pr.base.ref } : {}),
|
|
110
|
+
...(who ? { author: who } : {}),
|
|
111
|
+
...(pr.created_at ? { createdAt: pr.created_at } : {}),
|
|
112
|
+
...(pr.updated_at ? { updatedAt: pr.updated_at } : {}),
|
|
113
|
+
...(pr.merged_at !== undefined ? { mergedAt: pr.merged_at } : {}),
|
|
114
|
+
...(pr.draft !== undefined ? { draft: pr.draft } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function mapCommit(c) {
|
|
118
|
+
const msg = c.commit?.message ?? "";
|
|
119
|
+
const [title, ...rest] = msg.split("\n");
|
|
120
|
+
return {
|
|
121
|
+
id: c.sha,
|
|
122
|
+
title: (title ?? "").trim(),
|
|
123
|
+
...(rest.length ? { message: msg } : {}),
|
|
124
|
+
...(c.commit?.author?.name ? { authorName: c.commit.author.name } : {}),
|
|
125
|
+
...(c.commit?.author?.date ? { authoredDate: c.commit.author.date } : {}),
|
|
126
|
+
...(c.html_url ? { webUrl: c.html_url } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function mapCompare(c) {
|
|
130
|
+
return {
|
|
131
|
+
commits: (c.commits ?? []).map((x) => {
|
|
132
|
+
const title = x.commit?.message ? x.commit.message.split("\n")[0]?.trim() : undefined;
|
|
133
|
+
return {
|
|
134
|
+
id: x.sha,
|
|
135
|
+
...(title ? { title } : {}),
|
|
136
|
+
...(x.commit?.author?.name ? { authorName: x.commit.author.name } : {}),
|
|
137
|
+
...(x.commit?.author?.date ? { createdAt: x.commit.author.date } : {}),
|
|
138
|
+
};
|
|
139
|
+
}),
|
|
140
|
+
...(c.files
|
|
141
|
+
? {
|
|
142
|
+
diffs: c.files.map((f) => ({
|
|
143
|
+
oldPath: f.filename,
|
|
144
|
+
newPath: f.filename,
|
|
145
|
+
...(f.patch ? { diff: f.patch } : {}),
|
|
146
|
+
})),
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function normalizeRole(role, admin, push) {
|
|
152
|
+
const r = (role ?? "").toLowerCase();
|
|
153
|
+
if (r === "owner")
|
|
154
|
+
return "owner";
|
|
155
|
+
if (r === "admin" || admin)
|
|
156
|
+
return "admin";
|
|
157
|
+
if (r === "write" || r === "maintainer" || push)
|
|
158
|
+
return "write";
|
|
159
|
+
if (r === "read" || r === "reporter")
|
|
160
|
+
return "read";
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
// Derive the actor's normalized permission from a webhook payload. The forge
|
|
164
|
+
// computes `repository.permissions` for the payload's permission holder (the
|
|
165
|
+
// comment author / issue poster) and exposes it because it is that user's own
|
|
166
|
+
// permission, so it is only meaningful when the event sender is also that holder
|
|
167
|
+
// — otherwise the gate would attribute the wrong user's role to the actor.
|
|
168
|
+
// `repository.owner` is checked first, and without needing the permissions block,
|
|
169
|
+
// because the owner is knowable from the signed payload alone and a bot PAT can
|
|
170
|
+
// never read it back through the admin-only permission endpoint.
|
|
171
|
+
function permissionHint(repo, actor, holder) {
|
|
172
|
+
if (!actor || !holder || actor.id !== holder.id)
|
|
173
|
+
return undefined;
|
|
174
|
+
const ownerLogin = repo.owner?.login;
|
|
175
|
+
if (ownerLogin && ownerLogin.toLowerCase() === actor.username.toLowerCase())
|
|
176
|
+
return "owner";
|
|
177
|
+
if (!repo.permissions)
|
|
178
|
+
return undefined;
|
|
179
|
+
const p = repo.permissions;
|
|
180
|
+
if (p.admin)
|
|
181
|
+
return "admin";
|
|
182
|
+
if (p.push)
|
|
183
|
+
return "write";
|
|
184
|
+
if (p.pull)
|
|
185
|
+
return "read";
|
|
186
|
+
return "none";
|
|
187
|
+
}
|
|
188
|
+
// ---- Driver ----------------------------------------------------------------
|
|
189
|
+
export function createGiteaLikePlatform(opts) {
|
|
190
|
+
const { platform, eventHeader, signatureHeader, deliveryHeader } = opts;
|
|
191
|
+
const apiBase = () => `${config.PLATFORM_URL.replace(/\/$/, "")}/api/v1`;
|
|
192
|
+
function repoPath(fullName) {
|
|
193
|
+
const { owner, repo } = splitFullName(fullName);
|
|
194
|
+
return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
195
|
+
}
|
|
196
|
+
// Numeric repo id -> full_name cache. The MCP + runner only carry ids.
|
|
197
|
+
const fullNameCache = new Map();
|
|
198
|
+
async function fullNameOf(repoId) {
|
|
199
|
+
const hit = fullNameCache.get(repoId);
|
|
200
|
+
if (hit)
|
|
201
|
+
return hit;
|
|
202
|
+
const repo = await giteaRequest(`/repositories/${repoId}`, { method: "GET" }, GiteaRepositorySchema, BOT);
|
|
203
|
+
fullNameCache.set(repoId, repo.full_name);
|
|
204
|
+
return repo.full_name;
|
|
205
|
+
}
|
|
206
|
+
// ---- Ingress -------------------------------------------------------------
|
|
207
|
+
const ingress = {
|
|
208
|
+
platform,
|
|
209
|
+
eventName(headers) {
|
|
210
|
+
const v = headers[eventHeader];
|
|
211
|
+
return Array.isArray(v) ? v[0] : v;
|
|
212
|
+
},
|
|
213
|
+
projectIdFromBody(body) {
|
|
214
|
+
const raw = body?.repository?.id;
|
|
215
|
+
return typeof raw === "number" ? raw : undefined;
|
|
216
|
+
},
|
|
217
|
+
deliveryId(headers) {
|
|
218
|
+
const v = headers[deliveryHeader];
|
|
219
|
+
return Array.isArray(v) ? v[0] : v;
|
|
220
|
+
},
|
|
221
|
+
verifySignature(secret, headers, rawBody) {
|
|
222
|
+
const v = headers[signatureHeader];
|
|
223
|
+
const sig = Array.isArray(v) ? v[0] : v;
|
|
224
|
+
if (!sig || !secret)
|
|
225
|
+
return false;
|
|
226
|
+
const digest = createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
227
|
+
const got = Buffer.from(sig, "utf8");
|
|
228
|
+
const exp = Buffer.from(digest, "utf8");
|
|
229
|
+
if (got.length !== exp.length)
|
|
230
|
+
return false;
|
|
231
|
+
return timingSafeEqual(got, exp);
|
|
232
|
+
},
|
|
233
|
+
generateWebhookSecret() {
|
|
234
|
+
return randomBytes(32).toString("hex");
|
|
235
|
+
},
|
|
236
|
+
parseWebhook(headers, body) {
|
|
237
|
+
const event = this.eventName(headers);
|
|
238
|
+
if (event === "issue_comment") {
|
|
239
|
+
const p = GiteaIssueCommentPayloadSchema.safeParse(body);
|
|
240
|
+
if (!p.success)
|
|
241
|
+
return null;
|
|
242
|
+
const d = p.data;
|
|
243
|
+
if (d.action !== "created")
|
|
244
|
+
return null;
|
|
245
|
+
const actor = mapUser(d.sender);
|
|
246
|
+
const hint = permissionHint(d.repository, actor, d.comment.user);
|
|
247
|
+
return {
|
|
248
|
+
kind: "comment",
|
|
249
|
+
rawEvent: event,
|
|
250
|
+
repo: mapRepo(d.repository),
|
|
251
|
+
workItem: mapIssue(d.issue),
|
|
252
|
+
actor,
|
|
253
|
+
body: d.comment.body,
|
|
254
|
+
commentId: d.comment.id,
|
|
255
|
+
...(hint ? { actorPermission: hint } : {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (event === "issues") {
|
|
259
|
+
const p = GiteaIssuePayloadSchema.safeParse(body);
|
|
260
|
+
if (!p.success)
|
|
261
|
+
return null;
|
|
262
|
+
const d = p.data;
|
|
263
|
+
if (d.action === "closed") {
|
|
264
|
+
return {
|
|
265
|
+
kind: "workitem-close",
|
|
266
|
+
rawEvent: event,
|
|
267
|
+
repo: mapRepo(d.repository),
|
|
268
|
+
workItem: mapIssue(d.issue),
|
|
269
|
+
actor: mapUser(d.sender),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (d.action === "opened" || d.action === "reopened") {
|
|
273
|
+
const actor = mapUser(d.sender);
|
|
274
|
+
const hint = permissionHint(d.repository, actor, d.issue.user);
|
|
275
|
+
return {
|
|
276
|
+
kind: "workitem-open",
|
|
277
|
+
rawEvent: event,
|
|
278
|
+
repo: mapRepo(d.repository),
|
|
279
|
+
workItem: mapIssue(d.issue),
|
|
280
|
+
actor,
|
|
281
|
+
body: d.issue.body ?? "",
|
|
282
|
+
...(hint ? { actorPermission: hint } : {}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
if (event === "pull_request") {
|
|
288
|
+
const p = GiteaPullRequestPayloadSchema.safeParse(body);
|
|
289
|
+
if (!p.success)
|
|
290
|
+
return null;
|
|
291
|
+
const d = p.data;
|
|
292
|
+
return {
|
|
293
|
+
kind: "other",
|
|
294
|
+
rawEvent: event,
|
|
295
|
+
repo: mapRepo(d.repository),
|
|
296
|
+
actor: mapUser(d.sender),
|
|
297
|
+
...(d.pull_request.head?.ref ? { sourceBranch: d.pull_request.head.ref } : {}),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (event === "workflow_run") {
|
|
301
|
+
const p = GiteaWorkflowRunPayloadSchema.safeParse(body);
|
|
302
|
+
if (!p.success)
|
|
303
|
+
return null;
|
|
304
|
+
const d = p.data;
|
|
305
|
+
if (d.action !== "completed")
|
|
306
|
+
return null;
|
|
307
|
+
const run = d.workflow_run;
|
|
308
|
+
// GitHub-compatible payloads use `conclusion`; older ForgeJo uses
|
|
309
|
+
// `status`. Only a run that actually failed is actionable.
|
|
310
|
+
const failed = run.conclusion === "failure" || (run.conclusion == null && run.status === "failure");
|
|
311
|
+
if (!failed)
|
|
312
|
+
return null;
|
|
313
|
+
const prNumber = run.pull_requests?.find((pr) => pr.number !== undefined)?.number;
|
|
314
|
+
return {
|
|
315
|
+
kind: "pipeline-failed",
|
|
316
|
+
rawEvent: event,
|
|
317
|
+
repo: mapRepo(d.repository),
|
|
318
|
+
...(run.head_branch ? { sourceBranch: run.head_branch } : {}),
|
|
319
|
+
pipelineId: run.id,
|
|
320
|
+
...(run.head_sha ? { commitSha: run.head_sha } : {}),
|
|
321
|
+
...(prNumber !== undefined ? { changeRequestNumber: prNumber } : {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
// ---- Work items ----------------------------------------------------------
|
|
328
|
+
const workItems = {
|
|
329
|
+
async postComment(repoId, number, body) {
|
|
330
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments`, { method: "POST", body: JSON.stringify({ body }) }, GiteaCommentSchema, BOT);
|
|
331
|
+
return mapComment(r);
|
|
332
|
+
},
|
|
333
|
+
// Gitea/ForgeJo issue comments are flat, so a thread reply degrades to a
|
|
334
|
+
// top-level comment (still posts in the issue's comment stream).
|
|
335
|
+
async replyInThread(repoId, number, _commentId, body) {
|
|
336
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments`, { method: "POST", body: JSON.stringify({ body }) }, GiteaCommentSchema, BOT);
|
|
337
|
+
return mapComment(r);
|
|
338
|
+
},
|
|
339
|
+
async postErrorReply(repoId, number, _triggeringCommentId, body) {
|
|
340
|
+
try {
|
|
341
|
+
await giteaRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments`, { method: "POST", body: JSON.stringify({ body }) }, GiteaCommentSchema, BOT);
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
// Best-effort, matching GitLab's postErrorReply swallow-and-log.
|
|
345
|
+
void e;
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
async listComments(repoId, number) {
|
|
349
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments?${pageParams(undefined, 100)}`, { method: "GET" }, GiteaCommentSchema, BOT);
|
|
350
|
+
return rs.map(mapComment);
|
|
351
|
+
},
|
|
352
|
+
async getWorkItem(repoId, number) {
|
|
353
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}`, { method: "GET" }, GiteaIssueSchema, BOT);
|
|
354
|
+
return mapIssue(r);
|
|
355
|
+
},
|
|
356
|
+
async getWorkItemStateOrNull(repoId, number) {
|
|
357
|
+
try {
|
|
358
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}`, { method: "GET" }, GiteaIssueSchema, BOT);
|
|
359
|
+
return mapWorkItemState(r.state) === "closed" ? "closed" : "open";
|
|
360
|
+
}
|
|
361
|
+
catch (e) {
|
|
362
|
+
if (e instanceof PlatformHttpError && e.status === 404)
|
|
363
|
+
return null;
|
|
364
|
+
throw e;
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
async getMemberPermission(repoId, user) {
|
|
368
|
+
// Gitea exposes effective repo permission by username. Prefer the actor's
|
|
369
|
+
// username (webhook payloads carry it); fall back to a users-by-id lookup
|
|
370
|
+
// where the instance supports it, else deny.
|
|
371
|
+
const full = await fullNameOf(repoId);
|
|
372
|
+
const { owner, repo } = splitFullName(full);
|
|
373
|
+
let login = user.username;
|
|
374
|
+
if (!login) {
|
|
375
|
+
try {
|
|
376
|
+
const u = await giteaRequest(`/users/${user.id}`, { method: "GET" }, GiteaUserSchema, BOT);
|
|
377
|
+
login = u.login;
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
if (e instanceof PlatformHttpError && e.status === 404)
|
|
381
|
+
return "none";
|
|
382
|
+
throw e;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const p = await giteaRequest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(login)}/permission`, { method: "GET" }, GiteaPermissionsSchema, BOT);
|
|
387
|
+
return normalizeRole(p.permission ?? p.role_name, p.can_admin, p.can_write) ?? "none";
|
|
388
|
+
}
|
|
389
|
+
catch (e) {
|
|
390
|
+
// The per-user permission endpoint is restricted to site admins, repo
|
|
391
|
+
// admins, and the user themselves (see ForgeJo/Gitea
|
|
392
|
+
// routers/api/v1/repo/collaborators.go). A bot PAT therefore gets 403
|
|
393
|
+
// for anyone else — a policy response, not an infra failure. Map it to
|
|
394
|
+
// "none" (like 404) so the trigger gate can honor minRole=none instead
|
|
395
|
+
// of failing closed on the misleading lookup-failure note. 5xx still
|
|
396
|
+
// throws (fail-closed).
|
|
397
|
+
if (e instanceof PlatformHttpError && (e.status === 404 || e.status === 403))
|
|
398
|
+
return "none";
|
|
399
|
+
throw e;
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
async searchUsers(query, opts = {}) {
|
|
403
|
+
const q = query.trim();
|
|
404
|
+
if (q === "")
|
|
405
|
+
return [];
|
|
406
|
+
const limit = Math.min(Math.max(opts.perPage ?? 20, 1), 50);
|
|
407
|
+
const rs = await giteaRequestArray(`/users/search?q=${encodeURIComponent(q)}&limit=${limit}`, { method: "GET" }, GiteaUserSchema, BOT);
|
|
408
|
+
return rs.slice(0, limit).map((u) => ({ id: u.id, username: u.login, name: u.full_name || u.login }));
|
|
409
|
+
},
|
|
410
|
+
async resolveUser(username) {
|
|
411
|
+
// Gitea/ForgeJo expose /users/search?q=<login> (prefix/substring match).
|
|
412
|
+
// Filter to an exact, case-insensitive login match so "alice" never
|
|
413
|
+
// resolves to "alice-bot". An empty result is a null (not-found).
|
|
414
|
+
const target = username.trim().toLowerCase();
|
|
415
|
+
if (!target)
|
|
416
|
+
return null;
|
|
417
|
+
const rs = await giteaRequestArray(`/users/search?q=${encodeURIComponent(username.trim())}&limit=50`, { method: "GET" }, GiteaUserSchema, BOT);
|
|
418
|
+
const hit = rs.find((u) => u.login.toLowerCase() === target);
|
|
419
|
+
return hit ? { id: hit.id, username: hit.login, name: hit.full_name || hit.login } : null;
|
|
420
|
+
},
|
|
421
|
+
async listWorkItems(repoId, o = {}) {
|
|
422
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
423
|
+
if (o.state)
|
|
424
|
+
params.set("state", o.state);
|
|
425
|
+
if (o.search)
|
|
426
|
+
params.set("q", o.search);
|
|
427
|
+
if (o.labels)
|
|
428
|
+
params.set("labels", o.labels);
|
|
429
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/issues?${params.toString()}`, { method: "GET" }, GiteaIssueSchema, BOT);
|
|
430
|
+
return rs.filter((i) => !i.pull_request).map(mapIssueSummary);
|
|
431
|
+
},
|
|
432
|
+
async getWorkItemSummary(repoId, number) {
|
|
433
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}`, { method: "GET" }, GiteaIssueSchema, BOT);
|
|
434
|
+
return mapIssueSummary(r);
|
|
435
|
+
},
|
|
436
|
+
async listCommentsFull(repoId, number, o = {}) {
|
|
437
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GiteaCommentSchema, BOT);
|
|
438
|
+
return rs.map(mapComment);
|
|
439
|
+
},
|
|
440
|
+
async listLabels(repoId, o = {}) {
|
|
441
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
442
|
+
if (o.search)
|
|
443
|
+
params.set("q", o.search);
|
|
444
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/labels?${params.toString()}`, { method: "GET" }, GiteaLabelSchema, BOT);
|
|
445
|
+
return rs.map((l) => ({
|
|
446
|
+
id: l.id,
|
|
447
|
+
name: l.name,
|
|
448
|
+
...(l.color ? { color: l.color } : {}),
|
|
449
|
+
...(l.description !== undefined ? { description: l.description } : {}),
|
|
450
|
+
}));
|
|
451
|
+
},
|
|
452
|
+
async listMilestones(repoId, o = {}) {
|
|
453
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
454
|
+
if (o.state)
|
|
455
|
+
params.set("state", o.state);
|
|
456
|
+
if (o.title)
|
|
457
|
+
params.set("name", o.title);
|
|
458
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/milestones?${params.toString()}`, { method: "GET" }, GiteaMilestoneSchema, BOT);
|
|
459
|
+
return rs.map((m) => ({
|
|
460
|
+
id: m.id,
|
|
461
|
+
number: m.id,
|
|
462
|
+
title: m.title,
|
|
463
|
+
state: m.state,
|
|
464
|
+
...(m.description !== undefined ? { description: m.description } : {}),
|
|
465
|
+
...(m.due_on !== undefined ? { dueDate: m.due_on } : {}),
|
|
466
|
+
...(m.html_url ? { webUrl: m.html_url } : {}),
|
|
467
|
+
}));
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
// ---- Change requests -----------------------------------------------------
|
|
471
|
+
const changeRequests = {
|
|
472
|
+
async list(repoId, o = {}) {
|
|
473
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
474
|
+
// Neutral port states map 1:1 to Gitea/ForgeJo wire values (open/closed/all).
|
|
475
|
+
if (o.state)
|
|
476
|
+
params.set("state", o.state);
|
|
477
|
+
if (o.search)
|
|
478
|
+
params.set("q", o.search);
|
|
479
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/pulls?${params.toString()}`, { method: "GET" }, GiteaPullRequestSchema, BOT);
|
|
480
|
+
const filtered = o.sourceBranch ? rs.filter((p) => (p.head?.ref ?? "") === o.sourceBranch) : rs;
|
|
481
|
+
return filtered.map(mapPrSummary);
|
|
482
|
+
},
|
|
483
|
+
async create(repoId, input) {
|
|
484
|
+
const full = await fullNameOf(repoId);
|
|
485
|
+
const { owner, repo } = splitFullName(full);
|
|
486
|
+
const r = await giteaRequest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, {
|
|
487
|
+
method: "POST",
|
|
488
|
+
body: JSON.stringify({
|
|
489
|
+
title: input.title,
|
|
490
|
+
body: input.description,
|
|
491
|
+
head: input.sourceBranch,
|
|
492
|
+
base: input.targetBranch,
|
|
493
|
+
...(input.assignee ? { assignees: [input.assignee.username] } : {}),
|
|
494
|
+
}),
|
|
495
|
+
}, GiteaPullRequestSchema, BOT);
|
|
496
|
+
return mapPr(r);
|
|
497
|
+
},
|
|
498
|
+
async update(repoId, number, input) {
|
|
499
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/pulls/${number}`, { method: "PATCH", body: JSON.stringify({ title: input.title, body: input.description }) }, GiteaPullRequestSchema, BOT);
|
|
500
|
+
return mapPr(r);
|
|
501
|
+
},
|
|
502
|
+
async get(repoId, number) {
|
|
503
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/pulls/${number}`, { method: "GET" }, GiteaPullRequestSchema, BOT);
|
|
504
|
+
return mapPrSummary(r);
|
|
505
|
+
},
|
|
506
|
+
async listReviewDiscussions(repoId, number, o = {}) {
|
|
507
|
+
const full = await fullNameOf(repoId);
|
|
508
|
+
const reviews = await giteaRequestArray(`${repoPath(full)}/pulls/${number}/reviews?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GiteaReviewSchema, BOT).catch(() => []);
|
|
509
|
+
const comments = await giteaRequestArray(`${repoPath(full)}/pulls/${number}/reviews/0/comments?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GiteaReviewCommentSchema, BOT).catch(() => []);
|
|
510
|
+
const out = reviews.map((r) => ({
|
|
511
|
+
id: String(r.id),
|
|
512
|
+
individualNote: true,
|
|
513
|
+
notes: (r.comments ?? []).map((c) => ({
|
|
514
|
+
id: c.id,
|
|
515
|
+
body: c.body,
|
|
516
|
+
...(c.user ? { author: mapUser(c.user) } : {}),
|
|
517
|
+
...(c.created_at ? { createdAt: c.created_at } : {}),
|
|
518
|
+
...(c.path ? { position: { path: c.path, line: c.line } } : {}),
|
|
519
|
+
})),
|
|
520
|
+
}));
|
|
521
|
+
if (out.length === 0 && comments.length > 0) {
|
|
522
|
+
out.push({
|
|
523
|
+
id: "reviews",
|
|
524
|
+
individualNote: true,
|
|
525
|
+
notes: comments.map((c) => ({
|
|
526
|
+
id: c.id,
|
|
527
|
+
body: c.body,
|
|
528
|
+
...(c.user ? { author: mapUser(c.user) } : {}),
|
|
529
|
+
...(c.created_at ? { createdAt: c.created_at } : {}),
|
|
530
|
+
...(c.path ? { position: { path: c.path, line: c.line } } : {}),
|
|
531
|
+
})),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
},
|
|
536
|
+
async listNotes(repoId, number, o = {}) {
|
|
537
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GiteaCommentSchema, BOT);
|
|
538
|
+
return rs.map(mapComment);
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
// ---- Repository ----------------------------------------------------------
|
|
542
|
+
async function defaultBranchOf(repoId) {
|
|
543
|
+
const repo = await giteaRequest(`/repositories/${repoId}`, { method: "GET" }, GiteaRepositorySchema, BOT);
|
|
544
|
+
return repo.default_branch ?? "main";
|
|
545
|
+
}
|
|
546
|
+
const repository = {
|
|
547
|
+
async getRepo(repoId) {
|
|
548
|
+
const repo = await giteaRequest(`/repositories/${repoId}`, { method: "GET" }, GiteaRepositorySchema, BOT);
|
|
549
|
+
// Heal the id -> full_name cache so a rename/transfer is picked up here
|
|
550
|
+
// and every later fullNameOf() call uses the new path.
|
|
551
|
+
fullNameCache.set(repoId, repo.full_name);
|
|
552
|
+
return mapRepo(repo);
|
|
553
|
+
},
|
|
554
|
+
async branchExists(repoId, branch) {
|
|
555
|
+
const full = await fullNameOf(repoId);
|
|
556
|
+
const res = await fetch(`${apiBase()}${repoPath(full)}/branches/${encodeURIComponent(branch)}`, {
|
|
557
|
+
headers: { Authorization: `token ${config.PLATFORM_TOKEN}` },
|
|
558
|
+
});
|
|
559
|
+
if (res.status === 404)
|
|
560
|
+
return false;
|
|
561
|
+
if (!res.ok)
|
|
562
|
+
throw new PlatformHttpError(res.status, `branchExists ${branch}`, await res.text());
|
|
563
|
+
return true;
|
|
564
|
+
},
|
|
565
|
+
async firstExistingBranch(repoId, branches) {
|
|
566
|
+
for (const b of branches) {
|
|
567
|
+
if (await repository.branchExists(repoId, b))
|
|
568
|
+
return b;
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
},
|
|
572
|
+
async listBranches(repoId, o = {}) {
|
|
573
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
574
|
+
if (o.search)
|
|
575
|
+
params.set("q", o.search);
|
|
576
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/branches?${params.toString()}`, { method: "GET" }, GiteaBranchSchema, BOT);
|
|
577
|
+
return rs.map((b) => ({
|
|
578
|
+
name: b.name,
|
|
579
|
+
...(b.protected !== undefined ? { protected: b.protected } : {}),
|
|
580
|
+
}));
|
|
581
|
+
},
|
|
582
|
+
async listTree(repoId, o = {}) {
|
|
583
|
+
const full = await fullNameOf(repoId);
|
|
584
|
+
const ref = o.ref ?? (await defaultBranchOf(repoId));
|
|
585
|
+
const sub = o.path ? `/${o.path.split("/").map(encodeURIComponent).join("/")}` : "";
|
|
586
|
+
const rs = await giteaRequestArray(`${repoPath(full)}/contents${sub}?ref=${encodeURIComponent(ref)}&${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GiteaContentSchema, BOT);
|
|
587
|
+
return rs.map((e) => ({
|
|
588
|
+
id: e.sha ?? "",
|
|
589
|
+
name: e.name,
|
|
590
|
+
type: e.type === "dir" ? "tree" : "blob",
|
|
591
|
+
path: e.path,
|
|
592
|
+
}));
|
|
593
|
+
},
|
|
594
|
+
async getFile(repoId, filePath, ref, maxBytes) {
|
|
595
|
+
const full = await fullNameOf(repoId);
|
|
596
|
+
const params = new URLSearchParams();
|
|
597
|
+
if (ref)
|
|
598
|
+
params.set("ref", ref);
|
|
599
|
+
const path = `${repoPath(full)}/raw/${filePath.split("/").map(encodeURIComponent).join("/")}${params.toString() ? `?${params.toString()}` : ""}`;
|
|
600
|
+
try {
|
|
601
|
+
const { bytes, contentType } = await giteaFetchRaw(path, BOT, maxBytes);
|
|
602
|
+
return { content: bytes.toString("utf8"), truncated: false, bytes: bytes.byteLength, contentType };
|
|
603
|
+
}
|
|
604
|
+
catch (e) {
|
|
605
|
+
if (e instanceof Error && e.name === "GiteaOversizedError") {
|
|
606
|
+
return { content: "", truncated: true, bytes: maxBytes, contentType: "text/plain" };
|
|
607
|
+
}
|
|
608
|
+
throw e;
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
async listCommits(repoId, o = {}) {
|
|
612
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
613
|
+
if (o.refName)
|
|
614
|
+
params.set("sha", o.refName);
|
|
615
|
+
if (o.path)
|
|
616
|
+
params.set("path", o.path);
|
|
617
|
+
if (o.since)
|
|
618
|
+
params.set("since", o.since);
|
|
619
|
+
if (o.until)
|
|
620
|
+
params.set("until", o.until);
|
|
621
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/commits?${params.toString()}`, { method: "GET" }, GiteaCommitSchema, BOT);
|
|
622
|
+
return rs.map(mapCommit);
|
|
623
|
+
},
|
|
624
|
+
async getCommit(repoId, sha) {
|
|
625
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/git/commits/${encodeURIComponent(sha)}`, { method: "GET" }, GiteaCommitSchema, BOT);
|
|
626
|
+
return mapCommit(r);
|
|
627
|
+
},
|
|
628
|
+
async compare(repoId, from, to) {
|
|
629
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/compare/${encodeURIComponent(from)}...${encodeURIComponent(to)}`, { method: "GET" }, GiteaCompareSchema, BOT);
|
|
630
|
+
return mapCompare(r);
|
|
631
|
+
},
|
|
632
|
+
async listTags(repoId, o = {}) {
|
|
633
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
634
|
+
if (o.search)
|
|
635
|
+
params.set("q", o.search);
|
|
636
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/tags?${params.toString()}`, { method: "GET" }, GiteaTagSchema, BOT);
|
|
637
|
+
return rs.map((t) => ({
|
|
638
|
+
name: t.name,
|
|
639
|
+
...(t.message !== undefined ? { message: t.message } : {}),
|
|
640
|
+
...(t.commit?.sha ? { target: t.commit.sha } : {}),
|
|
641
|
+
}));
|
|
642
|
+
},
|
|
643
|
+
async listMembers(repoId, o = {}) {
|
|
644
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
645
|
+
// Gitea/ForgeJo recent versions honor `q`; older versions ignore it, so
|
|
646
|
+
// keep the client-side filter below as the fallback.
|
|
647
|
+
if (o.search)
|
|
648
|
+
params.set("q", o.search);
|
|
649
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/collaborators?${params.toString()}`, { method: "GET" }, GiteaCollaboratorSchema, BOT);
|
|
650
|
+
const filtered = o.search
|
|
651
|
+
? rs.filter((c) => c.login.includes(o.search) || (c.full_name ?? "").includes(o.search))
|
|
652
|
+
: rs;
|
|
653
|
+
return filtered.map((c) => {
|
|
654
|
+
const perm = normalizeRole(c.role_name, c.permissions?.admin, c.permissions?.push);
|
|
655
|
+
return {
|
|
656
|
+
id: c.id,
|
|
657
|
+
username: c.login,
|
|
658
|
+
name: c.full_name || c.login,
|
|
659
|
+
...(perm !== undefined ? { permission: perm } : {}),
|
|
660
|
+
};
|
|
661
|
+
});
|
|
662
|
+
},
|
|
663
|
+
async listReleases(repoId, o = {}) {
|
|
664
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/releases?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GiteaReleaseSchema, BOT);
|
|
665
|
+
return rs.map((r) => ({
|
|
666
|
+
tagName: r.tag_name,
|
|
667
|
+
...(r.name ? { name: r.name } : {}),
|
|
668
|
+
...(r.body != null ? { description: r.body } : {}),
|
|
669
|
+
...(r.created_at ? { createdAt: r.created_at } : {}),
|
|
670
|
+
...(r.published_at ? { releasedAt: r.published_at } : {}),
|
|
671
|
+
...(r.author ? { author: mapUser(r.author) } : {}),
|
|
672
|
+
}));
|
|
673
|
+
},
|
|
674
|
+
async listWikiPages(repoId, o = {}) {
|
|
675
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/wiki/pages?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GiteaWikiPageSchema, BOT);
|
|
676
|
+
return rs.map((w) => ({
|
|
677
|
+
slug: w.slug,
|
|
678
|
+
title: w.title,
|
|
679
|
+
...(w.updated_at ? { updatedAt: w.updated_at } : {}),
|
|
680
|
+
}));
|
|
681
|
+
},
|
|
682
|
+
async getWikiPage(repoId, slug) {
|
|
683
|
+
const w = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/wiki/page/${encodeURIComponent(slug)}`, { method: "GET" }, GiteaWikiPageSchema, BOT);
|
|
684
|
+
const content = w.content_base64 ? Buffer.from(w.content_base64, "base64").toString("utf8") : w.body;
|
|
685
|
+
return { slug: w.slug, title: w.title, ...(content ? { content } : {}) };
|
|
686
|
+
},
|
|
687
|
+
async searchProject(repoId, search, o = {}) {
|
|
688
|
+
// Only code search is available on Gitea/ForgeJo (instance-wide, scoped
|
|
689
|
+
// client-side to this repo's full_name). Any other neutral scope is a
|
|
690
|
+
// typed unsupported result rather than a silent wrong answer.
|
|
691
|
+
if (o.scope && o.scope !== "code") {
|
|
692
|
+
throw new UnsupportedError(`repository.searchProject(scope=${o.scope})`);
|
|
693
|
+
}
|
|
694
|
+
const full = await fullNameOf(repoId);
|
|
695
|
+
const params = new URLSearchParams({
|
|
696
|
+
q: search,
|
|
697
|
+
...(o.page ? { page: String(o.page) } : {}),
|
|
698
|
+
limit: String(o.perPage ?? 50),
|
|
699
|
+
});
|
|
700
|
+
// `/repos/search` returns `{ ok, data: [...] }`, not a bare array.
|
|
701
|
+
const res = await giteaRequest(`/repos/search?${params.toString()}`, { method: "GET" }, z.object({ data: z.array(z.record(z.string(), z.unknown())).optional() }).passthrough(), BOT).catch(() => ({ data: [] }));
|
|
702
|
+
const hits = [];
|
|
703
|
+
for (const h of res.data ?? []) {
|
|
704
|
+
const repo = h.repository;
|
|
705
|
+
if (repo?.full_name && repo.full_name !== full)
|
|
706
|
+
continue;
|
|
707
|
+
hits.push({
|
|
708
|
+
...(typeof h.id === "number" ? { id: h.id } : {}),
|
|
709
|
+
...(typeof h.name === "string" ? { title: h.name } : {}),
|
|
710
|
+
...(typeof h.path === "string" ? { path: h.path } : {}),
|
|
711
|
+
...(typeof h.content === "string" ? { data: h.content } : {}),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return hits;
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
// ---- CI (Actions) --------------------------------------------------------
|
|
718
|
+
const ci = {
|
|
719
|
+
async listPipelines(repoId, o = {}) {
|
|
720
|
+
if (o.scope)
|
|
721
|
+
throw new UnsupportedError(`ci.listPipelines(scope=${o.scope})`);
|
|
722
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
723
|
+
if (o.ref)
|
|
724
|
+
params.set("branch", o.ref);
|
|
725
|
+
if (o.status)
|
|
726
|
+
params.set("status", o.status);
|
|
727
|
+
if (o.username)
|
|
728
|
+
params.set("actor", o.username);
|
|
729
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/actions/runs?${params.toString()}`, { method: "GET" }, GiteaActionRunSchema, BOT);
|
|
730
|
+
return rs.map(mapRun);
|
|
731
|
+
},
|
|
732
|
+
async getPipeline(repoId, pipelineId) {
|
|
733
|
+
const r = await giteaRequest(`${repoPath(await fullNameOf(repoId))}/actions/runs/${pipelineId}`, { method: "GET" }, GiteaActionRunSchema, BOT);
|
|
734
|
+
return mapRun(r);
|
|
735
|
+
},
|
|
736
|
+
async listPipelineJobs(repoId, pipelineId, o = {}) {
|
|
737
|
+
if (o.scope)
|
|
738
|
+
throw new UnsupportedError(`ci.listPipelineJobs(scope=${o.scope})`);
|
|
739
|
+
const rs = await giteaRequestArray(`${repoPath(await fullNameOf(repoId))}/actions/runs/${pipelineId}/jobs?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GiteaActionJobSchema, BOT);
|
|
740
|
+
const jobs = rs.map((j) => ({
|
|
741
|
+
id: j.id,
|
|
742
|
+
name: j.name,
|
|
743
|
+
status: j.status,
|
|
744
|
+
...(j.html_url ? { webUrl: j.html_url } : {}),
|
|
745
|
+
...(j.started_at !== undefined ? { startedAt: j.started_at } : {}),
|
|
746
|
+
...(j.completed_at !== undefined ? { finishedAt: j.completed_at } : {}),
|
|
747
|
+
}));
|
|
748
|
+
// Neutral `outcome: "failed"` filters client-side: the Gitea/ForgeJo jobs
|
|
749
|
+
// endpoint has no failure scope (unlike GitLab).
|
|
750
|
+
return o.outcome === "failed" ? jobs.filter((j) => j.status === "failure") : jobs;
|
|
751
|
+
},
|
|
752
|
+
async getJobLog(repoId, jobId, maxBytes) {
|
|
753
|
+
const path = `${repoPath(await fullNameOf(repoId))}/actions/jobs/${jobId}/logs`;
|
|
754
|
+
const { bytes, contentType, totalBytes } = await giteaFetchRaw(path, BOT, maxBytes, "tail");
|
|
755
|
+
return tailFileContent(bytes, totalBytes, contentType || "text/plain", maxBytes);
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
// ---- Git auth ------------------------------------------------------------
|
|
759
|
+
const gitAuth = {
|
|
760
|
+
gitExtraHeader: () => gitExtraHeaderGitea(config.PLATFORM_TOKEN),
|
|
761
|
+
cloneUrl: (path) => `${config.PLATFORM_URL.replace(/\/$/, "")}/${path}.git`,
|
|
762
|
+
};
|
|
763
|
+
// ---- Attachments ---------------------------------------------------------
|
|
764
|
+
// Gitea/ForgeJo issue-comment attachments are markdown links to
|
|
765
|
+
// `/attachments/{uuid}`. The driver fetches the raw bytes for the agent.
|
|
766
|
+
const attachments = {
|
|
767
|
+
findRefs: (body) => findGiteaAttachmentRefs(body),
|
|
768
|
+
download: (_repoId, ref) => downloadGiteaAttachment(ref),
|
|
769
|
+
upload: async (repoId, number, file) => {
|
|
770
|
+
const { owner, repo } = splitFullName(await fullNameOf(repoId));
|
|
771
|
+
return uploadGiteaAttachment(owner, repo, number, file);
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
// ---- Onboarding ----------------------------------------------------------
|
|
775
|
+
const onboarding = {
|
|
776
|
+
authorizeUrl(state) {
|
|
777
|
+
const params = new URLSearchParams({
|
|
778
|
+
client_id: config.PLATFORM_OAUTH_CLIENT_ID,
|
|
779
|
+
redirect_uri: `${config.PUBLIC_URL}/oauth/callback`,
|
|
780
|
+
response_type: "code",
|
|
781
|
+
state,
|
|
782
|
+
});
|
|
783
|
+
return `${config.PLATFORM_URL.replace(/\/$/, "")}${opts.oauth.authorizePath}?${params.toString()}`;
|
|
784
|
+
},
|
|
785
|
+
async exchangeCode(code) {
|
|
786
|
+
const res = await fetch(`${config.PLATFORM_URL.replace(/\/$/, "")}${opts.oauth.tokenPath}`, {
|
|
787
|
+
method: "POST",
|
|
788
|
+
headers: { "Content-Type": "application/json" },
|
|
789
|
+
body: JSON.stringify({
|
|
790
|
+
grant_type: "authorization_code",
|
|
791
|
+
client_id: config.PLATFORM_OAUTH_CLIENT_ID,
|
|
792
|
+
client_secret: config.PLATFORM_OAUTH_CLIENT_SECRET,
|
|
793
|
+
code,
|
|
794
|
+
redirect_uri: `${config.PUBLIC_URL}/oauth/callback`,
|
|
795
|
+
}),
|
|
796
|
+
});
|
|
797
|
+
if (!res.ok)
|
|
798
|
+
throw new PlatformHttpError(res.status, "oauth token", await res.text());
|
|
799
|
+
const parsed = z.object({ access_token: z.string().min(1) }).safeParse(await res.json());
|
|
800
|
+
if (!parsed.success)
|
|
801
|
+
throw new Error("oauth token schema mismatch");
|
|
802
|
+
return { accessToken: parsed.data.access_token };
|
|
803
|
+
},
|
|
804
|
+
async currentUser(accessToken) {
|
|
805
|
+
const u = await giteaRequest(opts.oauth.userPath, { method: "GET" }, GiteaUserSchema, { kind: "bearer", token: accessToken });
|
|
806
|
+
return { id: u.id, username: u.login };
|
|
807
|
+
},
|
|
808
|
+
async listProjects(accessToken, q) {
|
|
809
|
+
const out = [];
|
|
810
|
+
let page = 1;
|
|
811
|
+
const search = q?.trim() ? `&q=${encodeURIComponent(q.trim())}` : "";
|
|
812
|
+
for (;;) {
|
|
813
|
+
// Gitea/ForgeJo `/repos/search` returns an object `{ ok, data: [...] }`,
|
|
814
|
+
// not a bare array — so use giteaRequest (which parses a single JSON
|
|
815
|
+
// body) and read `.data`, rather than giteaRequestArray.
|
|
816
|
+
const res = await giteaRequest(`/repos/search?${pageParams(page, 50)}${search}`, { method: "GET" }, z.object({ data: z.array(GiteaRepositorySchema).optional() }).passthrough(), { kind: "bearer", token: accessToken });
|
|
817
|
+
const data = res.data ?? [];
|
|
818
|
+
if (data.length === 0)
|
|
819
|
+
break;
|
|
820
|
+
for (const r of data)
|
|
821
|
+
out.push({ id: r.id, path: r.full_name, webUrl: r.html_url ?? "" });
|
|
822
|
+
if (data.length < 50)
|
|
823
|
+
break;
|
|
824
|
+
page += 1;
|
|
825
|
+
}
|
|
826
|
+
return out;
|
|
827
|
+
},
|
|
828
|
+
async addBot(accessToken, project) {
|
|
829
|
+
const { owner, repo } = splitFullName(project.path);
|
|
830
|
+
await giteaVoid(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(config.BOT_USERNAME)}`, { method: "PUT", body: JSON.stringify({ permission: "write" }) }, { kind: "bearer", token: accessToken }, { okStatuses: [409, 422] });
|
|
831
|
+
},
|
|
832
|
+
async removeBot(accessToken, project) {
|
|
833
|
+
const { owner, repo } = splitFullName(project.path);
|
|
834
|
+
await giteaVoid(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(config.BOT_USERNAME)}`, { method: "DELETE" }, { kind: "bearer", token: accessToken }, { okStatuses: [404] });
|
|
835
|
+
},
|
|
836
|
+
async findOrCreateWebhook(accessToken, project, url, secret) {
|
|
837
|
+
const { owner, repo } = splitFullName(project.path);
|
|
838
|
+
const base = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks`;
|
|
839
|
+
const hooks = await giteaRequestArray(base, { method: "GET" }, GiteaWebhookSchema, { kind: "bearer", token: accessToken });
|
|
840
|
+
const existing = hooks.find((h) => h.url === url);
|
|
841
|
+
if (existing)
|
|
842
|
+
return { id: existing.id, created: false };
|
|
843
|
+
const created = await giteaRequest(base, {
|
|
844
|
+
method: "POST",
|
|
845
|
+
body: JSON.stringify({
|
|
846
|
+
type: opts.webhookType,
|
|
847
|
+
active: true,
|
|
848
|
+
events: ["issues", "issue_comment", "pull_request", "workflow_run"],
|
|
849
|
+
// The secret is honored only at creation time: ForgeJo/Gitea's
|
|
850
|
+
// PATCH (editHook) silently drops config.secret, so it must be
|
|
851
|
+
// present here or deliveries carry an empty signature forever.
|
|
852
|
+
config: { url, content_type: "json", secret },
|
|
853
|
+
}),
|
|
854
|
+
}, GiteaWebhookSchema, { kind: "bearer", token: accessToken });
|
|
855
|
+
return { id: created.id, created: true };
|
|
856
|
+
},
|
|
857
|
+
async setWebhookSecret(_accessToken, project, webhookId) {
|
|
858
|
+
// ForgeJo/Gitea's EditHook API ignores config.secret (verified in
|
|
859
|
+
// routers/api/v1/utils/hook.go editHook — it is neither read nor
|
|
860
|
+
// written), and the API never returns the stored secret, so the value
|
|
861
|
+
// cannot be rotated or repaired after creation. The secret is set at
|
|
862
|
+
// create time by findOrCreateWebhook; an already-onboarded hook with an
|
|
863
|
+
// empty secret must be fixed in the forge web UI (Settings -> Webhooks
|
|
864
|
+
// -> Secret) — see docs/platforms.md.
|
|
865
|
+
const { owner, repo } = splitFullName(project.path);
|
|
866
|
+
log.warn("gitea/forgejo cannot set a webhook secret after creation; ignoring", {
|
|
867
|
+
project: `${owner}/${repo}`,
|
|
868
|
+
webhookId,
|
|
869
|
+
hint: "recreate the hook or set the secret in the forge web UI",
|
|
870
|
+
});
|
|
871
|
+
},
|
|
872
|
+
async ensureCiFailureEvents(accessToken, project, webhookId) {
|
|
873
|
+
const { owner, repo } = splitFullName(project.path);
|
|
874
|
+
const base = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks`;
|
|
875
|
+
const hooks = await giteaRequestArray(base, { method: "GET" }, GiteaWebhookSchema, { kind: "bearer", token: accessToken });
|
|
876
|
+
const existing = hooks.find((h) => h.id === webhookId);
|
|
877
|
+
const current = existing?.events ?? ["issues", "issue_comment", "pull_request"];
|
|
878
|
+
if (current.includes("workflow_run"))
|
|
879
|
+
return;
|
|
880
|
+
// PATCH merges our event list; unlike the secret, event names are honored
|
|
881
|
+
// on edit. Preserve the hook's existing events and append workflow_run.
|
|
882
|
+
await giteaVoid(`${base}/${webhookId}`, {
|
|
883
|
+
method: "PATCH",
|
|
884
|
+
body: JSON.stringify({ events: [...new Set([...current, "workflow_run"])] }),
|
|
885
|
+
}, { kind: "bearer", token: accessToken });
|
|
886
|
+
},
|
|
887
|
+
async deleteWebhook(accessToken, project, webhookId) {
|
|
888
|
+
const { owner, repo } = splitFullName(project.path);
|
|
889
|
+
await giteaVoid(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks/${webhookId}`, { method: "DELETE" }, { kind: "bearer", token: accessToken }, { okStatuses: [404] });
|
|
890
|
+
},
|
|
891
|
+
async testWebhook(accessToken, project, webhookId) {
|
|
892
|
+
const { owner, repo } = splitFullName(project.path);
|
|
893
|
+
try {
|
|
894
|
+
await giteaVoid(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks/${webhookId}/tests`, { method: "POST" }, { kind: "bearer", token: accessToken });
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
// Test delivery is best-effort, matching GitLab's swallow.
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
// ---- Links -------------------------------------------------------------
|
|
902
|
+
const links = {
|
|
903
|
+
workItemUrl(projectPath, number) {
|
|
904
|
+
if (!projectPath || !Number.isInteger(number) || number <= 0)
|
|
905
|
+
return "";
|
|
906
|
+
return `${config.PLATFORM_URL.replace(/\/$/, "")}/${projectPath}/issues/${number}`;
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
return {
|
|
910
|
+
name: platform,
|
|
911
|
+
ingress,
|
|
912
|
+
workItems,
|
|
913
|
+
changeRequests,
|
|
914
|
+
repository,
|
|
915
|
+
ci,
|
|
916
|
+
gitAuth,
|
|
917
|
+
attachments,
|
|
918
|
+
onboarding,
|
|
919
|
+
links,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function mapRun(r) {
|
|
923
|
+
return {
|
|
924
|
+
id: r.id,
|
|
925
|
+
status: r.status,
|
|
926
|
+
...(r.head_branch ? { ref: r.head_branch } : {}),
|
|
927
|
+
...(r.head_sha ? { sha: r.head_sha } : {}),
|
|
928
|
+
...(r.title ? { name: r.title } : {}),
|
|
929
|
+
...(r.html_url ? { webUrl: r.html_url } : {}),
|
|
930
|
+
...(r.created_at ? { createdAt: r.created_at } : {}),
|
|
931
|
+
...(r.updated_at ? { updatedAt: r.updated_at } : {}),
|
|
932
|
+
...(r.duration !== undefined ? { duration: r.duration } : {}),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
//# sourceMappingURL=base.js.map
|