@wascartech/devbot 0.0.0-stage → 2.1.1
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 +150 -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 +438 -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 +158 -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,936 @@
|
|
|
1
|
+
// GitHub driver (github.com). Implements the Platform port against the GitHub
|
|
2
|
+
// REST API (`/repos/{owner}/{repo}/…`). Unlike GitLab and ForgeJo/Gitea,
|
|
3
|
+
// GitHub addresses repos by `owner/name`; the driver keeps a numeric-id →
|
|
4
|
+
// full_name cache because the port (and MCP/runner) only carry ids.
|
|
5
|
+
//
|
|
6
|
+
// GitHub Enterprise Server is out of scope; GITHUB_API_URL defaults to
|
|
7
|
+
// https://api.github.com. Wiki pages have no REST API (typed UnsupportedError).
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
10
|
+
import { config } from "../../config.js";
|
|
11
|
+
import { PlatformHttpError, UnsupportedError, tailFileContent } from "../types.js";
|
|
12
|
+
import { gitExtraHeaderGithub } from "../git-auth.js";
|
|
13
|
+
import { findGithubAttachmentRefs, downloadGithubAttachment } from "./attachments.js";
|
|
14
|
+
import { zipText } from "./zip.js";
|
|
15
|
+
import { GithubActionRunSchema, GithubBranchSchema, GithubCollaboratorSchema, GithubCommentSchema, GithubCommitListSchema, GithubCompareSchema, GithubContentSchema, GithubIssueCommentPayloadSchema, GithubIssuePayloadSchema, GithubIssueSchema, GithubLabelSchema, GithubMilestoneSchema, GithubPermissionSchema, GithubPullRequestPayloadSchema, GithubPullRequestSchema, GithubReleaseSchema, GithubRepositorySchema, GithubReviewCommentSchema, GithubReviewSchema, GithubSearchCodeSchema, GithubTagSchema, GithubUserSchema, GithubWebhookSchema, GithubWorkflowJobsSchema, GithubWorkflowRunPayloadSchema, GithubWorkflowRunsSchema, } from "./schemas.js";
|
|
16
|
+
import { githubFetchRaw, githubRequest, githubRequestArray, githubVoid, pageParams, GithubOversizedError, } from "./http.js";
|
|
17
|
+
const BOT = { kind: "bot" };
|
|
18
|
+
// Safety cap for the raw Actions log ZIP before inflation. The meaningful tail
|
|
19
|
+
// is taken after inflating; this only stops an unbounded archive download.
|
|
20
|
+
const GITHUB_LOG_ARCHIVE_MAX_BYTES = 50_000_000;
|
|
21
|
+
// ---- Mappers ---------------------------------------------------------------
|
|
22
|
+
function splitFullName(fullName) {
|
|
23
|
+
const idx = fullName.indexOf("/");
|
|
24
|
+
if (idx === -1)
|
|
25
|
+
return { owner: "", repo: fullName };
|
|
26
|
+
return { owner: fullName.slice(0, idx), repo: fullName.slice(idx + 1) };
|
|
27
|
+
}
|
|
28
|
+
function mapRepo(r) {
|
|
29
|
+
return {
|
|
30
|
+
id: r.id,
|
|
31
|
+
path: r.full_name,
|
|
32
|
+
webUrl: r.html_url ?? "",
|
|
33
|
+
cloneUrl: r.clone_url || `${config.PLATFORM_URL.replace(/\/$/, "")}/${r.full_name}.git`,
|
|
34
|
+
defaultBranch: r.default_branch ?? null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function mapUser(u) {
|
|
38
|
+
if (!u)
|
|
39
|
+
return undefined;
|
|
40
|
+
return { id: u.id, username: u.login, name: u.name || u.login };
|
|
41
|
+
}
|
|
42
|
+
// GitHub issue states are already "open"/"closed"; the port uses the same.
|
|
43
|
+
function mapWorkItemState(state) {
|
|
44
|
+
return state;
|
|
45
|
+
}
|
|
46
|
+
function mapIssue(i) {
|
|
47
|
+
const assignees = i.assignees && i.assignees.length > 0 ? i.assignees : i.assignee ? [i.assignee] : [];
|
|
48
|
+
return {
|
|
49
|
+
id: i.id,
|
|
50
|
+
number: i.number,
|
|
51
|
+
title: i.title,
|
|
52
|
+
description: i.body ?? "",
|
|
53
|
+
state: mapWorkItemState(i.state),
|
|
54
|
+
webUrl: i.html_url ?? "",
|
|
55
|
+
...(i.user ? { author: { id: i.user.id } } : {}),
|
|
56
|
+
...(assignees.length > 0 ? { assignees: assignees.map((a) => ({ id: a.id })) } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function labelName(l) {
|
|
60
|
+
return typeof l === "string" ? l : l.name;
|
|
61
|
+
}
|
|
62
|
+
function mapIssueSummary(i) {
|
|
63
|
+
const who = mapUser(i.user);
|
|
64
|
+
const labels = i.labels?.map(labelName).filter((n) => !!n);
|
|
65
|
+
return {
|
|
66
|
+
id: i.id,
|
|
67
|
+
number: i.number,
|
|
68
|
+
title: i.title,
|
|
69
|
+
state: mapWorkItemState(i.state),
|
|
70
|
+
webUrl: i.html_url ?? "",
|
|
71
|
+
...(i.body != null ? { description: i.body } : {}),
|
|
72
|
+
...(labels && labels.length > 0 ? { labels } : {}),
|
|
73
|
+
...(i.assignees
|
|
74
|
+
? { assignees: i.assignees.map((a) => ({ id: a.id, username: a.login, name: a.name || a.login })) }
|
|
75
|
+
: {}),
|
|
76
|
+
...(who ? { author: who } : {}),
|
|
77
|
+
...(i.created_at ? { createdAt: i.created_at } : {}),
|
|
78
|
+
...(i.due_on !== undefined ? { dueDate: i.due_on } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function mapComment(c) {
|
|
82
|
+
const who = mapUser(c.user);
|
|
83
|
+
return {
|
|
84
|
+
id: c.id,
|
|
85
|
+
body: c.body ?? "",
|
|
86
|
+
...(who ? { author: who } : {}),
|
|
87
|
+
...(c.created_at ? { createdAt: c.created_at } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function mapPrState(pr) {
|
|
91
|
+
if (pr.merged)
|
|
92
|
+
return "merged";
|
|
93
|
+
if (pr.state === "open")
|
|
94
|
+
return "open";
|
|
95
|
+
if (pr.state === "closed")
|
|
96
|
+
return "closed";
|
|
97
|
+
return pr.state;
|
|
98
|
+
}
|
|
99
|
+
function mapPr(pr) {
|
|
100
|
+
return {
|
|
101
|
+
id: pr.id,
|
|
102
|
+
number: pr.number,
|
|
103
|
+
title: pr.title,
|
|
104
|
+
state: mapPrState(pr),
|
|
105
|
+
sourceBranch: pr.head?.ref ?? "",
|
|
106
|
+
targetBranch: pr.base?.ref ?? "",
|
|
107
|
+
webUrl: pr.html_url ?? "",
|
|
108
|
+
...(pr.draft !== undefined ? { draft: pr.draft } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function mapPrSummary(pr) {
|
|
112
|
+
const who = mapUser(pr.user);
|
|
113
|
+
return {
|
|
114
|
+
id: pr.id,
|
|
115
|
+
number: pr.number,
|
|
116
|
+
title: pr.title,
|
|
117
|
+
state: mapPrState(pr),
|
|
118
|
+
webUrl: pr.html_url ?? "",
|
|
119
|
+
...(pr.body != null ? { description: pr.body } : {}),
|
|
120
|
+
...(pr.head ? { sourceBranch: pr.head.ref } : {}),
|
|
121
|
+
...(pr.base ? { targetBranch: pr.base.ref } : {}),
|
|
122
|
+
...(who ? { author: who } : {}),
|
|
123
|
+
...(pr.created_at ? { createdAt: pr.created_at } : {}),
|
|
124
|
+
...(pr.updated_at ? { updatedAt: pr.updated_at } : {}),
|
|
125
|
+
...(pr.merged_at !== undefined ? { mergedAt: pr.merged_at } : {}),
|
|
126
|
+
...(pr.draft !== undefined ? { draft: pr.draft } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function mapCommit(c) {
|
|
130
|
+
const msg = c.commit?.message ?? "";
|
|
131
|
+
const [title, ...rest] = msg.split("\n");
|
|
132
|
+
return {
|
|
133
|
+
id: c.sha,
|
|
134
|
+
title: (title ?? "").trim(),
|
|
135
|
+
...(rest.length ? { message: msg } : {}),
|
|
136
|
+
...(c.commit?.author?.name ? { authorName: c.commit.author.name } : {}),
|
|
137
|
+
...(c.commit?.author?.date ? { authoredDate: c.commit.author.date } : {}),
|
|
138
|
+
...(c.html_url ? { webUrl: c.html_url } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function mapCompare(c) {
|
|
142
|
+
return {
|
|
143
|
+
commits: (c.commits ?? []).map((x) => {
|
|
144
|
+
const title = x.commit?.message ? x.commit.message.split("\n")[0]?.trim() : undefined;
|
|
145
|
+
return {
|
|
146
|
+
id: x.sha,
|
|
147
|
+
...(title ? { title } : {}),
|
|
148
|
+
...(x.commit?.author?.name ? { authorName: x.commit.author.name } : {}),
|
|
149
|
+
...(x.commit?.author?.date ? { createdAt: x.commit.author.date } : {}),
|
|
150
|
+
};
|
|
151
|
+
}),
|
|
152
|
+
...(c.files
|
|
153
|
+
? {
|
|
154
|
+
diffs: c.files.map((f) => ({
|
|
155
|
+
oldPath: f.previous_filename ?? f.filename,
|
|
156
|
+
newPath: f.filename,
|
|
157
|
+
...(f.patch ? { diff: f.patch } : {}),
|
|
158
|
+
...(typeof f.status === "string"
|
|
159
|
+
? {
|
|
160
|
+
newFile: f.status === "added",
|
|
161
|
+
deletedFile: f.status === "removed",
|
|
162
|
+
renamedFile: f.status === "renamed",
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
165
|
+
})),
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// GitHub's permission values: admin | maintain | write | triage | read | none.
|
|
171
|
+
function normalizePermission(perm, role) {
|
|
172
|
+
const p = (perm ?? "").toLowerCase();
|
|
173
|
+
const r = (role ?? "").toLowerCase();
|
|
174
|
+
const v = p || r;
|
|
175
|
+
if (v === "admin")
|
|
176
|
+
return "admin";
|
|
177
|
+
if (v === "maintain")
|
|
178
|
+
return "write";
|
|
179
|
+
if (v === "write" || v === "push")
|
|
180
|
+
return "write";
|
|
181
|
+
if (v === "triage")
|
|
182
|
+
return "read";
|
|
183
|
+
if (v === "read" || v === "pull")
|
|
184
|
+
return "read";
|
|
185
|
+
if (v === "none")
|
|
186
|
+
return "none";
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
function normalizeRoleName(role) {
|
|
190
|
+
const r = (role ?? "").toLowerCase();
|
|
191
|
+
if (r === "admin")
|
|
192
|
+
return "admin";
|
|
193
|
+
if (r === "maintain" || r === "write")
|
|
194
|
+
return "write";
|
|
195
|
+
if (r === "triage" || r === "read")
|
|
196
|
+
return "read";
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
// GitHub issue `labels` on collaborators etc. can be objects; map to names.
|
|
200
|
+
function normalizeChangeState(state) {
|
|
201
|
+
if (!state)
|
|
202
|
+
return undefined;
|
|
203
|
+
if (state === "opened")
|
|
204
|
+
return "open";
|
|
205
|
+
return state;
|
|
206
|
+
}
|
|
207
|
+
// ---- Driver ----------------------------------------------------------------
|
|
208
|
+
export function createGithubPlatform() {
|
|
209
|
+
const platform = "github";
|
|
210
|
+
const apiBase = () => config.GITHUB_API_URL.replace(/\/$/, "");
|
|
211
|
+
function repoPath(fullName) {
|
|
212
|
+
const { owner, repo } = splitFullName(fullName);
|
|
213
|
+
return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
214
|
+
}
|
|
215
|
+
// Numeric repo id -> full_name cache. The MCP + runner only carry ids.
|
|
216
|
+
const fullNameCache = new Map();
|
|
217
|
+
async function fullNameOf(repoId) {
|
|
218
|
+
const hit = fullNameCache.get(repoId);
|
|
219
|
+
if (hit)
|
|
220
|
+
return hit;
|
|
221
|
+
const repo = await githubRequest(`/repositories/${repoId}`, { method: "GET" }, GithubRepositorySchema, BOT);
|
|
222
|
+
fullNameCache.set(repoId, repo.full_name);
|
|
223
|
+
return repo.full_name;
|
|
224
|
+
}
|
|
225
|
+
// ---- Ingress -------------------------------------------------------------
|
|
226
|
+
const ingress = {
|
|
227
|
+
platform,
|
|
228
|
+
eventName(headers) {
|
|
229
|
+
const v = headers["x-github-event"];
|
|
230
|
+
return Array.isArray(v) ? v[0] : v;
|
|
231
|
+
},
|
|
232
|
+
projectIdFromBody(body) {
|
|
233
|
+
const raw = body?.repository?.id;
|
|
234
|
+
return typeof raw === "number" ? raw : undefined;
|
|
235
|
+
},
|
|
236
|
+
deliveryId(headers) {
|
|
237
|
+
const v = headers["x-github-delivery"];
|
|
238
|
+
return Array.isArray(v) ? v[0] : v;
|
|
239
|
+
},
|
|
240
|
+
verifySignature(secret, headers, rawBody) {
|
|
241
|
+
const v = headers["x-hub-signature-256"];
|
|
242
|
+
const sig = Array.isArray(v) ? v[0] : v;
|
|
243
|
+
if (!sig || !secret)
|
|
244
|
+
return false;
|
|
245
|
+
const digest = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
246
|
+
const got = Buffer.from(sig, "utf8");
|
|
247
|
+
const exp = Buffer.from(digest, "utf8");
|
|
248
|
+
if (got.length !== exp.length)
|
|
249
|
+
return false;
|
|
250
|
+
return timingSafeEqual(got, exp);
|
|
251
|
+
},
|
|
252
|
+
generateWebhookSecret() {
|
|
253
|
+
return randomBytes(32).toString("hex");
|
|
254
|
+
},
|
|
255
|
+
parseWebhook(headers, body) {
|
|
256
|
+
const event = this.eventName(headers);
|
|
257
|
+
if (event === "issue_comment") {
|
|
258
|
+
const p = GithubIssueCommentPayloadSchema.safeParse(body);
|
|
259
|
+
if (!p.success)
|
|
260
|
+
return null;
|
|
261
|
+
const d = p.data;
|
|
262
|
+
if (d.action !== "created")
|
|
263
|
+
return null;
|
|
264
|
+
// Issues endpoints consider PRs issues; a PR comment is a review
|
|
265
|
+
// conversation, not a work-item mention — ignore it here.
|
|
266
|
+
if (d.issue.pull_request)
|
|
267
|
+
return null;
|
|
268
|
+
return {
|
|
269
|
+
kind: "comment",
|
|
270
|
+
rawEvent: event,
|
|
271
|
+
repo: mapRepo(d.repository),
|
|
272
|
+
workItem: mapIssue(d.issue),
|
|
273
|
+
actor: mapUser(d.sender),
|
|
274
|
+
body: d.comment.body ?? "",
|
|
275
|
+
commentId: d.comment.id,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (event === "issues") {
|
|
279
|
+
const p = GithubIssuePayloadSchema.safeParse(body);
|
|
280
|
+
if (!p.success)
|
|
281
|
+
return null;
|
|
282
|
+
const d = p.data;
|
|
283
|
+
if (d.issue.pull_request)
|
|
284
|
+
return null;
|
|
285
|
+
if (d.action === "closed") {
|
|
286
|
+
return {
|
|
287
|
+
kind: "workitem-close",
|
|
288
|
+
rawEvent: event,
|
|
289
|
+
repo: mapRepo(d.repository),
|
|
290
|
+
workItem: mapIssue(d.issue),
|
|
291
|
+
actor: mapUser(d.sender),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (d.action === "opened" || d.action === "reopened") {
|
|
295
|
+
return {
|
|
296
|
+
kind: "workitem-open",
|
|
297
|
+
rawEvent: event,
|
|
298
|
+
repo: mapRepo(d.repository),
|
|
299
|
+
workItem: mapIssue(d.issue),
|
|
300
|
+
actor: mapUser(d.sender),
|
|
301
|
+
body: d.issue.body ?? "",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (event === "pull_request") {
|
|
307
|
+
const p = GithubPullRequestPayloadSchema.safeParse(body);
|
|
308
|
+
if (!p.success)
|
|
309
|
+
return null;
|
|
310
|
+
const d = p.data;
|
|
311
|
+
return {
|
|
312
|
+
kind: "other",
|
|
313
|
+
rawEvent: event,
|
|
314
|
+
repo: mapRepo(d.repository),
|
|
315
|
+
actor: mapUser(d.sender),
|
|
316
|
+
...(d.pull_request.head?.ref ? { sourceBranch: d.pull_request.head.ref } : {}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (event === "workflow_run") {
|
|
320
|
+
const p = GithubWorkflowRunPayloadSchema.safeParse(body);
|
|
321
|
+
if (!p.success)
|
|
322
|
+
return null;
|
|
323
|
+
const d = p.data;
|
|
324
|
+
if (d.action !== "completed" || d.workflow_run.conclusion !== "failure")
|
|
325
|
+
return null;
|
|
326
|
+
const run = d.workflow_run;
|
|
327
|
+
const prNumber = run.pull_requests?.find((pr) => pr.number !== undefined)?.number;
|
|
328
|
+
return {
|
|
329
|
+
kind: "pipeline-failed",
|
|
330
|
+
rawEvent: event,
|
|
331
|
+
repo: mapRepo(d.repository),
|
|
332
|
+
...(run.head_branch ? { sourceBranch: run.head_branch } : {}),
|
|
333
|
+
pipelineId: run.id,
|
|
334
|
+
...(run.head_sha ? { commitSha: run.head_sha } : {}),
|
|
335
|
+
...(prNumber !== undefined ? { changeRequestNumber: prNumber } : {}),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
// ---- Work items ----------------------------------------------------------
|
|
342
|
+
async function issueComments(repoId, number, o = {}) {
|
|
343
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubCommentSchema, BOT);
|
|
344
|
+
return rs.map(mapComment);
|
|
345
|
+
}
|
|
346
|
+
const workItems = {
|
|
347
|
+
async postComment(repoId, number, body) {
|
|
348
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments`, { method: "POST", body: JSON.stringify({ body }) }, GithubCommentSchema, BOT);
|
|
349
|
+
return mapComment(r);
|
|
350
|
+
},
|
|
351
|
+
// GitHub issue comments are flat, so a thread reply degrades to a
|
|
352
|
+
// top-level comment (still posts in the issue's comment stream).
|
|
353
|
+
async replyInThread(repoId, number, _commentId, body) {
|
|
354
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments`, { method: "POST", body: JSON.stringify({ body }) }, GithubCommentSchema, BOT);
|
|
355
|
+
return mapComment(r);
|
|
356
|
+
},
|
|
357
|
+
async postErrorReply(repoId, number, _triggeringCommentId, body) {
|
|
358
|
+
try {
|
|
359
|
+
await githubRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}/comments`, { method: "POST", body: JSON.stringify({ body }) }, GithubCommentSchema, BOT);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// Best-effort, matching GitLab's postErrorReply swallow-and-log.
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
async listComments(repoId, number) {
|
|
366
|
+
return issueComments(repoId, number);
|
|
367
|
+
},
|
|
368
|
+
async getWorkItem(repoId, number) {
|
|
369
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}`, { method: "GET" }, GithubIssueSchema, BOT);
|
|
370
|
+
return mapIssue(r);
|
|
371
|
+
},
|
|
372
|
+
async getWorkItemStateOrNull(repoId, number) {
|
|
373
|
+
try {
|
|
374
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}`, { method: "GET" }, GithubIssueSchema, BOT);
|
|
375
|
+
return mapIssue(r).state === "closed" ? "closed" : "open";
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
if (e instanceof PlatformHttpError && e.status === 404)
|
|
379
|
+
return null;
|
|
380
|
+
throw e;
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
async getMemberPermission(repoId, user) {
|
|
384
|
+
const full = await fullNameOf(repoId);
|
|
385
|
+
const { owner, repo } = splitFullName(full);
|
|
386
|
+
let login = user.username;
|
|
387
|
+
if (!login) {
|
|
388
|
+
try {
|
|
389
|
+
const u = await githubRequest(`/user/${user.id}`, { method: "GET" }, GithubUserSchema, BOT);
|
|
390
|
+
login = u.login;
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
if (e instanceof PlatformHttpError && e.status === 404)
|
|
394
|
+
return "none";
|
|
395
|
+
throw e;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const p = await githubRequest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(login)}/permission`, { method: "GET" }, GithubPermissionSchema, BOT);
|
|
400
|
+
return normalizePermission(p.permission, p.role_name) ?? "none";
|
|
401
|
+
}
|
|
402
|
+
catch (e) {
|
|
403
|
+
if (e instanceof PlatformHttpError && e.status === 404)
|
|
404
|
+
return "none";
|
|
405
|
+
throw e;
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
async searchUsers(query, opts = {}) {
|
|
409
|
+
const q = query.trim();
|
|
410
|
+
if (q === "")
|
|
411
|
+
return [];
|
|
412
|
+
const perPage = Math.min(Math.max(opts.perPage ?? 20, 1), 100);
|
|
413
|
+
const rs = await githubRequestArray(`/search/users?q=${encodeURIComponent(q)}&per_page=${perPage}`, { method: "GET" }, GithubUserSchema, BOT);
|
|
414
|
+
return rs.slice(0, perPage).map((u) => ({ id: u.id, username: u.login, name: u.name || u.login }));
|
|
415
|
+
},
|
|
416
|
+
async resolveUser(username) {
|
|
417
|
+
// GET /users/{login} is an exact lookup (GitHub logins are
|
|
418
|
+
// case-insensitive). 404 → null (not-found).
|
|
419
|
+
const login = username.trim();
|
|
420
|
+
if (!login)
|
|
421
|
+
return null;
|
|
422
|
+
try {
|
|
423
|
+
const u = await githubRequest(`/users/${encodeURIComponent(login)}`, { method: "GET" }, GithubUserSchema, BOT);
|
|
424
|
+
return { id: u.id, username: u.login, name: u.name || u.login };
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
if (e instanceof PlatformHttpError && e.status === 404)
|
|
428
|
+
return null;
|
|
429
|
+
throw e;
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
async listWorkItems(repoId, o = {}) {
|
|
433
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
434
|
+
params.set("state", normalizeChangeState(o.state) ?? "open");
|
|
435
|
+
if (o.labels)
|
|
436
|
+
params.set("labels", o.labels);
|
|
437
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/issues?${params.toString()}`, { method: "GET" }, GithubIssueSchema, BOT);
|
|
438
|
+
const filtered = rs.filter((i) => !i.pull_request);
|
|
439
|
+
const searched = o.search
|
|
440
|
+
? filtered.filter((i) => i.title.toLowerCase().includes(o.search.toLowerCase()))
|
|
441
|
+
: filtered;
|
|
442
|
+
return searched.map(mapIssueSummary);
|
|
443
|
+
},
|
|
444
|
+
async getWorkItemSummary(repoId, number) {
|
|
445
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/issues/${number}`, { method: "GET" }, GithubIssueSchema, BOT);
|
|
446
|
+
return mapIssueSummary(r);
|
|
447
|
+
},
|
|
448
|
+
async listCommentsFull(repoId, number, o = {}) {
|
|
449
|
+
return issueComments(repoId, number, o);
|
|
450
|
+
},
|
|
451
|
+
async listLabels(repoId, o = {}) {
|
|
452
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/labels?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubLabelSchema, BOT);
|
|
453
|
+
const searched = o.search ? rs.filter((l) => l.name.toLowerCase().includes(o.search.toLowerCase())) : rs;
|
|
454
|
+
return searched.map((l) => ({
|
|
455
|
+
id: l.id,
|
|
456
|
+
name: l.name,
|
|
457
|
+
...(l.color ? { color: l.color } : {}),
|
|
458
|
+
...(l.description !== undefined ? { description: l.description } : {}),
|
|
459
|
+
}));
|
|
460
|
+
},
|
|
461
|
+
async listMilestones(repoId, o = {}) {
|
|
462
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
463
|
+
if (o.state)
|
|
464
|
+
params.set("state", normalizeChangeState(o.state) ?? "open");
|
|
465
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/milestones?${params.toString()}`, { method: "GET" }, GithubMilestoneSchema, BOT);
|
|
466
|
+
const titled = o.title ? rs.filter((m) => m.title === o.title) : rs;
|
|
467
|
+
return titled.map((m) => ({
|
|
468
|
+
id: m.id,
|
|
469
|
+
number: m.number,
|
|
470
|
+
title: m.title,
|
|
471
|
+
state: m.state,
|
|
472
|
+
...(m.description !== undefined ? { description: m.description } : {}),
|
|
473
|
+
...(m.due_on !== undefined ? { dueDate: m.due_on } : {}),
|
|
474
|
+
...(m.html_url ? { webUrl: m.html_url } : {}),
|
|
475
|
+
}));
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
// ---- Change requests -----------------------------------------------------
|
|
479
|
+
const changeRequests = {
|
|
480
|
+
async list(repoId, o = {}) {
|
|
481
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
482
|
+
// GitHub PR state is open|closed|all; "merged" is a closed PR with a
|
|
483
|
+
// merged_at timestamp, handled by the client-side filter below.
|
|
484
|
+
const state = o.state === "merged" ? "closed" : normalizeChangeState(o.state) ?? "open";
|
|
485
|
+
params.set("state", state);
|
|
486
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/pulls?${params.toString()}`, { method: "GET" }, GithubPullRequestSchema, BOT);
|
|
487
|
+
let filtered = o.sourceBranch ? rs.filter((p) => (p.head?.ref ?? "") === o.sourceBranch) : rs;
|
|
488
|
+
if (o.state === "merged")
|
|
489
|
+
filtered = filtered.filter((p) => p.merged === true);
|
|
490
|
+
const searched = o.search
|
|
491
|
+
? filtered.filter((p) => p.title.toLowerCase().includes(o.search.toLowerCase()))
|
|
492
|
+
: filtered;
|
|
493
|
+
return searched.map(mapPrSummary);
|
|
494
|
+
},
|
|
495
|
+
async create(repoId, input) {
|
|
496
|
+
const full = await fullNameOf(repoId);
|
|
497
|
+
const { owner, repo } = splitFullName(full);
|
|
498
|
+
const r = await githubRequest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, {
|
|
499
|
+
method: "POST",
|
|
500
|
+
body: JSON.stringify({
|
|
501
|
+
title: input.title,
|
|
502
|
+
body: input.description,
|
|
503
|
+
head: input.sourceBranch,
|
|
504
|
+
base: input.targetBranch,
|
|
505
|
+
...(input.assignee ? { assignees: [input.assignee.username] } : {}),
|
|
506
|
+
}),
|
|
507
|
+
}, GithubPullRequestSchema, BOT);
|
|
508
|
+
return mapPr(r);
|
|
509
|
+
},
|
|
510
|
+
async update(repoId, number, input) {
|
|
511
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/pulls/${number}`, { method: "PATCH", body: JSON.stringify({ title: input.title, body: input.description }) }, GithubPullRequestSchema, BOT);
|
|
512
|
+
return mapPr(r);
|
|
513
|
+
},
|
|
514
|
+
async get(repoId, number) {
|
|
515
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/pulls/${number}`, { method: "GET" }, GithubPullRequestSchema, BOT);
|
|
516
|
+
return mapPrSummary(r);
|
|
517
|
+
},
|
|
518
|
+
async listReviewDiscussions(repoId, number, o = {}) {
|
|
519
|
+
const full = await fullNameOf(repoId);
|
|
520
|
+
const reviews = await githubRequestArray(`${repoPath(full)}/pulls/${number}/reviews?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubReviewSchema, BOT).catch(() => []);
|
|
521
|
+
const comments = await githubRequestArray(`${repoPath(full)}/pulls/${number}/comments?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubReviewCommentSchema, BOT).catch(() => []);
|
|
522
|
+
const note = (c) => ({
|
|
523
|
+
id: c.id,
|
|
524
|
+
body: c.body ?? "",
|
|
525
|
+
...(mapUser(c.user) ? { author: mapUser(c.user) } : {}),
|
|
526
|
+
...(c.created_at ? { createdAt: c.created_at } : {}),
|
|
527
|
+
...(c.path ? { position: { path: c.path, line: c.line ?? c.original_line ?? null } } : {}),
|
|
528
|
+
});
|
|
529
|
+
const byReview = new Map();
|
|
530
|
+
const orphan = [];
|
|
531
|
+
for (const c of comments) {
|
|
532
|
+
if (c.pull_request_review_id != null) {
|
|
533
|
+
const list = byReview.get(c.pull_request_review_id) ?? [];
|
|
534
|
+
list.push(c);
|
|
535
|
+
byReview.set(c.pull_request_review_id, list);
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
orphan.push(c);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const out = [];
|
|
542
|
+
for (const r of reviews) {
|
|
543
|
+
const cs = byReview.get(r.id);
|
|
544
|
+
const body = r.body ?? "";
|
|
545
|
+
const notes = cs && cs.length > 0 ? cs.map(note) : body ? [{ id: r.id, body }] : [];
|
|
546
|
+
if (notes.length === 0)
|
|
547
|
+
continue;
|
|
548
|
+
out.push({ id: String(r.id), individualNote: true, notes });
|
|
549
|
+
}
|
|
550
|
+
if (orphan.length > 0) {
|
|
551
|
+
out.push({ id: "comments", individualNote: true, notes: orphan.map(note) });
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
},
|
|
555
|
+
async listNotes(repoId, number, o = {}) {
|
|
556
|
+
return issueComments(repoId, number, o);
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
// ---- Repository ----------------------------------------------------------
|
|
560
|
+
async function defaultBranchOf(repoId) {
|
|
561
|
+
const repo = await githubRequest(`/repositories/${repoId}`, { method: "GET" }, GithubRepositorySchema, BOT);
|
|
562
|
+
return repo.default_branch ?? "main";
|
|
563
|
+
}
|
|
564
|
+
const repository = {
|
|
565
|
+
async getRepo(repoId) {
|
|
566
|
+
const repo = await githubRequest(`/repositories/${repoId}`, { method: "GET" }, GithubRepositorySchema, BOT);
|
|
567
|
+
// Heal the id -> full_name cache so a rename/transfer is picked up here
|
|
568
|
+
// and every later fullNameOf() call uses the new path.
|
|
569
|
+
fullNameCache.set(repoId, repo.full_name);
|
|
570
|
+
return mapRepo(repo);
|
|
571
|
+
},
|
|
572
|
+
async branchExists(repoId, branch) {
|
|
573
|
+
const full = await fullNameOf(repoId);
|
|
574
|
+
const res = await fetch(`${apiBase()}${repoPath(full)}/branches/${encodeURIComponent(branch)}`, {
|
|
575
|
+
headers: { Authorization: `Bearer ${config.PLATFORM_TOKEN}`, "User-Agent": "devbot" },
|
|
576
|
+
});
|
|
577
|
+
if (res.status === 404)
|
|
578
|
+
return false;
|
|
579
|
+
if (!res.ok)
|
|
580
|
+
throw new PlatformHttpError(res.status, `branchExists ${branch}`, await res.text());
|
|
581
|
+
return true;
|
|
582
|
+
},
|
|
583
|
+
async firstExistingBranch(repoId, branches) {
|
|
584
|
+
for (const b of branches) {
|
|
585
|
+
if (await repository.branchExists(repoId, b))
|
|
586
|
+
return b;
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
589
|
+
},
|
|
590
|
+
async listBranches(repoId, o = {}) {
|
|
591
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/branches?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubBranchSchema, BOT);
|
|
592
|
+
const searched = o.search ? rs.filter((b) => b.name.includes(o.search)) : rs;
|
|
593
|
+
return searched.map((b) => ({
|
|
594
|
+
name: b.name,
|
|
595
|
+
...(b.protected !== undefined ? { protected: b.protected } : {}),
|
|
596
|
+
}));
|
|
597
|
+
},
|
|
598
|
+
async listTree(repoId, o = {}) {
|
|
599
|
+
const full = await fullNameOf(repoId);
|
|
600
|
+
const ref = o.ref ?? (await defaultBranchOf(repoId));
|
|
601
|
+
const sub = o.path ? `/${o.path.split("/").map(encodeURIComponent).join("/")}` : "";
|
|
602
|
+
const rs = await githubRequestArray(`${repoPath(full)}/contents${sub}?ref=${encodeURIComponent(ref)}&${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubContentSchema, BOT);
|
|
603
|
+
return rs.map((e) => ({
|
|
604
|
+
id: e.sha ?? "",
|
|
605
|
+
name: e.name,
|
|
606
|
+
type: e.type === "dir" ? "tree" : "blob",
|
|
607
|
+
path: e.path,
|
|
608
|
+
}));
|
|
609
|
+
},
|
|
610
|
+
async getFile(repoId, filePath, ref, maxBytes) {
|
|
611
|
+
const full = await fullNameOf(repoId);
|
|
612
|
+
const params = new URLSearchParams();
|
|
613
|
+
if (ref)
|
|
614
|
+
params.set("ref", ref);
|
|
615
|
+
const path = `${repoPath(full)}/contents/${filePath.split("/").map(encodeURIComponent).join("/")}${params.toString() ? `?${params.toString()}` : ""}`;
|
|
616
|
+
try {
|
|
617
|
+
const { bytes, contentType } = await githubFetchRaw(path, BOT, maxBytes, {
|
|
618
|
+
headers: { Accept: "application/vnd.github.raw" },
|
|
619
|
+
});
|
|
620
|
+
return { content: bytes.toString("utf8"), truncated: false, bytes: bytes.byteLength, contentType };
|
|
621
|
+
}
|
|
622
|
+
catch (e) {
|
|
623
|
+
if (e instanceof GithubOversizedError) {
|
|
624
|
+
return { content: "", truncated: true, bytes: maxBytes, contentType: "text/plain" };
|
|
625
|
+
}
|
|
626
|
+
throw e;
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
async listCommits(repoId, o = {}) {
|
|
630
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
631
|
+
if (o.refName)
|
|
632
|
+
params.set("sha", o.refName);
|
|
633
|
+
if (o.path)
|
|
634
|
+
params.set("path", o.path);
|
|
635
|
+
if (o.since)
|
|
636
|
+
params.set("since", o.since);
|
|
637
|
+
if (o.until)
|
|
638
|
+
params.set("until", o.until);
|
|
639
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/commits?${params.toString()}`, { method: "GET" }, GithubCommitListSchema, BOT);
|
|
640
|
+
return rs.map(mapCommit);
|
|
641
|
+
},
|
|
642
|
+
async getCommit(repoId, sha) {
|
|
643
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/commits/${encodeURIComponent(sha)}`, { method: "GET" }, GithubCommitListSchema, BOT);
|
|
644
|
+
return mapCommit(r);
|
|
645
|
+
},
|
|
646
|
+
async compare(repoId, from, to) {
|
|
647
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/compare/${encodeURIComponent(from)}...${encodeURIComponent(to)}`, { method: "GET" }, GithubCompareSchema, BOT);
|
|
648
|
+
return mapCompare(r);
|
|
649
|
+
},
|
|
650
|
+
async listTags(repoId, o = {}) {
|
|
651
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/tags?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubTagSchema, BOT);
|
|
652
|
+
const searched = o.search ? rs.filter((t) => t.name.includes(o.search)) : rs;
|
|
653
|
+
return searched.map((t) => ({
|
|
654
|
+
name: t.name,
|
|
655
|
+
...(t.commit?.sha ? { target: t.commit.sha } : {}),
|
|
656
|
+
}));
|
|
657
|
+
},
|
|
658
|
+
async listMembers(repoId, o = {}) {
|
|
659
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/collaborators?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubCollaboratorSchema, BOT);
|
|
660
|
+
const searched = o.search
|
|
661
|
+
? rs.filter((c) => c.login.includes(o.search) || (c.name ?? "").includes(o.search))
|
|
662
|
+
: rs;
|
|
663
|
+
return searched.map((c) => {
|
|
664
|
+
const perm = normalizePermission(c.permissions?.admin ? "admin" : c.permissions?.maintain ? "maintain" : c.permissions?.push ? "write" : c.permissions?.triage ? "triage" : c.permissions?.pull ? "read" : undefined, c.role_name) ?? normalizeRoleName(c.role_name);
|
|
665
|
+
return {
|
|
666
|
+
id: c.id,
|
|
667
|
+
username: c.login,
|
|
668
|
+
name: c.name || c.login,
|
|
669
|
+
...(perm !== undefined ? { permission: perm } : {}),
|
|
670
|
+
};
|
|
671
|
+
});
|
|
672
|
+
},
|
|
673
|
+
async listReleases(repoId, o = {}) {
|
|
674
|
+
const rs = await githubRequestArray(`${repoPath(await fullNameOf(repoId))}/releases?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubReleaseSchema, BOT);
|
|
675
|
+
const searched = o.search
|
|
676
|
+
? rs.filter((r) => r.tag_name.includes(o.search) || (r.name ?? "").includes(o.search))
|
|
677
|
+
: rs;
|
|
678
|
+
return searched.map((r) => ({
|
|
679
|
+
tagName: r.tag_name,
|
|
680
|
+
...(r.name ? { name: r.name } : {}),
|
|
681
|
+
...(r.body != null ? { description: r.body } : {}),
|
|
682
|
+
...(r.created_at ? { createdAt: r.created_at } : {}),
|
|
683
|
+
...(r.published_at ? { releasedAt: r.published_at } : {}),
|
|
684
|
+
...(r.author ? { author: mapUser(r.author) } : {}),
|
|
685
|
+
}));
|
|
686
|
+
},
|
|
687
|
+
// GitHub has no REST API for wikis (they are a separate git repo).
|
|
688
|
+
async listWikiPages() {
|
|
689
|
+
throw new UnsupportedError("repository.listWikiPages");
|
|
690
|
+
},
|
|
691
|
+
async getWikiPage() {
|
|
692
|
+
throw new UnsupportedError("repository.getWikiPage");
|
|
693
|
+
},
|
|
694
|
+
async searchProject(repoId, search, o = {}) {
|
|
695
|
+
// GitHub's REST search here is code search only. Any other neutral scope
|
|
696
|
+
// is a typed unsupported result rather than a silent wrong answer.
|
|
697
|
+
if (o.scope && o.scope !== "code") {
|
|
698
|
+
throw new UnsupportedError(`repository.searchProject(scope=${o.scope})`);
|
|
699
|
+
}
|
|
700
|
+
const full = await fullNameOf(repoId);
|
|
701
|
+
const params = new URLSearchParams({
|
|
702
|
+
q: `repo:${full} ${search}`.trim(),
|
|
703
|
+
...(o.page ? { page: String(o.page) } : {}),
|
|
704
|
+
per_page: String(o.perPage ?? 30),
|
|
705
|
+
});
|
|
706
|
+
try {
|
|
707
|
+
const r = await githubRequest(`/search/code?${params.toString()}`, { method: "GET" }, GithubSearchCodeSchema, BOT);
|
|
708
|
+
return r.items.map((h) => ({
|
|
709
|
+
...(typeof h.name === "string" ? { title: h.name } : {}),
|
|
710
|
+
...(typeof h.path === "string" ? { path: h.path } : {}),
|
|
711
|
+
...(h.html_url ? { webUrl: h.html_url } : {}),
|
|
712
|
+
...(h.text_matches && h.text_matches[0]?.fragment ? { data: h.text_matches[0].fragment } : {}),
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
715
|
+
catch (e) {
|
|
716
|
+
// Code search requires the repo scope / indexing and can 403 or 422.
|
|
717
|
+
// Degrade to an empty result rather than failing the whole MCP call.
|
|
718
|
+
if (e instanceof PlatformHttpError && (e.status === 403 || e.status === 422 || e.status === 404))
|
|
719
|
+
return [];
|
|
720
|
+
throw e;
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
// ---- CI (Actions) --------------------------------------------------------
|
|
725
|
+
const ci = {
|
|
726
|
+
async listPipelines(repoId, o = {}) {
|
|
727
|
+
if (o.scope)
|
|
728
|
+
throw new UnsupportedError(`ci.listPipelines(scope=${o.scope})`);
|
|
729
|
+
const params = new URLSearchParams(pageParams(o.page, o.perPage ?? 100));
|
|
730
|
+
if (o.ref)
|
|
731
|
+
params.set("branch", o.ref);
|
|
732
|
+
if (o.status)
|
|
733
|
+
params.set("status", o.status);
|
|
734
|
+
if (o.username)
|
|
735
|
+
params.set("actor", o.username);
|
|
736
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/actions/runs?${params.toString()}`, { method: "GET" }, GithubWorkflowRunsSchema, BOT);
|
|
737
|
+
return r.workflow_runs.map(mapRun);
|
|
738
|
+
},
|
|
739
|
+
async getPipeline(repoId, pipelineId) {
|
|
740
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/actions/runs/${pipelineId}`, { method: "GET" }, GithubActionRunSchema, BOT);
|
|
741
|
+
return mapRun(r);
|
|
742
|
+
},
|
|
743
|
+
async listPipelineJobs(repoId, pipelineId, o = {}) {
|
|
744
|
+
if (o.scope)
|
|
745
|
+
throw new UnsupportedError(`ci.listPipelineJobs(scope=${o.scope})`);
|
|
746
|
+
const r = await githubRequest(`${repoPath(await fullNameOf(repoId))}/actions/runs/${pipelineId}/jobs?${pageParams(o.page, o.perPage ?? 100)}`, { method: "GET" }, GithubWorkflowJobsSchema, BOT);
|
|
747
|
+
const jobs = r.jobs.map((j) => ({
|
|
748
|
+
id: j.id,
|
|
749
|
+
name: j.name,
|
|
750
|
+
status: j.conclusion ?? j.status,
|
|
751
|
+
...(j.html_url ? { webUrl: j.html_url } : {}),
|
|
752
|
+
...(j.started_at !== undefined ? { startedAt: j.started_at } : {}),
|
|
753
|
+
...(j.completed_at !== undefined ? { finishedAt: j.completed_at } : {}),
|
|
754
|
+
}));
|
|
755
|
+
// Neutral `outcome: "failed"` filters client-side: the Actions jobs
|
|
756
|
+
// endpoint has no failure scope (unlike GitLab).
|
|
757
|
+
return o.outcome === "failed" ? jobs.filter((j) => j.status === "failure") : jobs;
|
|
758
|
+
},
|
|
759
|
+
async getJobLog(repoId, jobId, maxBytes) {
|
|
760
|
+
const path = `${repoPath(await fullNameOf(repoId))}/actions/jobs/${jobId}/logs`;
|
|
761
|
+
// GitHub serves the log as a ZIP, so we must fetch and inflate the whole
|
|
762
|
+
// archive before we can slice its text. The archive is bounded by a
|
|
763
|
+
// safety cap; the tail is taken on the inflated text.
|
|
764
|
+
const { bytes } = await githubFetchRaw(path, BOT, GITHUB_LOG_ARCHIVE_MAX_BYTES);
|
|
765
|
+
const text = Buffer.from(zipText(bytes), "utf8");
|
|
766
|
+
return tailFileContent(text, text.byteLength, "text/plain", maxBytes);
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
// ---- Git auth ------------------------------------------------------------
|
|
770
|
+
const gitAuth = {
|
|
771
|
+
gitExtraHeader: () => gitExtraHeaderGithub(config.PLATFORM_TOKEN),
|
|
772
|
+
cloneUrl: (path) => `${config.PLATFORM_URL.replace(/\/$/, "")}/${path}.git`,
|
|
773
|
+
};
|
|
774
|
+
// ---- Attachments ---------------------------------------------------------
|
|
775
|
+
// GitHub uploads appear as absolute URLs on github.com/user-attachments or
|
|
776
|
+
// user-images.githubusercontent.com. The fetcher pulls the asset URL with the
|
|
777
|
+
// bot token (needed for private repos).
|
|
778
|
+
const attachments = {
|
|
779
|
+
findRefs: (body) => findGithubAttachmentRefs(body),
|
|
780
|
+
download: (_repoId, ref) => downloadGithubAttachment(ref),
|
|
781
|
+
// GitHub has no public REST API to attach a file to an issue or comment
|
|
782
|
+
// (the web UI uses a private endpoint). Typed gap, per the parity rules.
|
|
783
|
+
upload: async () => {
|
|
784
|
+
throw new UnsupportedError("attachments.upload");
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
// ---- Onboarding ----------------------------------------------------------
|
|
788
|
+
const onboarding = {
|
|
789
|
+
authorizeUrl(state) {
|
|
790
|
+
const params = new URLSearchParams({
|
|
791
|
+
client_id: config.PLATFORM_OAUTH_CLIENT_ID,
|
|
792
|
+
redirect_uri: `${config.PUBLIC_URL}/oauth/callback`,
|
|
793
|
+
// `repo` covers private repos, webhooks and collaborator management.
|
|
794
|
+
scope: "repo read:user",
|
|
795
|
+
state,
|
|
796
|
+
});
|
|
797
|
+
return `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
798
|
+
},
|
|
799
|
+
async exchangeCode(code) {
|
|
800
|
+
const res = await fetch("https://github.com/login/oauth/access_token", {
|
|
801
|
+
method: "POST",
|
|
802
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
803
|
+
body: JSON.stringify({
|
|
804
|
+
client_id: config.PLATFORM_OAUTH_CLIENT_ID,
|
|
805
|
+
client_secret: config.PLATFORM_OAUTH_CLIENT_SECRET,
|
|
806
|
+
code,
|
|
807
|
+
redirect_uri: `${config.PUBLIC_URL}/oauth/callback`,
|
|
808
|
+
}),
|
|
809
|
+
});
|
|
810
|
+
if (!res.ok)
|
|
811
|
+
throw new PlatformHttpError(res.status, "oauth token", await res.text());
|
|
812
|
+
const parsed = z.object({ access_token: z.string().min(1) }).safeParse(await res.json());
|
|
813
|
+
if (!parsed.success)
|
|
814
|
+
throw new Error("oauth token schema mismatch");
|
|
815
|
+
return { accessToken: parsed.data.access_token };
|
|
816
|
+
},
|
|
817
|
+
async currentUser(accessToken) {
|
|
818
|
+
const u = await githubRequest("/user", { method: "GET" }, GithubUserSchema, { kind: "bearer", token: accessToken });
|
|
819
|
+
// Authorization (allowlist) is enforced platform-neutrally in
|
|
820
|
+
// oauth-routes; the driver only reports identity.
|
|
821
|
+
return {
|
|
822
|
+
id: u.id,
|
|
823
|
+
username: u.login,
|
|
824
|
+
};
|
|
825
|
+
},
|
|
826
|
+
async listProjects(accessToken, q) {
|
|
827
|
+
const out = [];
|
|
828
|
+
for (let page = 1;; page++) {
|
|
829
|
+
const rs = await githubRequestArray(`/user/repos?${pageParams(page, 100)}&affiliation=owner,collaborator,organization_member&sort=full_name`, { method: "GET" }, GithubRepositorySchema, { kind: "bearer", token: accessToken });
|
|
830
|
+
if (rs.length === 0)
|
|
831
|
+
break;
|
|
832
|
+
for (const r of rs)
|
|
833
|
+
out.push({ id: r.id, path: r.full_name, webUrl: r.html_url ?? "" });
|
|
834
|
+
if (rs.length < 100)
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
const query = q?.trim().toLowerCase();
|
|
838
|
+
return query ? out.filter((p) => p.path.toLowerCase().includes(query)) : out;
|
|
839
|
+
},
|
|
840
|
+
async addBot(accessToken, project) {
|
|
841
|
+
const { owner, repo } = splitFullName(project.path);
|
|
842
|
+
await githubVoid(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(config.BOT_USERNAME)}`, { method: "PUT", body: JSON.stringify({ permission: "push" }) }, { kind: "bearer", token: accessToken }, { okStatuses: [201, 204, 404, 422] });
|
|
843
|
+
},
|
|
844
|
+
async removeBot(accessToken, project) {
|
|
845
|
+
const { owner, repo } = splitFullName(project.path);
|
|
846
|
+
await githubVoid(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(config.BOT_USERNAME)}`, { method: "DELETE" }, { kind: "bearer", token: accessToken }, { okStatuses: [204, 404] });
|
|
847
|
+
},
|
|
848
|
+
async findOrCreateWebhook(accessToken, project, url, secret) {
|
|
849
|
+
const { owner, repo } = splitFullName(project.path);
|
|
850
|
+
const base = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks`;
|
|
851
|
+
const hooks = await githubRequestArray(base, { method: "GET" }, GithubWebhookSchema, { kind: "bearer", token: accessToken });
|
|
852
|
+
const existing = hooks.find((h) => h.config?.url === url);
|
|
853
|
+
if (existing)
|
|
854
|
+
return { id: existing.id, created: false };
|
|
855
|
+
const created = await githubRequest(base, {
|
|
856
|
+
method: "POST",
|
|
857
|
+
body: JSON.stringify({
|
|
858
|
+
name: "web",
|
|
859
|
+
active: true,
|
|
860
|
+
events: ["issues", "issue_comment", "pull_request", "workflow_run"],
|
|
861
|
+
config: { url, content_type: "json", secret },
|
|
862
|
+
}),
|
|
863
|
+
}, GithubWebhookSchema, { kind: "bearer", token: accessToken });
|
|
864
|
+
return { id: created.id, created: true };
|
|
865
|
+
},
|
|
866
|
+
async setWebhookSecret(accessToken, project, webhookId, secret) {
|
|
867
|
+
const { owner, repo } = splitFullName(project.path);
|
|
868
|
+
await githubVoid(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks/${webhookId}`, {
|
|
869
|
+
method: "PATCH",
|
|
870
|
+
body: JSON.stringify({
|
|
871
|
+
config: { url: `${config.PUBLIC_URL}/webhook`, content_type: "json", secret },
|
|
872
|
+
}),
|
|
873
|
+
}, { kind: "bearer", token: accessToken });
|
|
874
|
+
},
|
|
875
|
+
async ensureCiFailureEvents(accessToken, project, webhookId) {
|
|
876
|
+
const { owner, repo } = splitFullName(project.path);
|
|
877
|
+
const base = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks`;
|
|
878
|
+
const hooks = await githubRequestArray(base, { method: "GET" }, GithubWebhookSchema, { kind: "bearer", token: accessToken });
|
|
879
|
+
const existing = hooks.find((h) => h.id === webhookId);
|
|
880
|
+
const current = existing?.events ?? ["issues", "issue_comment", "pull_request"];
|
|
881
|
+
if (current.includes("workflow_run"))
|
|
882
|
+
return;
|
|
883
|
+
await githubVoid(`${base}/${webhookId}`, {
|
|
884
|
+
method: "PATCH",
|
|
885
|
+
body: JSON.stringify({ events: [...new Set([...current, "workflow_run"])] }),
|
|
886
|
+
}, { kind: "bearer", token: accessToken });
|
|
887
|
+
},
|
|
888
|
+
async deleteWebhook(accessToken, project, webhookId) {
|
|
889
|
+
const { owner, repo } = splitFullName(project.path);
|
|
890
|
+
await githubVoid(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks/${webhookId}`, { method: "DELETE" }, { kind: "bearer", token: accessToken }, { okStatuses: [204, 404] });
|
|
891
|
+
},
|
|
892
|
+
async testWebhook(accessToken, project, webhookId) {
|
|
893
|
+
const { owner, repo } = splitFullName(project.path);
|
|
894
|
+
try {
|
|
895
|
+
await githubVoid(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/hooks/${webhookId}/pings`, { method: "POST" }, { kind: "bearer", token: accessToken }, { okStatuses: [204] });
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
// Test delivery is best-effort, matching GitLab's swallow.
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
// ---- Links -------------------------------------------------------------
|
|
903
|
+
const links = {
|
|
904
|
+
workItemUrl(projectPath, number) {
|
|
905
|
+
if (!projectPath || !Number.isInteger(number) || number <= 0)
|
|
906
|
+
return "";
|
|
907
|
+
return `${config.PLATFORM_URL.replace(/\/$/, "")}/${projectPath}/issues/${number}`;
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
return {
|
|
911
|
+
name: platform,
|
|
912
|
+
ingress,
|
|
913
|
+
workItems,
|
|
914
|
+
changeRequests,
|
|
915
|
+
repository,
|
|
916
|
+
ci,
|
|
917
|
+
gitAuth,
|
|
918
|
+
attachments,
|
|
919
|
+
onboarding,
|
|
920
|
+
links,
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function mapRun(r) {
|
|
924
|
+
const name = r.display_title ?? r.name;
|
|
925
|
+
return {
|
|
926
|
+
id: r.id,
|
|
927
|
+
status: r.conclusion ?? r.status ?? "unknown",
|
|
928
|
+
...(r.head_branch ? { ref: r.head_branch } : {}),
|
|
929
|
+
...(r.head_sha ? { sha: r.head_sha } : {}),
|
|
930
|
+
...(name ? { name } : {}),
|
|
931
|
+
...(r.html_url ? { webUrl: r.html_url } : {}),
|
|
932
|
+
...(r.created_at ? { createdAt: r.created_at } : {}),
|
|
933
|
+
...(r.updated_at ? { updatedAt: r.updated_at } : {}),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
//# sourceMappingURL=index.js.map
|