@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
package/dist/git.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import { config } from "./config.js";
|
|
2
|
+
import { runtime } from "./runtime-state.js";
|
|
3
|
+
import { log } from "./logger.js";
|
|
4
|
+
import { shq, redact } from "./redact.js";
|
|
5
|
+
import { podman } from "./podman.js";
|
|
6
|
+
import { buildPostCloneScript } from "./devbot-scripts.js";
|
|
7
|
+
import { getPlatform } from "./platform/index.js";
|
|
8
|
+
import { conflictCheckScript, parseConflictCheckResult, publishScript, parsePublishResult, parseBranchTip, resolverPrompt, parseResolverResult } from "./conflict-fix.js";
|
|
9
|
+
// Volume GC lives in src/volume-gc.ts: a startup reconcile + periodic sweep
|
|
10
|
+
// remove devbot-wi-* volumes inactive for VOLUME_GC_TTL_MS (default 1 week).
|
|
11
|
+
// Build the `--env NAME=VALUE` podman args for a set of per-project env vars.
|
|
12
|
+
// Spread into any podman `run` args array so every container (clone, sync,
|
|
13
|
+
// auth-write, opencode, push) sees the same env. Values are injected as-is —
|
|
14
|
+
// they're already decrypted by the store. podman --env passes them into the
|
|
15
|
+
// container's environment; no shell interpolation occurs.
|
|
16
|
+
export function envArgs(vars) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const v of vars) {
|
|
19
|
+
out.push("--env", `${v.name}=${v.value}`);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
export function volumeName(workItemId) {
|
|
24
|
+
const prefix = config.INSTANCE_ID ? `${config.INSTANCE_ID}-` : "";
|
|
25
|
+
return `${prefix}devbot-wi-${workItemId}`;
|
|
26
|
+
}
|
|
27
|
+
// Every container that mounts a work item's named volumes must use the same
|
|
28
|
+
// SELinux MCS pair. Podman assigns each container a random pair, and `:Z`
|
|
29
|
+
// relabels the whole volume to the mounting container's pair — so a helper
|
|
30
|
+
// spawned while the agent is running (attachment download/read, peer sync)
|
|
31
|
+
// would revoke the agent's access to /workspace and make the files it writes
|
|
32
|
+
// unreadable to the agent (EACCES). Deriving one pair from the work item id
|
|
33
|
+
// keeps every container of that work item compatible while still separating
|
|
34
|
+
// different work items. No-op where SELinux is disabled (InitLabels returns
|
|
35
|
+
// early), so it is safe to pass unconditionally.
|
|
36
|
+
export function volumeSelinuxLevel(workItemId) {
|
|
37
|
+
const id = Math.abs(Math.trunc(workItemId));
|
|
38
|
+
const a = id % 1024;
|
|
39
|
+
const b = (a + 1 + (id % 1023)) % 1024;
|
|
40
|
+
return `s0:c${a},c${b}`;
|
|
41
|
+
}
|
|
42
|
+
export function volumeSelinuxArgs(workItemId) {
|
|
43
|
+
return ["--security-opt", `label=level:${volumeSelinuxLevel(workItemId)}`];
|
|
44
|
+
}
|
|
45
|
+
// Name of the opencode `run` container for a work item. Single-sourced so the
|
|
46
|
+
// runner and the logs route can never drift (see runner.ts and task-log.ts).
|
|
47
|
+
export function runContainerName(workItemId) {
|
|
48
|
+
return `devbot-wi-${workItemId}-run`;
|
|
49
|
+
}
|
|
50
|
+
// Prefix shared by every phase container for a work item. The live-log viewer
|
|
51
|
+
// discovers the active phase by listing running containers with this prefix.
|
|
52
|
+
export function workItemContainerPrefix(workItemId) {
|
|
53
|
+
return `devbot-wi-${workItemId}-`;
|
|
54
|
+
}
|
|
55
|
+
// Pin the container log driver so `podman logs` can serve a RUNNING container's
|
|
56
|
+
// output. Some hosts default to the journald driver, which returns nothing for a
|
|
57
|
+
// live container (and may have no journal backend at all); k8s-file writes each
|
|
58
|
+
// container's stdout/stderr to a file that `podman logs` can read/tail
|
|
59
|
+
// immediately. Every phase container launch spreads this in.
|
|
60
|
+
export function logDriverArgs() {
|
|
61
|
+
return ["--log-driver", "k8s-file"];
|
|
62
|
+
}
|
|
63
|
+
// Name of a concrete phase container for a work item. Single-sourced so stop/
|
|
64
|
+
// reset, the launch sites, and the live-log phase discovery can never drift.
|
|
65
|
+
// `runContainerName` stays separate because it is referenced widely; every other
|
|
66
|
+
// phase goes through here.
|
|
67
|
+
export function workItemContainerName(workItemId, phase) {
|
|
68
|
+
return `${workItemContainerPrefix(workItemId)}${phase}`;
|
|
69
|
+
}
|
|
70
|
+
// Every phase container for a work item, in one place so stop/reset and the
|
|
71
|
+
// launch sites can never drift. These are the exact `--name`s used when the
|
|
72
|
+
// containers are created (git clone/sync/push, auth/podman-init, peer sync,
|
|
73
|
+
// opencode run). `podman rm -f` tolerates absent names.
|
|
74
|
+
export function workItemContainerNames(workItemId) {
|
|
75
|
+
return [
|
|
76
|
+
workItemContainerName(workItemId, "detect"),
|
|
77
|
+
workItemContainerName(workItemId, "clone"),
|
|
78
|
+
workItemContainerName(workItemId, "sync"),
|
|
79
|
+
workItemContainerName(workItemId, "auth"),
|
|
80
|
+
workItemContainerName(workItemId, "podman-init"),
|
|
81
|
+
workItemContainerName(workItemId, "peerinit"),
|
|
82
|
+
workItemContainerName(workItemId, "peersync"),
|
|
83
|
+
workItemContainerName(workItemId, "gitexclude"),
|
|
84
|
+
workItemContainerName(workItemId, "attachread"),
|
|
85
|
+
workItemContainerName(workItemId, "attachdl"),
|
|
86
|
+
runContainerName(workItemId),
|
|
87
|
+
workItemContainerName(workItemId, "push"),
|
|
88
|
+
workItemContainerName(workItemId, "forcepush"),
|
|
89
|
+
workItemContainerName(workItemId, "conflictcheck"),
|
|
90
|
+
workItemContainerName(workItemId, "tip"),
|
|
91
|
+
workItemContainerName(workItemId, "resolve"),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
// Human-facing phase labels for the log viewer, keyed by the container-name
|
|
95
|
+
// suffix. A phase with no label falls back to the raw suffix.
|
|
96
|
+
const PHASE_LABELS = {
|
|
97
|
+
detect: "Detecting workspace",
|
|
98
|
+
clone: "Cloning repository",
|
|
99
|
+
sync: "Syncing repository",
|
|
100
|
+
auth: "Writing run files",
|
|
101
|
+
"podman-init": "Preparing nested Podman",
|
|
102
|
+
peerinit: "Preparing peer projects",
|
|
103
|
+
peersync: "Syncing peer project",
|
|
104
|
+
gitexclude: "Updating git excludes",
|
|
105
|
+
attachread: "Reading attachment",
|
|
106
|
+
attachdl: "Downloading attachment",
|
|
107
|
+
run: "Running agent",
|
|
108
|
+
push: "Pushing branch",
|
|
109
|
+
forcepush: "Force-pushing branch",
|
|
110
|
+
conflictcheck: "Checking for conflicts",
|
|
111
|
+
tip: "Reading branch tip",
|
|
112
|
+
resolve: "Resolving conflicts",
|
|
113
|
+
};
|
|
114
|
+
export function phaseLabel(phase) {
|
|
115
|
+
return PHASE_LABELS[phase] ?? phase;
|
|
116
|
+
}
|
|
117
|
+
// Private nested-Podman store for a work item. Paired 1:1 with the workspace
|
|
118
|
+
// volume; created only when DEVCONTAINER_ENABLED, removed alongside it. The
|
|
119
|
+
// `-podman` suffix is deliberately outside the `(\d+)$` workspace reconciler
|
|
120
|
+
// regex so the two are never confused.
|
|
121
|
+
export function podmanVolumeName(workItemId) {
|
|
122
|
+
const prefix = config.INSTANCE_ID ? `${config.INSTANCE_ID}-` : "";
|
|
123
|
+
return `${prefix}devbot-wi-${workItemId}-podman`;
|
|
124
|
+
}
|
|
125
|
+
// Mount point for the dedicated per-work-item XDG volume. Deliberately outside
|
|
126
|
+
// /workspace (the cloned target repo's working tree) so opencode's config,
|
|
127
|
+
// auth, and session data — including the per-run MCP bearer token and the
|
|
128
|
+
// provider API keys — can never be staged by the agent's `git add -A`.
|
|
129
|
+
export const XDG_HOME_DIR = "/home/devbot/.xdg";
|
|
130
|
+
// Dedicated per-work-item XDG volume: opencode's config + data live OUTSIDE the
|
|
131
|
+
// work tree. Sibling of the workspace and nested-Podman store volumes; all
|
|
132
|
+
// three share one ledger row and are reclaimed together by reset/GC. The
|
|
133
|
+
// `-xdg` suffix is recognized by the volume reconciler (volume-gc.ts).
|
|
134
|
+
export function xdgVolumeName(workItemId) {
|
|
135
|
+
const prefix = config.INSTANCE_ID ? `${config.INSTANCE_ID}-` : "";
|
|
136
|
+
return `${prefix}devbot-wi-${workItemId}-xdg`;
|
|
137
|
+
}
|
|
138
|
+
export function branchName(workItemNumber) {
|
|
139
|
+
return `wi-${workItemNumber}`;
|
|
140
|
+
}
|
|
141
|
+
// Resolve the branch a work item runs on: the stored /branch choice when set,
|
|
142
|
+
// otherwise the default wi-<number>. Pure so the resolution rule is unit
|
|
143
|
+
// testable without a DB; the store read happens in runner.ts.
|
|
144
|
+
export function resolveWorkBranch(stored, workItemNumber) {
|
|
145
|
+
return stored ?? branchName(workItemNumber);
|
|
146
|
+
}
|
|
147
|
+
export function timeoutFor(mode) {
|
|
148
|
+
return mode === "build" ? runtime().buildTimeoutMs : runtime().planTimeoutMs;
|
|
149
|
+
}
|
|
150
|
+
// Strip trailing slashes from PLATFORM_URL so URL-scoped git config keys stay
|
|
151
|
+
// well-formed (http.<url>/.extraHeader, not http.<url>//.extraHeader).
|
|
152
|
+
export function normalizeForgeUrl(url) {
|
|
153
|
+
return url.replace(/\/+$/, "");
|
|
154
|
+
}
|
|
155
|
+
// Hostname (no port) of PLATFORM_URL, for the SSH→HTTPS insteadOf rewrite. Uses
|
|
156
|
+
// the URL parser so a path on PLATFORM_URL (rare) doesn't leak into the rewrite
|
|
157
|
+
// key, and strips any HTTPS port so it never contaminates an SSH rewrite. The
|
|
158
|
+
// SSH host is assumed to equal the PLATFORM_URL hostname.
|
|
159
|
+
export function forgeHostname(url) {
|
|
160
|
+
return new URL(normalizeForgeUrl(url)).hostname;
|
|
161
|
+
}
|
|
162
|
+
// Git auth for the forge's smart-HTTP endpoint. The driver supplies the header
|
|
163
|
+
// (URL-scoped to the configured forge host for GitLab, so git only sends the
|
|
164
|
+
// bot token there — a submodule on an external host is fetched without the
|
|
165
|
+
// token instead of leaking it). The `-c` config propagates to the child git
|
|
166
|
+
// processes `submodule update` spawns.
|
|
167
|
+
export function gitExtraHeader() {
|
|
168
|
+
return getPlatform().gitAuth.gitExtraHeader();
|
|
169
|
+
}
|
|
170
|
+
// Same-host SSH→HTTPS URL rewriting for submodules. A submodule whose
|
|
171
|
+
// .gitmodules URL is `git@<forge-host>:…` or `ssh://git@<forge-host>/…`
|
|
172
|
+
// would otherwise try SSH (no key in the container) and fail. Rewriting it to
|
|
173
|
+
// the forge HTTPS base lets it inherit the scoped http extraHeader above.
|
|
174
|
+
// External SSH submodules are deliberately NOT rewritten.
|
|
175
|
+
// git's `insteadOf` is a literal PREFIX match, so a URL with an explicit SSH
|
|
176
|
+
// port (`ssh://git@host:2222/…`) only matches a key that carries that same
|
|
177
|
+
// `:2222`. The port can't be derived from the HTTPS PLATFORM_URL, so it comes
|
|
178
|
+
// from PLATFORM_SSH_PORT; when set, an extra port-bearing ssh:// key is emitted.
|
|
179
|
+
// Only the ssh:// form has a port suffix — the scp-like `git@host:path` form
|
|
180
|
+
// has no port slot, so a single scp key suffices.
|
|
181
|
+
export function submoduleGitConfig(extraHeader, sshPort = config.PLATFORM_SSH_PORT) {
|
|
182
|
+
const base = normalizeForgeUrl(config.PLATFORM_URL);
|
|
183
|
+
const host = forgeHostname(config.PLATFORM_URL);
|
|
184
|
+
const scp = `url.${base}/.insteadOf=${shq(`git@${host}:`)}`;
|
|
185
|
+
const ssh = `url.${base}/.insteadOf=${shq(`ssh://git@${host}/`)}`;
|
|
186
|
+
const parts = [extraHeader, `-c ${scp}`, `-c ${ssh}`];
|
|
187
|
+
if (sshPort) {
|
|
188
|
+
parts.push(`-c url.${base}/.insteadOf=${shq(`ssh://git@${host}:${sshPort}/`)}`);
|
|
189
|
+
}
|
|
190
|
+
return parts.join(" ");
|
|
191
|
+
}
|
|
192
|
+
// Marker printed (one per path) when a submodule can't be initialized. The
|
|
193
|
+
// group always exits 0 so the submodule step stays best-effort and can never
|
|
194
|
+
// mask an earlier clone/fetch failure.
|
|
195
|
+
export const SUBMODULE_FAILED_MARKER = "DEVBOT_SUBMODULE_FAILED";
|
|
196
|
+
// Max submodule paths named in the warning note; the rest are summarized.
|
|
197
|
+
export const SUBMODULE_WARNING_CAP = 10;
|
|
198
|
+
// Compose the submodule init/update step. Recursive, best-effort, and grouped
|
|
199
|
+
// in braces so its `||` applies to the submodule command alone (a bare
|
|
200
|
+
// `a && b || c` would mask an earlier clone/fetch failure). On failure,
|
|
201
|
+
// `git submodule status --recursive` lists the uninitialized paths (leading
|
|
202
|
+
// '-' in column 1) — the failing one plus any never reached.
|
|
203
|
+
export function buildSubmoduleStep(submoduleCfg, enabled = true) {
|
|
204
|
+
if (!enabled || !submoduleCfg)
|
|
205
|
+
return "";
|
|
206
|
+
const update = `${submoduleCfg} submodule update --init --recursive`;
|
|
207
|
+
const report = `git submodule status --recursive | awk 'substr($1,1,1)=="-" {print "${SUBMODULE_FAILED_MARKER}:" $2}'`;
|
|
208
|
+
return `{ ${update} || ${report}; }`;
|
|
209
|
+
}
|
|
210
|
+
// Extract the failing submodule paths from a submodule-step stdout. Each failed
|
|
211
|
+
// path is emitted on its own marker line; order is preserved.
|
|
212
|
+
export function parseSubmoduleFailures(stdout) {
|
|
213
|
+
const out = [];
|
|
214
|
+
const re = new RegExp(`^${SUBMODULE_FAILED_MARKER}:(.+)$`, "gm");
|
|
215
|
+
let m;
|
|
216
|
+
while ((m = re.exec(stdout)) !== null) {
|
|
217
|
+
const path = m[1]?.trim();
|
|
218
|
+
if (path)
|
|
219
|
+
out.push(path);
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
// Build the informational note for submodule fetch failures. Returns null when
|
|
224
|
+
// there's nothing to report. Paths are repo-controlled: strip newlines/CR and
|
|
225
|
+
// backticks before wrapping in an inline-code span (a stray backtick would
|
|
226
|
+
// break the Markdown and could inject a mention). The list is capped.
|
|
227
|
+
export function formatSubmoduleWarning(paths) {
|
|
228
|
+
if (paths.length === 0)
|
|
229
|
+
return null;
|
|
230
|
+
const sanitize = (p) => p.replace(/[`\r\n]/g, "").trim();
|
|
231
|
+
const shown = paths.slice(0, SUBMODULE_WARNING_CAP).map((p) => `\`${sanitize(p)}\``);
|
|
232
|
+
const overflow = paths.length - shown.length;
|
|
233
|
+
const list = shown.join(", ") + (overflow > 0 ? ` and ${overflow} more` : "");
|
|
234
|
+
return `ℹ️ Some git submodules could not be fetched: ${list}. Continuing without them.`;
|
|
235
|
+
}
|
|
236
|
+
export async function volumeExists(vol) {
|
|
237
|
+
const r = await podman(["volume", "inspect", vol]);
|
|
238
|
+
return r.code === 0;
|
|
239
|
+
}
|
|
240
|
+
// Best-effort removal of the legacy in-repo XDG dirs written by DevBot before
|
|
241
|
+
// the xdg volume existed. They now live on /home/devbot/.xdg, but a volume
|
|
242
|
+
// created by an older release still contains /workspace/.xdg-data and
|
|
243
|
+
// /workspace/.xdg-config with a stale MCP token and provider API keys — which
|
|
244
|
+
// the agent's `git add -A` could commit into a target repo that doesn't
|
|
245
|
+
// ignore them. `|| true` so a permissions hiccup can never block a sync.
|
|
246
|
+
export function legacyXdgCleanup() {
|
|
247
|
+
return "rm -rf /workspace/.xdg-data /workspace/.xdg-config || true";
|
|
248
|
+
}
|
|
249
|
+
// Compose the host-side sync script for an existing volume. It MUST fetch
|
|
250
|
+
// origin before every run so the agent sees the latest remote refs — the
|
|
251
|
+
// orchestrator runs this before spawning opencode. Branch checkout is
|
|
252
|
+
// host-side and deterministic in both modes: relying on the agent to checkout
|
|
253
|
+
// left HEAD on the clone target (e.g. dev) whenever the LLM skipped the step,
|
|
254
|
+
// which the HEAD guard then had to refuse. Rebase onto origin/<branch> stays
|
|
255
|
+
// agent-owned in build mode (it is in the prompt); only branch selection is
|
|
256
|
+
// enforced here. Exported as a pure helper so scripts/check-git-sync.ts can
|
|
257
|
+
// pin the fetch guarantee. submoduleStep (when non-empty) runs last, after
|
|
258
|
+
// fetch/checkout, best-effort.
|
|
259
|
+
// Checkout chain (first match wins):
|
|
260
|
+
// 1. local branch exists → git checkout <branch>
|
|
261
|
+
// 2. remote branch exists → git checkout -b <branch> origin/<branch>
|
|
262
|
+
// 3. neither (fresh work item) → git checkout -B <branch> (off clone target HEAD)
|
|
263
|
+
// `mode` is kept in the signature for shape stability (caller + tests); the
|
|
264
|
+
// mode split was dropped when branch selection became orchestrator-owned.
|
|
265
|
+
export function syncScript(branch, extraHeader, _mode, submoduleStep = "") {
|
|
266
|
+
const b = shq(branch);
|
|
267
|
+
const checkout = `git checkout ${b} 2>/dev/null` +
|
|
268
|
+
` || git checkout -b ${b} origin/${b} 2>/dev/null` +
|
|
269
|
+
` || git checkout -B ${b}`;
|
|
270
|
+
// umask 002 + group-writable tree: the bash tool runs as the unprivileged
|
|
271
|
+
// `agent` UID (gid devbot) and must be able to edit/commit alongside opencode
|
|
272
|
+
// (devbot). chmod -R keeps pre-existing files (from older volumes) editable.
|
|
273
|
+
// Braced so its `|| true` applies to the chmod alone: a fetch/checkout failure
|
|
274
|
+
// must still abort the sync, while a chmod EPERM is harmless. After the UID
|
|
275
|
+
// split (#81) `/workspace` can hold files devbot doesn't own — the `agent` UID
|
|
276
|
+
// (1001) and nested-Podman inner-root (subuid) — and chmod then fails
|
|
277
|
+
// "Operation not permitted" (#91). A bare chmod here turned that into a fatal
|
|
278
|
+
// sync failure misreported as a rebase conflict, blocking every re-run.
|
|
279
|
+
const lines = ["umask 002", "cd /workspace", legacyXdgCleanup(), `${extraHeader} fetch origin`, checkout];
|
|
280
|
+
if (submoduleStep)
|
|
281
|
+
lines.push(submoduleStep);
|
|
282
|
+
lines.push("{ chmod -R g+rwX /workspace 2>/dev/null || true; }");
|
|
283
|
+
lines.push(`echo "TIP:$(git rev-parse origin/${b} 2>/dev/null || true)"`);
|
|
284
|
+
return lines.join(" && ");
|
|
285
|
+
}
|
|
286
|
+
export async function ensureVolumeAndWorkspace(input, mode, envVars = [], branches = [], builderImage = config.BUILDER_IMAGE) {
|
|
287
|
+
const vol = volumeName(input.workItemId);
|
|
288
|
+
const exists = await volumeExists(vol);
|
|
289
|
+
if (!exists) {
|
|
290
|
+
const create = await podman(["volume", "create", vol]);
|
|
291
|
+
if (create.code !== 0) {
|
|
292
|
+
return { ok: false, reason: `volume create failed: ${redact(create.stderr.slice(-256))}`, isFirstRun: true, logs: redact(create.stderr) };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const cloneUrl = input.repoHttpUrl;
|
|
296
|
+
const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
|
|
297
|
+
const extraHeader = gitExtraHeader();
|
|
298
|
+
// Submodule init/update runs on first clone and every sync (picks up new
|
|
299
|
+
// submodules + updated gitlinks). Best-effort and never fails the run; the
|
|
300
|
+
// scoped header + same-host SSH rewrite apply because the `-c` config
|
|
301
|
+
// propagates to the child git processes.
|
|
302
|
+
const submoduleStep = buildSubmoduleStep(submoduleGitConfig(extraHeader), runtime().gitSubmodulesEnabled);
|
|
303
|
+
// Detect whether the workspace already has a git repo.
|
|
304
|
+
const detect = await podman([
|
|
305
|
+
"run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-detect`,
|
|
306
|
+
"-v", `${vol}:/workspace:Z`,
|
|
307
|
+
...envArgs(envVars),
|
|
308
|
+
builderImage,
|
|
309
|
+
"sh", "-c", "cd /workspace 2>/dev/null && git rev-parse --is-inside-work-tree 2>/dev/null && echo OK || echo EMPTY",
|
|
310
|
+
], { timeoutMs: timeoutFor(mode) });
|
|
311
|
+
const isInit = detect.stdout.includes("OK");
|
|
312
|
+
if (!isInit) {
|
|
313
|
+
// First run: clone with a fallback chain.
|
|
314
|
+
// 1. wi-<iid> — prior WIP branch for this work item, if it exists remotely
|
|
315
|
+
// 2. targetBranch — /base <branch> if given, else the first existing branch
|
|
316
|
+
// from BASE_BRANCHES (tried top-to-bottom, e.g. default
|
|
317
|
+
// "dev,master" tries dev then master); the MR target.
|
|
318
|
+
// Every mode force-checks-out wi-<iid> after the clone, so the local work
|
|
319
|
+
// branch exists even after a fallback clone. Plan mode creates it as a local
|
|
320
|
+
// ref alongside the target; build mode makes it HEAD before the agent runs.
|
|
321
|
+
// /base is a hard per-work-item override that bypasses the env list entirely.
|
|
322
|
+
// If neither /base nor any BASE_BRANCHES branch exists remotely, this is an
|
|
323
|
+
// explicit error (no bare-clone fall-through) — the runner posts an
|
|
324
|
+
// actionable note. targetBranch is always a verified-existing branch by the
|
|
325
|
+
// time the clone runs, so the chain is 2 links (no repo-default bare clone
|
|
326
|
+
// slot).
|
|
327
|
+
log.info("first-run clone", { workItemId: input.workItemId, repo: input.repoHttpUrl, mode, baseBranch: input.baseBranch });
|
|
328
|
+
let targetBranch;
|
|
329
|
+
if (input.baseBranch) {
|
|
330
|
+
targetBranch = input.baseBranch;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
let hit;
|
|
334
|
+
try {
|
|
335
|
+
hit = await getPlatform().repository.firstExistingBranch(input.projectId, branches);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
return { ok: false, reason: `base branch existence check failed: ${e instanceof Error ? e.message : String(e)}`, isFirstRun: true };
|
|
339
|
+
}
|
|
340
|
+
if (!hit) {
|
|
341
|
+
return { ok: false, reason: `no base branch found (tried: ${branches.join(", ")})`, isFirstRun: true };
|
|
342
|
+
}
|
|
343
|
+
targetBranch = hit;
|
|
344
|
+
}
|
|
345
|
+
const gitConfig = `git config user.name ${shq(config.BOT_GIT_NAME)} && git config user.email ${shq(config.BOT_GIT_EMAIL)}`;
|
|
346
|
+
const cloneChain = [
|
|
347
|
+
`${extraHeader} clone --branch ${shq(branch)} ${shq(cloneUrl)} /workspace`,
|
|
348
|
+
`${extraHeader} clone --branch ${shq(targetBranch)} ${shq(cloneUrl)} /workspace`,
|
|
349
|
+
].join(" || ");
|
|
350
|
+
const checkoutBranch = ` && git checkout -B ${shq(branch)}`;
|
|
351
|
+
// Run .devbot/init.sh if present (after clone, before opencode). Env vars
|
|
352
|
+
// are already exported into this container via --env flags. Non-zero exit
|
|
353
|
+
// aborts with exit 8 + DEVBOT_INIT_FAILED marker for an actionable error.
|
|
354
|
+
const postClone = buildPostCloneScript();
|
|
355
|
+
// Submodule init/update runs after clone + checkout + git config, before
|
|
356
|
+
// .devbot/init.sh (so init.sh sees populated submodules). The step is
|
|
357
|
+
// best-effort: its brace group exits 0 even on failure, then we surface
|
|
358
|
+
// the affected paths as a non-fatal warning below.
|
|
359
|
+
const submoduleSegment = submoduleStep ? ` && ${submoduleStep}` : "";
|
|
360
|
+
// umask 002 + a final group-writable pass: the bash tool runs as the
|
|
361
|
+
// unprivileged `agent` UID (gid devbot) and must edit/commit the tree.
|
|
362
|
+
const cloneCmd = `umask 002 && ${cloneChain} && cd /workspace && ${gitConfig}${checkoutBranch}${submoduleSegment} && ${postClone} && ${legacyXdgCleanup()} && chmod -R g+rwX /workspace && echo "TIP:$(git rev-parse origin/${shq(branch)} 2>/dev/null || true)"`;
|
|
363
|
+
const r = await podman([
|
|
364
|
+
"run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-clone`,
|
|
365
|
+
"-v", `${vol}:/workspace:Z`,
|
|
366
|
+
...envArgs(envVars),
|
|
367
|
+
builderImage,
|
|
368
|
+
"sh", "-c", cloneCmd,
|
|
369
|
+
], { timeoutMs: timeoutFor(mode) });
|
|
370
|
+
if (r.code !== 0) {
|
|
371
|
+
// init.sh failure (exit 8) → actionable, developer-fixable error.
|
|
372
|
+
if (r.code === 8 && r.stdout.includes("DEVBOT_INIT_FAILED")) {
|
|
373
|
+
const exitCode = r.stdout.split("DEVBOT_INIT_FAILED:")[1]?.split("\n")[0]?.trim() ?? "unknown";
|
|
374
|
+
return { ok: false, reason: `.devbot/init.sh failed (exit ${exitCode}) — check the script`, isFirstRun: true, logs: redact(r.stdout + r.stderr) };
|
|
375
|
+
}
|
|
376
|
+
return { ok: false, reason: `clone failed: ${redact(r.stderr.slice(-512))}`, isFirstRun: true, logs: redact(r.stdout + r.stderr) };
|
|
377
|
+
}
|
|
378
|
+
return { ok: true, isFirstRun: true, submoduleWarnings: parseSubmoduleFailures(r.stdout), branchTip: parseBranchTip(r.stdout) };
|
|
379
|
+
}
|
|
380
|
+
// Existing volume: fetch + checkout. The branch transition is host-side and
|
|
381
|
+
// deterministic (see syncScript); the agent's build-mode prompt still owns
|
|
382
|
+
// the rebase step. Plan and build modes now share the same checkout chain —
|
|
383
|
+
// a plan-mode run no longer leaves HEAD on the clone target.
|
|
384
|
+
log.info("existing volume, syncing", { workItemId: input.workItemId, mode });
|
|
385
|
+
const syncCmd = syncScript(branch, extraHeader, mode, submoduleStep);
|
|
386
|
+
const r = await podman([
|
|
387
|
+
"run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-sync`,
|
|
388
|
+
"-v", `${vol}:/workspace:Z`,
|
|
389
|
+
...envArgs(envVars),
|
|
390
|
+
builderImage,
|
|
391
|
+
"sh", "-c", syncCmd,
|
|
392
|
+
], { timeoutMs: timeoutFor(mode) });
|
|
393
|
+
if (r.code !== 0) {
|
|
394
|
+
// fix #4: do not discard local work via fallback. Report and abort.
|
|
395
|
+
// The sync chain ends in an unconditional `checkout -B`, so a non-zero
|
|
396
|
+
// exit is a fetch/upstream failure or a pathological ref state — infra,
|
|
397
|
+
// not a rebase conflict (the rebase is agent-owned and never runs here).
|
|
398
|
+
// The stderr check distinguishes a missing work-item branch (plan mode,
|
|
399
|
+
// developer-actionable) from a fetch/upstream error.
|
|
400
|
+
const stderr = redact(r.stderr.slice(-512));
|
|
401
|
+
const missing = /pathspec|is not a commit/.test(stderr);
|
|
402
|
+
const reason = missing
|
|
403
|
+
? `work item branch '${branch}' not found locally or on origin (merged/deleted upstream?)`
|
|
404
|
+
: `workspace sync failed: ${stderr}`;
|
|
405
|
+
return { ok: false, reason, isFirstRun: false, logs: redact(r.stdout + r.stderr) };
|
|
406
|
+
}
|
|
407
|
+
return { ok: true, isFirstRun: false, submoduleWarnings: parseSubmoduleFailures(r.stdout), branchTip: parseBranchTip(r.stdout) };
|
|
408
|
+
}
|
|
409
|
+
// Push the work item branch. The agent has already committed its own work
|
|
410
|
+
// (Conventional Commits, #<iid> in subject — see the build-mode prompt suffix
|
|
411
|
+
// in run()), so this pushes directly. Push is always host-side — the
|
|
412
|
+
// container has no token; the extraHeader injects Basic auth for this one
|
|
413
|
+
// git call only.
|
|
414
|
+
// The host-side sync (syncScript) now places HEAD on the work branch
|
|
415
|
+
// deterministically before the agent runs; this function still carries a
|
|
416
|
+
// HEAD guard as the last-resort check: if HEAD is somehow elsewhere (agent
|
|
417
|
+
// ran git checkout itself, devcontainer init raced, etc.), abort the push
|
|
418
|
+
// and post an actionable note. This is the one check the agent cannot skip —
|
|
419
|
+
// it runs host-side before the push touches the remote. Exit 7 distinguishes
|
|
420
|
+
// a wrong-HEAD from a push failure.
|
|
421
|
+
// Compose the host-side publish script for a work item branch. Thin delegate to
|
|
422
|
+
// publishScript (src/conflict-fix.ts), kept for scripts/check-push-fallback.ts
|
|
423
|
+
// and any other pure-shape callers. Rewrite-aware: see publishScript.
|
|
424
|
+
export function pushScript(workItemNumber, branch, extraHeader, force = false) {
|
|
425
|
+
return publishScript(workItemNumber, branch, extraHeader, "", force);
|
|
426
|
+
}
|
|
427
|
+
// Compose the host-side script for a standalone `/force-push` (no /build, no
|
|
428
|
+
// agent run): force-push the work item's existing local branch to origin
|
|
429
|
+
// using --force-with-lease. Unlike pushScript there is no HEAD guard (the
|
|
430
|
+
// named ref is pushed, not HEAD) and no fallback commit (we are pushing, not
|
|
431
|
+
// committing). Exported as a pure helper for scripts/check-push-fallback.ts.
|
|
432
|
+
// Order is load-bearing:
|
|
433
|
+
// 1. branch guard — abort if the local wi-<iid> branch doesn't exist (exit 6,
|
|
434
|
+
// NO_BRANCH); there is nothing to push.
|
|
435
|
+
// 2. fetch — refresh origin/<branch> so the lease compares against
|
|
436
|
+
// the latest remote, not a stale tracking ref.
|
|
437
|
+
// 3. force-push — overwrite the remote branch iff it hasn't moved since
|
|
438
|
+
// the fetch (--force-with-lease, never plain --force).
|
|
439
|
+
export function forcePushStandaloneScript(branch, extraHeader) {
|
|
440
|
+
return [
|
|
441
|
+
"cd /workspace",
|
|
442
|
+
`git rev-parse --verify --quiet refs/heads/${shq(branch)} >/dev/null || { echo "NO_BRANCH:${branch}"; exit 6; }`,
|
|
443
|
+
`${extraHeader} fetch origin`,
|
|
444
|
+
`${extraHeader} push origin ${shq(branch)} --force-with-lease && echo PUSHED`,
|
|
445
|
+
].join(" && ");
|
|
446
|
+
}
|
|
447
|
+
// Compose the host-side script for a standalone `/push` (no /build, no agent
|
|
448
|
+
// run): publish the work item's existing local branch to origin without
|
|
449
|
+
// force. Never emits git's raw fast-forward hint: a branch whose remote is not
|
|
450
|
+
// an ancestor of HEAD (diverged, e.g. after a rebase) reports REWRITTEN and
|
|
451
|
+
// exits 9 so the caller can tell the user to use `/force-push`. Order:
|
|
452
|
+
// 1. branch guard — abort if the local branch doesn't exist (exit 6,
|
|
453
|
+
// NO_BRANCH).
|
|
454
|
+
// 2. fetch — refresh origin/<branch> for the ancestor/ahead tests.
|
|
455
|
+
// 3. unborn → plain push.
|
|
456
|
+
// ancestor → ahead ? plain push : NO_COMMITS.
|
|
457
|
+
// not ancestor → REWRITTEN, exit 9 (no clobber; use /force-push).
|
|
458
|
+
// Exported as a pure helper for scripts/check-push-fallback.ts.
|
|
459
|
+
export function pushStandaloneScript(branch, extraHeader) {
|
|
460
|
+
const b = shq(branch);
|
|
461
|
+
return [
|
|
462
|
+
"cd /workspace",
|
|
463
|
+
`git rev-parse --verify --quiet refs/heads/${b} >/dev/null || { echo "NO_BRANCH:${branch}"; exit 6; }`,
|
|
464
|
+
`${extraHeader} fetch origin`,
|
|
465
|
+
`if ! git rev-parse --verify --quiet origin/${b} >/dev/null; then ${extraHeader} push origin ${b} && echo PUSHED;`,
|
|
466
|
+
`elif git merge-base --is-ancestor origin/${b} HEAD; then if test -n "$(git log --format=%H origin/${b}..HEAD)"; then ${extraHeader} push origin ${b} && echo PUSHED; else echo NO_COMMITS; fi;`,
|
|
467
|
+
`else echo "REWRITTEN:${branch}"; exit 9; fi`,
|
|
468
|
+
].join(" && ");
|
|
469
|
+
}
|
|
470
|
+
export async function forcePushStandalone(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
|
|
471
|
+
const vol = volumeName(input.workItemId);
|
|
472
|
+
// Precondition: the workspace must already exist. A standalone force-push
|
|
473
|
+
// has nothing to do without it (no clone, no local commits) — fail with an
|
|
474
|
+
// actionable reason instead of letting podman create an empty volume and
|
|
475
|
+
// the script hit NO_BRANCH.
|
|
476
|
+
if (!(await volumeExists(vol))) {
|
|
477
|
+
return { ok: false, reason: `no workspace for this work item (volume ${vol} not found)`, missingVolume: true };
|
|
478
|
+
}
|
|
479
|
+
const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
|
|
480
|
+
const extraHeader = gitExtraHeader();
|
|
481
|
+
const r = await podman([
|
|
482
|
+
"run", "--rm", ...logDriverArgs(), "--name", workItemContainerName(input.workItemId, "forcepush"),
|
|
483
|
+
"-v", `${vol}:/workspace:Z`,
|
|
484
|
+
"-w", "/workspace",
|
|
485
|
+
...envArgs(envVars),
|
|
486
|
+
builderImage,
|
|
487
|
+
"sh", "-c",
|
|
488
|
+
forcePushStandaloneScript(branch, extraHeader),
|
|
489
|
+
], { timeoutMs: runtime().buildTimeoutMs });
|
|
490
|
+
if (r.code !== 0) {
|
|
491
|
+
// exit 6 + NO_BRANCH = the work item never produced a local branch.
|
|
492
|
+
if (r.code === 6 && r.stdout.includes("NO_BRANCH")) {
|
|
493
|
+
return { ok: false, reason: `no local branch '${branch}' to force-push`, logs: redact(r.stdout + r.stderr), missingBranch: true };
|
|
494
|
+
}
|
|
495
|
+
return { ok: false, reason: redact(r.stderr.slice(-256)), logs: redact(r.stdout + r.stderr) };
|
|
496
|
+
}
|
|
497
|
+
return { ok: true, pushed: r.stdout.includes("PUSHED") };
|
|
498
|
+
}
|
|
499
|
+
// Publish-the-existing-branch counterpart of forcePushStandalone for a
|
|
500
|
+
// standalone `/push`. Same preconditions and error taxonomy; the push itself
|
|
501
|
+
// is a plain fast-forward (pushStandaloneScript). `pushed` is false when the
|
|
502
|
+
// remote already has the branch at HEAD (NO_COMMITS) — an idempotent no-op.
|
|
503
|
+
export async function pushStandalone(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
|
|
504
|
+
const vol = volumeName(input.workItemId);
|
|
505
|
+
if (!(await volumeExists(vol))) {
|
|
506
|
+
return { ok: false, reason: `no workspace for this work item (volume ${vol} not found)`, missingVolume: true };
|
|
507
|
+
}
|
|
508
|
+
const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
|
|
509
|
+
const extraHeader = gitExtraHeader();
|
|
510
|
+
const r = await podman([
|
|
511
|
+
"run", "--rm", ...logDriverArgs(), "--name", workItemContainerName(input.workItemId, "push"),
|
|
512
|
+
"-v", `${vol}:/workspace:Z`,
|
|
513
|
+
"-w", "/workspace",
|
|
514
|
+
...envArgs(envVars),
|
|
515
|
+
builderImage,
|
|
516
|
+
"sh", "-c",
|
|
517
|
+
pushStandaloneScript(branch, extraHeader),
|
|
518
|
+
], { timeoutMs: runtime().buildTimeoutMs });
|
|
519
|
+
if (r.code !== 0) {
|
|
520
|
+
if (r.code === 6 && r.stdout.includes("NO_BRANCH")) {
|
|
521
|
+
return { ok: false, reason: `no local branch '${branch}' to push`, logs: redact(r.stdout + r.stderr), missingBranch: true };
|
|
522
|
+
}
|
|
523
|
+
if (r.code === 9 && r.stdout.includes("REWRITTEN")) {
|
|
524
|
+
return { ok: false, rewritten: true, reason: `branch '${branch}' has a rewritten history; use \`/force-push\` to publish it`, logs: redact(r.stdout + r.stderr) };
|
|
525
|
+
}
|
|
526
|
+
return { ok: false, reason: redact(r.stderr.slice(-256)), logs: redact(r.stdout + r.stderr) };
|
|
527
|
+
}
|
|
528
|
+
return { ok: true, pushed: r.stdout.includes("PUSHED") };
|
|
529
|
+
}
|
|
530
|
+
export async function pushBranch(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
|
|
531
|
+
const vol = volumeName(input.workItemId);
|
|
532
|
+
const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
|
|
533
|
+
const extraHeader = gitExtraHeader();
|
|
534
|
+
const r = await podman([
|
|
535
|
+
"run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-push`,
|
|
536
|
+
"-v", `${vol}:/workspace:Z`,
|
|
537
|
+
"-w", "/workspace",
|
|
538
|
+
...envArgs(envVars),
|
|
539
|
+
builderImage,
|
|
540
|
+
"sh", "-c",
|
|
541
|
+
publishScript(input.workItemNumber, branch, extraHeader, input.preRunTip ?? "", input.forcePush),
|
|
542
|
+
], { timeoutMs: runtime().buildTimeoutMs });
|
|
543
|
+
const parsed = parsePublishResult(r.stdout);
|
|
544
|
+
if (r.code === 7 && r.stdout.includes("WRONG_HEAD")) {
|
|
545
|
+
const onBranch = r.stdout.split("WRONG_HEAD:")[1]?.split("\n")[0]?.trim() ?? "unknown";
|
|
546
|
+
return { ok: false, reason: `agent left HEAD on '${onBranch}'; push skipped to avoid corrupting the MR`, logs: redact(r.stdout + r.stderr) };
|
|
547
|
+
}
|
|
548
|
+
if (r.code === 10 && r.stdout.includes("CONFLICT_MARKERS")) {
|
|
549
|
+
return { ok: false, reason: "the work tree still contains conflict markers; refusing to commit them", logs: redact(r.stdout + r.stderr) };
|
|
550
|
+
}
|
|
551
|
+
if (r.code === 9 && parsed.remoteMoved) {
|
|
552
|
+
return { ok: false, remoteMoved: true, reason: "the remote branch moved during the run; re-run to fetch and retry", logs: redact(r.stdout + r.stderr) };
|
|
553
|
+
}
|
|
554
|
+
if (r.code !== 0) {
|
|
555
|
+
return { ok: false, reason: redact(r.stderr.slice(-256)), logs: redact(r.stdout + r.stderr) };
|
|
556
|
+
}
|
|
557
|
+
return { ok: true, pushed: parsed.pushed, fallbackCommitted: parsed.fallbackCommitted, forced: parsed.forced, noCommits: parsed.noCommits };
|
|
558
|
+
}
|
|
559
|
+
// Run the conflict check in the builder container. Returns null on an infra
|
|
560
|
+
// failure (missing target, fetch error) so the caller can fail closed.
|
|
561
|
+
export async function checkConflicts(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
|
|
562
|
+
const vol = volumeName(input.workItemId);
|
|
563
|
+
const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
|
|
564
|
+
const extraHeader = gitExtraHeader();
|
|
565
|
+
const r = await podman([
|
|
566
|
+
"run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-conflictcheck`,
|
|
567
|
+
"-v", `${vol}:/workspace:Z`,
|
|
568
|
+
"-w", "/workspace",
|
|
569
|
+
...envArgs(envVars),
|
|
570
|
+
builderImage,
|
|
571
|
+
"sh", "-c",
|
|
572
|
+
conflictCheckScript(input.target, branch, extraHeader),
|
|
573
|
+
], { timeoutMs: runtime().buildTimeoutMs });
|
|
574
|
+
const parsed = parseConflictCheckResult(r.code, r.stdout);
|
|
575
|
+
if (!parsed) {
|
|
576
|
+
log.warn("conflict check failed", { workItemId: input.workItemId, code: r.code, stderr: redact(r.stderr.slice(-256)) });
|
|
577
|
+
}
|
|
578
|
+
return parsed;
|
|
579
|
+
}
|
|
580
|
+
// Read the remote-tracking tip of the work branch (`origin/<branch>`) without
|
|
581
|
+
// fetching — used to pin the force-with-lease after the resolver rewrites
|
|
582
|
+
// history. The conflict check that runs just before this already fetched, so
|
|
583
|
+
// the tracking ref is current and no auth is needed here (a bare `fetch` in a
|
|
584
|
+
// private repo would fail without the token). Returns null when absent.
|
|
585
|
+
export async function readRemoteBranchTip(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
|
|
586
|
+
const vol = volumeName(input.workItemId);
|
|
587
|
+
const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
|
|
588
|
+
const r = await podman([
|
|
589
|
+
"run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-tip`,
|
|
590
|
+
"-v", `${vol}:/workspace:Z`,
|
|
591
|
+
"-w", "/workspace",
|
|
592
|
+
...envArgs(envVars),
|
|
593
|
+
builderImage,
|
|
594
|
+
"sh", "-c",
|
|
595
|
+
`cd /workspace && git rev-parse origin/${shq(branch)} 2>/dev/null || true`,
|
|
596
|
+
], { timeoutMs: runtime().buildTimeoutMs });
|
|
597
|
+
const sha = r.stdout.trim();
|
|
598
|
+
return /^[0-9a-f]{7,40}$/.test(sha) ? sha : null;
|
|
599
|
+
}
|
|
600
|
+
// The resolver prompt for the direct build run that resolves conflicts.
|
|
601
|
+
export function conflictResolverPrompt(branch, target) {
|
|
602
|
+
return resolverPrompt(branch, target);
|
|
603
|
+
}
|
|
604
|
+
export { parseResolverResult };
|
|
605
|
+
//# sourceMappingURL=git.js.map
|