@urateam/core 0.1.42 → 0.1.43

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.
Files changed (100) hide show
  1. package/dist/__tests__/bitbucket-webhook.test.d.ts +13 -0
  2. package/dist/__tests__/bitbucket-webhook.test.d.ts.map +1 -0
  3. package/dist/__tests__/bitbucket-webhook.test.js +379 -0
  4. package/dist/__tests__/bitbucket-webhook.test.js.map +1 -0
  5. package/dist/__tests__/bitbucket.test.d.ts +15 -0
  6. package/dist/__tests__/bitbucket.test.d.ts.map +1 -0
  7. package/dist/__tests__/bitbucket.test.js +237 -0
  8. package/dist/__tests__/bitbucket.test.js.map +1 -0
  9. package/dist/__tests__/gitlab-webhook.test.d.ts +13 -0
  10. package/dist/__tests__/gitlab-webhook.test.d.ts.map +1 -0
  11. package/dist/__tests__/gitlab-webhook.test.js +388 -0
  12. package/dist/__tests__/gitlab-webhook.test.js.map +1 -0
  13. package/dist/__tests__/runner-multi-vcs.test.d.ts +19 -0
  14. package/dist/__tests__/runner-multi-vcs.test.d.ts.map +1 -0
  15. package/dist/__tests__/runner-multi-vcs.test.js +346 -0
  16. package/dist/__tests__/runner-multi-vcs.test.js.map +1 -0
  17. package/dist/__tests__/triage-v2-prediction.test.d.ts +2 -0
  18. package/dist/__tests__/triage-v2-prediction.test.d.ts.map +1 -0
  19. package/dist/__tests__/triage-v2-prediction.test.js +70 -0
  20. package/dist/__tests__/triage-v2-prediction.test.js.map +1 -0
  21. package/dist/__tests__/triage-v2-prompt.test.d.ts +2 -0
  22. package/dist/__tests__/triage-v2-prompt.test.d.ts.map +1 -0
  23. package/dist/__tests__/triage-v2-prompt.test.js +127 -0
  24. package/dist/__tests__/triage-v2-prompt.test.js.map +1 -0
  25. package/dist/__tests__/triage-v2-render.test.d.ts +2 -0
  26. package/dist/__tests__/triage-v2-render.test.d.ts.map +1 -0
  27. package/dist/__tests__/triage-v2-render.test.js +200 -0
  28. package/dist/__tests__/triage-v2-render.test.js.map +1 -0
  29. package/dist/__tests__/triage-v2-schema.test.d.ts +2 -0
  30. package/dist/__tests__/triage-v2-schema.test.d.ts.map +1 -0
  31. package/dist/__tests__/triage-v2-schema.test.js +115 -0
  32. package/dist/__tests__/triage-v2-schema.test.js.map +1 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/pipeline/feedback-pipeline.d.ts +2 -0
  37. package/dist/pipeline/feedback-pipeline.d.ts.map +1 -1
  38. package/dist/pipeline/feedback-pipeline.js +4 -1
  39. package/dist/pipeline/feedback-pipeline.js.map +1 -1
  40. package/dist/pipeline/runner.d.ts +11 -0
  41. package/dist/pipeline/runner.d.ts.map +1 -1
  42. package/dist/pipeline/runner.js +314 -114
  43. package/dist/pipeline/runner.js.map +1 -1
  44. package/dist/pm/actions/triage-prompt.d.ts +42 -0
  45. package/dist/pm/actions/triage-prompt.d.ts.map +1 -0
  46. package/dist/pm/actions/triage-prompt.js +192 -0
  47. package/dist/pm/actions/triage-prompt.js.map +1 -0
  48. package/dist/pm/actions/triage-render.d.ts +39 -0
  49. package/dist/pm/actions/triage-render.d.ts.map +1 -0
  50. package/dist/pm/actions/triage-render.js +158 -0
  51. package/dist/pm/actions/triage-render.js.map +1 -0
  52. package/dist/pm/actions/triage.d.ts +2 -1
  53. package/dist/pm/actions/triage.d.ts.map +1 -1
  54. package/dist/pm/actions/triage.js +44 -58
  55. package/dist/pm/actions/triage.js.map +1 -1
  56. package/dist/pm/triage-prediction-quality.d.ts +26 -0
  57. package/dist/pm/triage-prediction-quality.d.ts.map +1 -0
  58. package/dist/pm/triage-prediction-quality.js +41 -0
  59. package/dist/pm/triage-prediction-quality.js.map +1 -0
  60. package/dist/pm/types.d.ts +60 -0
  61. package/dist/pm/types.d.ts.map +1 -1
  62. package/dist/pm/types.js +119 -0
  63. package/dist/pm/types.js.map +1 -1
  64. package/dist/repo/bitbucket.d.ts +136 -0
  65. package/dist/repo/bitbucket.d.ts.map +1 -0
  66. package/dist/repo/bitbucket.js +237 -0
  67. package/dist/repo/bitbucket.js.map +1 -0
  68. package/dist/repo/gitlab.d.ts +11 -0
  69. package/dist/repo/gitlab.d.ts.map +1 -1
  70. package/dist/repo/gitlab.js +37 -0
  71. package/dist/repo/gitlab.js.map +1 -1
  72. package/dist/repo/index.d.ts +3 -1
  73. package/dist/repo/index.d.ts.map +1 -1
  74. package/dist/repo/index.js +2 -1
  75. package/dist/repo/index.js.map +1 -1
  76. package/dist/server.d.ts +14 -0
  77. package/dist/server.d.ts.map +1 -1
  78. package/dist/server.js +32 -0
  79. package/dist/server.js.map +1 -1
  80. package/dist/types.d.ts +1 -0
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/types.js +2 -2
  83. package/dist/types.js.map +1 -1
  84. package/dist/webhook/bitbucket-handler.d.ts +65 -0
  85. package/dist/webhook/bitbucket-handler.d.ts.map +1 -0
  86. package/dist/webhook/bitbucket-handler.js +153 -0
  87. package/dist/webhook/bitbucket-handler.js.map +1 -0
  88. package/dist/webhook/gitlab-handler.d.ts +66 -0
  89. package/dist/webhook/gitlab-handler.d.ts.map +1 -0
  90. package/dist/webhook/gitlab-handler.js +159 -0
  91. package/dist/webhook/gitlab-handler.js.map +1 -0
  92. package/dist/webhook/index.d.ts +3 -0
  93. package/dist/webhook/index.d.ts.map +1 -1
  94. package/dist/webhook/index.js +3 -0
  95. package/dist/webhook/index.js.map +1 -1
  96. package/dist/webhook/shared-handlers.d.ts +110 -0
  97. package/dist/webhook/shared-handlers.d.ts.map +1 -0
  98. package/dist/webhook/shared-handlers.js +251 -0
  99. package/dist/webhook/shared-handlers.js.map +1 -0
  100. package/package.json +1 -1
@@ -23,7 +23,8 @@ import { promisify } from "node:util";
23
23
  const execFileAsync = promisify(execFileCb);
24
24
  import { cloneRepo, createWorktree, deleteWorktree, pushBranch, pushBranchForce, choosePushStrategy, rebaseBranch, abortRebase, autoCommitChanges, getAgentCommits, createPRViaCli, mergePRViaCli, getDiffLineCount, getChangedFiles, checkDuplicateBranch, branchName, pruneWorktreesInRepoDirs, gitExecSafe, } from "../repo/git.js";
25
25
  import { addPRComment, createGitHubClient, createPR, prHasCommentStartingWith, } from "../repo/github.js";
26
- import { createMR, buildAuthenticatedUrl, } from "../repo/gitlab.js";
26
+ import { createMR, buildAuthenticatedUrl, addMRComment, mergeMRWhenPipelineSucceeds, } from "../repo/gitlab.js";
27
+ import { buildBitbucketAuthenticatedUrl, createBitbucketPR, addBitbucketPRComment, mergeBitbucketPR, parseBitbucketUrl, } from "../repo/bitbucket.js";
27
28
  import { parseRepoUrl, parseGitLabUrl } from "../repo/config.js";
28
29
  import { detectTechStack } from "../repo/tech-stack.js";
29
30
  import { shouldUseDevcontainer, devcontainerUp, devcontainerDown, } from "../repo/devcontainer.js";
@@ -65,8 +66,11 @@ export class PipelineRunner {
65
66
  repoCloneDir;
66
67
  githubConfig;
67
68
  gitlabConfig;
69
+ bitbucketConfig;
68
70
  lockAdapter;
69
71
  prLockTimeoutMs;
72
+ /** Memoised Octokit promise — created once per PipelineRunner instance. */
73
+ _octokitPromise;
70
74
  constructor(config) {
71
75
  this.db = config.db;
72
76
  this.notifier = config.notifier;
@@ -76,9 +80,24 @@ export class PipelineRunner {
76
80
  this.repoCloneDir = config.repoCloneDir ?? join(homedir(), "work", "repos");
77
81
  this.githubConfig = config.github;
78
82
  this.gitlabConfig = config.gitlab;
83
+ this.bitbucketConfig = config.bitbucket;
79
84
  this.lockAdapter = createBranchLockAdapter(config.db);
80
85
  this.prLockTimeoutMs = config.prLockTimeoutMs ?? 120_000;
81
86
  }
87
+ /**
88
+ * Lazy-memoised Octokit instance — created at most once per PipelineRunner.
89
+ * Multiple concurrent callers share the same Promise so construction happens
90
+ * exactly once even under parallel await.
91
+ */
92
+ getOctokit() {
93
+ if (!this._octokitPromise) {
94
+ if (!this.githubConfig)
95
+ throw new Error("githubConfig required for Octokit");
96
+ this._octokitPromise = createGitHubClient(this.githubConfig);
97
+ }
98
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
99
+ return this._octokitPromise;
100
+ }
82
101
  async start(issue, pipelineKey, pipelineConfig, repoConfig, sanitizedIssue, linearTeamId = null) {
83
102
  log.info({ issueId: issue.identifier, pipeline: pipelineKey }, "start() called");
84
103
  if (this.activeRuns.has(issue.identifier)) {
@@ -413,6 +432,7 @@ export class PipelineRunner {
413
432
  agentRunDir: this.agentRunDir,
414
433
  githubConfig: this.githubConfig,
415
434
  gitlabConfig: this.gitlabConfig,
435
+ bitbucketConfig: this.bitbucketConfig,
416
436
  pushQueue: this.pushQueue,
417
437
  lockAdapter: this.lockAdapter,
418
438
  prLockTimeoutMs: this.prLockTimeoutMs,
@@ -498,10 +518,12 @@ export class PipelineRunner {
498
518
  // Fresh start — clone repository, create worktree, run setup.
499
519
  // ---------------------------------------------------------------
500
520
  const repoDir = `${this.repoCloneDir}/${sanitizedIssue.slug}`;
501
- // Inject credentials for GitLab private repos
521
+ // Inject credentials for GitLab / Bitbucket private repos
502
522
  const cloneUrl = (repoConfig.provider === "gitlab" && this.gitlabConfig)
503
523
  ? buildAuthenticatedUrl(repoConfig.url, this.gitlabConfig)
504
- : repoConfig.url;
524
+ : (repoConfig.provider === "bitbucket" && this.bitbucketConfig)
525
+ ? buildBitbucketAuthenticatedUrl(repoConfig.url, this.bitbucketConfig)
526
+ : repoConfig.url;
505
527
  const logUrl = cloneUrl.replace(/:\/\/[^@]+@/, "://[redacted]@");
506
528
  runLog.info({ repoUrl: logUrl, repoDir }, "cloning repository");
507
529
  await cloneRepo(cloneUrl, repoDir);
@@ -1509,6 +1531,27 @@ export class PipelineRunner {
1509
1531
  // via a DB advisory lock (Postgres) so they can't race on PR creation for
1510
1532
  // the same branch. If the lock cannot be acquired within prLockTimeoutMs,
1511
1533
  // the pipeline fails with a LockTimeoutError.
1534
+ // Parse the repo URL once here — repoConfig.url is constant for the
1535
+ // lifetime of this pipeline run. All GitHub/gh-CLI call sites below
1536
+ // consume parsedRepoUrl instead of re-parsing the same string.
1537
+ // Null on parse failure so the gh-CLI fallback path (which tolerates
1538
+ // a missing owner) keeps working; GitHub-App paths use
1539
+ // requireParsedRepoUrl() below to surface a clear error instead of
1540
+ // crashing on a non-null assertion.
1541
+ const parsedRepoUrl = (() => {
1542
+ try {
1543
+ return parseRepoUrl(repoConfig.url);
1544
+ }
1545
+ catch {
1546
+ return null;
1547
+ }
1548
+ })();
1549
+ const requireParsedRepoUrl = () => {
1550
+ if (!parsedRepoUrl) {
1551
+ throw new Error(`PipelineRunner: failed to parse repo URL '${repoConfig.url}' — cannot interact with GitHub API`);
1552
+ }
1553
+ return parsedRepoUrl;
1554
+ };
1512
1555
  let prUrl = "";
1513
1556
  let autoMerged = false;
1514
1557
  await this.pushQueue.enqueue(async () => {
@@ -1598,6 +1641,7 @@ export class PipelineRunner {
1598
1641
  agentCommits,
1599
1642
  });
1600
1643
  const isGitLab = repoConfig.provider === "gitlab";
1644
+ const isBitbucket = repoConfig.provider === "bitbucket";
1601
1645
  // Mandatory reviewer request (enterprise feature 4.6). Only non-null
1602
1646
  // when the org-policy feature is licensed and the pipeline config
1603
1647
  // specifies mandatoryReviewers.
@@ -1624,11 +1668,31 @@ export class PipelineRunner {
1624
1668
  runLog.error({ err: mrError }, "MR creation via GitLab API failed");
1625
1669
  }
1626
1670
  }
1627
- else if (!isGitLab && this.githubConfig) {
1671
+ else if (isBitbucket && this.bitbucketConfig) {
1672
+ // Bitbucket — create PR via REST API
1673
+ try {
1674
+ const { workspace, repoSlug } = parseBitbucketUrl(repoConfig.url);
1675
+ prUrl = await createBitbucketPR(this.bitbucketConfig, {
1676
+ workspace,
1677
+ repoSlug,
1678
+ sourceBranch: branch,
1679
+ targetBranch: repoConfig.defaultBranch,
1680
+ title: sanitizedIssue.title,
1681
+ description: prBody,
1682
+ draft: shouldDraft,
1683
+ });
1684
+ run.prUrl = prUrl;
1685
+ runLog.info({ prUrl }, "PR created via Bitbucket API");
1686
+ }
1687
+ catch (prError) {
1688
+ runLog.error({ err: prError }, "PR creation via Bitbucket API failed");
1689
+ }
1690
+ }
1691
+ else if (!isGitLab && !isBitbucket && this.githubConfig) {
1628
1692
  // GitHub App — use Octokit API
1629
1693
  try {
1630
- const { owner, repo } = parseRepoUrl(repoConfig.url);
1631
- const octokit = await createGitHubClient(this.githubConfig);
1694
+ const { owner, repo } = requireParsedRepoUrl();
1695
+ const octokit = await this.getOctokit();
1632
1696
  prUrl = await createPR(octokit, {
1633
1697
  owner,
1634
1698
  repo,
@@ -1646,17 +1710,10 @@ export class PipelineRunner {
1646
1710
  runLog.error({ err: prError }, "PR creation via GitHub App failed");
1647
1711
  }
1648
1712
  }
1649
- else {
1713
+ else if (!isGitLab && !isBitbucket) {
1650
1714
  // No provider-specific config — use gh CLI
1651
1715
  runLog.info("creating PR via gh CLI");
1652
- const { owner: ghOwner } = (() => {
1653
- try {
1654
- return parseRepoUrl(repoConfig.url);
1655
- }
1656
- catch {
1657
- return { owner: undefined };
1658
- }
1659
- })();
1716
+ const ghOwner = parsedRepoUrl?.owner;
1660
1717
  prUrl = await createPRViaCli({
1661
1718
  worktreePath: wtPath,
1662
1719
  branch,
@@ -1684,10 +1741,11 @@ export class PipelineRunner {
1684
1741
  }
1685
1742
  // BEC-134: Post fanout (per-model) review comments on the PR. Best-effort —
1686
1743
  // failures never block the pipeline. Requires the GitHub App for Octokit
1687
- // access; the gh-CLI/GitLab paths skip this (parity gap accepted for v1).
1744
+ // access; the gh-CLI/GitLab/Bitbucket paths skip this (parity gap accepted for v1).
1688
1745
  if (prUrl &&
1689
1746
  pendingFanoutRuns.length > 0 &&
1690
1747
  !isGitLab &&
1748
+ !isBitbucket &&
1691
1749
  this.githubConfig) {
1692
1750
  const fanoutPrNumberMatch = prUrl.match(/\/pull\/(\d+)/);
1693
1751
  const fanoutPrNumber = fanoutPrNumberMatch
@@ -1695,8 +1753,8 @@ export class PipelineRunner {
1695
1753
  : null;
1696
1754
  if (fanoutPrNumber !== null) {
1697
1755
  try {
1698
- const { owner: fanoutOwner, repo: fanoutRepo } = parseRepoUrl(repoConfig.url);
1699
- const fanoutOctokit = await createGitHubClient(this.githubConfig);
1756
+ const { owner: fanoutOwner, repo: fanoutRepo } = requireParsedRepoUrl();
1757
+ const fanoutOctokit = await this.getOctokit();
1700
1758
  const fanoutResult = await postFanoutCommentsToPR(fanoutOctokit, fanoutOwner, fanoutRepo, fanoutPrNumber, pendingFanoutRuns);
1701
1759
  runLog.info({
1702
1760
  prNumber: fanoutPrNumber,
@@ -1782,10 +1840,9 @@ export class PipelineRunner {
1782
1840
  : `${ralphGaps.length} unmet acceptance criteria`;
1783
1841
  await this.notifier.onHumanReviewNeeded?.(run, prUrl, `Draft PR created — ${ralphSummary}, ${unresolvedBlockingFindings.length} blocking findings`);
1784
1842
  }
1785
- // 6. Auto-merge (skip drafts, unresolved conflicts, or GitLab)
1843
+ // 6. Auto-merge (skip drafts, unresolved conflicts)
1786
1844
  const maxLines = config.autoMergeMaxLines ?? 200;
1787
- const isGitLabRepo = repoConfig.provider === "gitlab";
1788
- if (config.autoMerge && prUrl && !rebaseConflict && !isGitLabRepo && !shouldDraft) {
1845
+ if (config.autoMerge && prUrl && !rebaseConflict && !shouldDraft) {
1789
1846
  const diffLines = await getDiffLineCount(wtPath, repoConfig.defaultBranch);
1790
1847
  const lastHandoff = handoff;
1791
1848
  const hasBlockingFindings = lastHandoff?.context?.reviewFindings?.some((f) => f.severity === "blocking");
@@ -1815,7 +1872,7 @@ export class PipelineRunner {
1815
1872
  // Known limitation: the reviewer check requires an Octokit API
1816
1873
  // client (to call pulls.listReviews / teams.listMembersInOrg), so
1817
1874
  // it only fires when the GitHub App is configured. The `gh` CLI
1818
- // fallback and GitLab paths skip this check — documented in the
1875
+ // fallback and GitLab/Bitbucket paths skip this check — documented in the
1819
1876
  // plan as acceptable because production deployments use the App.
1820
1877
  if (shouldMerge && isFeatureLicensed("org-policy")) {
1821
1878
  const policyReviewerRequest = buildReviewerRequest(config.policy);
@@ -1833,7 +1890,7 @@ export class PipelineRunner {
1833
1890
  : undefined;
1834
1891
  if (owner && repo && prNumber) {
1835
1892
  try {
1836
- const octokit = await createGitHubClient(this.githubConfig);
1893
+ const octokit = await this.getOctokit();
1837
1894
  const check = await verifyApprovalsReceived(octokit, owner, repo, prNumber, policyReviewerRequest);
1838
1895
  if (!check.satisfied) {
1839
1896
  shouldMerge = false;
@@ -1855,16 +1912,81 @@ export class PipelineRunner {
1855
1912
  }
1856
1913
  }
1857
1914
  if (shouldMerge) {
1858
- runLog.info({ diffLines, maxLines }, "auto-merge eligible, merging PR");
1859
- autoMerged = await mergePRViaCli(wtPath, branch);
1860
- if (autoMerged) {
1861
- autoMergeReason = "PR auto-merged successfully";
1862
- runLog.info({ prUrl }, "PR auto-merged");
1915
+ runLog.info({ diffLines, maxLines, provider: repoConfig.provider }, "auto-merge eligible, merging PR");
1916
+ if (isGitLab && this.gitlabConfig) {
1917
+ // GitLab: use merge_when_pipeline_succeeds API
1918
+ try {
1919
+ const { projectPath } = parseGitLabUrl(repoConfig.url);
1920
+ // Extract MR IID from URL: .../-/merge_requests/42
1921
+ const mrIidMatch = prUrl.match(/\/merge_requests\/(\d+)/);
1922
+ const mrIid = mrIidMatch ? parseInt(mrIidMatch[1], 10) : null;
1923
+ if (mrIid !== null) {
1924
+ autoMerged = await mergeMRWhenPipelineSucceeds(this.gitlabConfig, projectPath, mrIid);
1925
+ if (autoMerged) {
1926
+ autoMergeReason = "PR auto-merged via GitLab merge_when_pipeline_succeeds";
1927
+ runLog.info({ prUrl }, "GitLab MR queued for merge when pipeline succeeds");
1928
+ }
1929
+ else {
1930
+ autoMergeReason = "GitLab merge_when_pipeline_succeeds API call failed";
1931
+ runLog.warn("GitLab auto-merge failed, sending human review alert");
1932
+ await this.notifier.onHumanReviewNeeded?.(run, prUrl, "GitLab auto-merge failed — please merge manually");
1933
+ }
1934
+ }
1935
+ else {
1936
+ autoMergeReason = "Could not parse MR IID from URL";
1937
+ runLog.warn({ prUrl }, "GitLab auto-merge: could not parse MR IID");
1938
+ await this.notifier.onHumanReviewNeeded?.(run, prUrl, "GitLab auto-merge failed — could not determine MR ID");
1939
+ }
1940
+ }
1941
+ catch (err) {
1942
+ autoMergeReason = "GitLab auto-merge threw an error";
1943
+ runLog.error({ err }, "GitLab auto-merge error");
1944
+ await this.notifier.onHumanReviewNeeded?.(run, prUrl, "GitLab auto-merge failed — please merge manually");
1945
+ }
1946
+ }
1947
+ else if (isBitbucket && this.bitbucketConfig) {
1948
+ // Bitbucket: use PR merge API
1949
+ try {
1950
+ const { workspace, repoSlug } = parseBitbucketUrl(repoConfig.url);
1951
+ // Extract PR ID from URL: .../pull-requests/42
1952
+ const prIdMatch = prUrl.match(/\/pull-requests\/(\d+)/);
1953
+ const prId = prIdMatch ? parseInt(prIdMatch[1], 10) : null;
1954
+ if (prId !== null) {
1955
+ autoMerged = await mergeBitbucketPR(this.bitbucketConfig, workspace, repoSlug, prId);
1956
+ if (autoMerged) {
1957
+ autoMergeReason = "PR auto-merged via Bitbucket API";
1958
+ runLog.info({ prUrl }, "Bitbucket PR merged");
1959
+ }
1960
+ else {
1961
+ autoMergeReason = "Bitbucket merge API call failed";
1962
+ runLog.warn("Bitbucket auto-merge failed, sending human review alert");
1963
+ await this.notifier.onHumanReviewNeeded?.(run, prUrl, "Bitbucket auto-merge failed — please merge manually");
1964
+ }
1965
+ }
1966
+ else {
1967
+ autoMergeReason = "Could not parse PR ID from Bitbucket URL";
1968
+ runLog.warn({ prUrl }, "Bitbucket auto-merge: could not parse PR ID");
1969
+ await this.notifier.onHumanReviewNeeded?.(run, prUrl, "Bitbucket auto-merge failed — could not determine PR ID");
1970
+ }
1971
+ }
1972
+ catch (err) {
1973
+ autoMergeReason = "Bitbucket auto-merge threw an error";
1974
+ runLog.error({ err }, "Bitbucket auto-merge error");
1975
+ await this.notifier.onHumanReviewNeeded?.(run, prUrl, "Bitbucket auto-merge failed — please merge manually");
1976
+ }
1863
1977
  }
1864
1978
  else {
1865
- autoMergeReason = "Auto-merge command failed";
1866
- runLog.warn("auto-merge failed, sending human review alert");
1867
- await this.notifier.onHumanReviewNeeded?.(run, prUrl, "Auto-merge failed — please merge manually");
1979
+ // GitHub / gh CLI fallback
1980
+ autoMerged = await mergePRViaCli(wtPath, branch);
1981
+ if (autoMerged) {
1982
+ autoMergeReason = "PR auto-merged successfully";
1983
+ runLog.info({ prUrl }, "PR auto-merged");
1984
+ }
1985
+ else {
1986
+ autoMergeReason = "Auto-merge command failed";
1987
+ runLog.warn("auto-merge failed, sending human review alert");
1988
+ await this.notifier.onHumanReviewNeeded?.(run, prUrl, "Auto-merge failed — please merge manually");
1989
+ }
1868
1990
  }
1869
1991
  }
1870
1992
  else {
@@ -1914,65 +2036,89 @@ export class PipelineRunner {
1914
2036
  });
1915
2037
  // BEC-175: optional per-PR cost summary comment. Opt-in via
1916
2038
  // URATEAM_PR_COST_SUMMARY=true. Best-effort — failures never block
1917
- // pipeline completion.
2039
+ // pipeline completion. Supported on GitHub, GitLab, and Bitbucket.
2040
+ // BEC-206: provider booleans must be local to this block — the
2041
+ // declarations inside the push queue scope are not visible here.
2042
+ const isGitLab = repoConfig.provider === "gitlab";
2043
+ const isBitbucket = repoConfig.provider === "bitbucket";
1918
2044
  if (process.env.URATEAM_PR_COST_SUMMARY === "true" &&
1919
- prUrl &&
1920
- repoConfig.provider !== "gitlab" &&
1921
- this.githubConfig) {
2045
+ prUrl) {
1922
2046
  try {
1923
- const summaryPrMatch = prUrl.match(/\/pull\/(\d+)/);
1924
- const summaryPrNumber = summaryPrMatch
1925
- ? parseInt(summaryPrMatch[1], 10)
1926
- : null;
1927
- if (summaryPrNumber !== null) {
1928
- // Fetch stage runs first; reviewModelRuns query depends on the
1929
- // resulting stageIds so the two queries are necessarily sequential.
1930
- const stages = await this.db
2047
+ // Build cost body regardless of provider
2048
+ const stages = await this.db
2049
+ .select()
2050
+ .from(stageRuns)
2051
+ .where(eq(stageRuns.pipelineRunId, runId));
2052
+ const stageIds = stages.map((s) => s.id);
2053
+ const modelRows = stageIds.length > 0
2054
+ ? await this.db
1931
2055
  .select()
1932
- .from(stageRuns)
1933
- .where(eq(stageRuns.pipelineRunId, runId));
1934
- const stageIds = stages.map((s) => s.id);
1935
- const modelRows = stageIds.length > 0
1936
- ? await this.db
1937
- .select()
1938
- .from(reviewModelRuns)
1939
- .where(inArray(reviewModelRuns.stageRunId, stageIds))
1940
- : [];
1941
- const modelsByStage = new Map();
1942
- for (const mr of modelRows) {
1943
- const arr = modelsByStage.get(mr.stageRunId) ?? [];
1944
- arr.push({
1945
- modelId: mr.modelId,
1946
- inputTokens: mr.inputTokens,
1947
- outputTokens: mr.outputTokens,
1948
- });
1949
- modelsByStage.set(mr.stageRunId, arr);
1950
- }
1951
- const breakdown = stages.map((s) => ({
1952
- stage: s.stage,
1953
- inputTokens: s.inputTokens,
1954
- outputTokens: s.outputTokens,
1955
- cacheCreationInputTokens: s.cacheCreationInputTokens ?? 0,
1956
- cacheReadInputTokens: s.cacheReadInputTokens ?? 0,
1957
- modelRuns: modelsByStage.get(s.id),
1958
- }));
1959
- const body = formatPRCostSummary(breakdown, run.pipelineKey, {
1960
- pipelineConfigs: { [run.pipelineKey]: config },
2056
+ .from(reviewModelRuns)
2057
+ .where(inArray(reviewModelRuns.stageRunId, stageIds))
2058
+ : [];
2059
+ const modelsByStage = new Map();
2060
+ for (const mr of modelRows) {
2061
+ const arr = modelsByStage.get(mr.stageRunId) ?? [];
2062
+ arr.push({
2063
+ modelId: mr.modelId,
2064
+ inputTokens: mr.inputTokens,
2065
+ outputTokens: mr.outputTokens,
1961
2066
  });
1962
- if (body) {
1963
- const { owner: summaryOwner, repo: summaryRepo } = parseRepoUrl(repoConfig.url);
1964
- const summaryOctokit = await createGitHubClient(this.githubConfig);
1965
- // Dedup: skip when a prior pipeline run on this PR already
1966
- // posted a cost summary. We use the markdown header as the
1967
- // sentinel so the check survives any token/dollar diff between
1968
- // runs.
1969
- const alreadyPosted = await prHasCommentStartingWith(summaryOctokit, summaryOwner, summaryRepo, summaryPrNumber, "🤖 **Pipeline cost summary**");
1970
- if (alreadyPosted) {
1971
- runLog.info({ prNumber: summaryPrNumber }, "BEC-175: cost summary already exists on PR — skipping");
2067
+ modelsByStage.set(mr.stageRunId, arr);
2068
+ }
2069
+ const breakdown = stages.map((s) => ({
2070
+ stage: s.stage,
2071
+ inputTokens: s.inputTokens,
2072
+ outputTokens: s.outputTokens,
2073
+ cacheCreationInputTokens: s.cacheCreationInputTokens ?? 0,
2074
+ cacheReadInputTokens: s.cacheReadInputTokens ?? 0,
2075
+ modelRuns: modelsByStage.get(s.id),
2076
+ }));
2077
+ const costBody = formatPRCostSummary(breakdown, run.pipelineKey, {
2078
+ pipelineConfigs: { [run.pipelineKey]: config },
2079
+ });
2080
+ if (costBody) {
2081
+ if (isGitLab && this.gitlabConfig) {
2082
+ // GitLab: post via REST API
2083
+ const { projectPath } = parseGitLabUrl(repoConfig.url);
2084
+ const mrIidMatch = prUrl.match(/\/merge_requests\/(\d+)/);
2085
+ const mrIid = mrIidMatch ? parseInt(mrIidMatch[1], 10) : null;
2086
+ if (mrIid !== null) {
2087
+ await addMRComment(this.gitlabConfig, projectPath, mrIid, costBody);
2088
+ runLog.info({ mrIid }, "BEC-175: posted GitLab MR cost summary");
1972
2089
  }
1973
- else {
1974
- await addPRComment(summaryOctokit, summaryOwner, summaryRepo, summaryPrNumber, body);
1975
- runLog.info({ prNumber: summaryPrNumber }, "BEC-175: posted PR cost summary");
2090
+ }
2091
+ else if (isBitbucket && this.bitbucketConfig) {
2092
+ // Bitbucket: post via REST API
2093
+ const { workspace, repoSlug } = parseBitbucketUrl(repoConfig.url);
2094
+ const prIdMatch = prUrl.match(/\/pull-requests\/(\d+)/);
2095
+ const prId = prIdMatch ? parseInt(prIdMatch[1], 10) : null;
2096
+ if (prId !== null) {
2097
+ await addBitbucketPRComment(this.bitbucketConfig, workspace, repoSlug, prId, costBody);
2098
+ runLog.info({ prId }, "BEC-175: posted Bitbucket PR cost summary");
2099
+ }
2100
+ }
2101
+ else if (this.githubConfig) {
2102
+ // GitHub: use Octokit with dedup check
2103
+ const summaryPrMatch = prUrl.match(/\/pull\/(\d+)/);
2104
+ const summaryPrNumber = summaryPrMatch
2105
+ ? parseInt(summaryPrMatch[1], 10)
2106
+ : null;
2107
+ if (summaryPrNumber !== null) {
2108
+ const { owner: summaryOwner, repo: summaryRepo } = requireParsedRepoUrl();
2109
+ const summaryOctokit = await this.getOctokit();
2110
+ // Dedup: skip when a prior pipeline run on this PR already
2111
+ // posted a cost summary. We use the markdown header as the
2112
+ // sentinel so the check survives any token/dollar diff between
2113
+ // runs.
2114
+ const alreadyPosted = await prHasCommentStartingWith(summaryOctokit, summaryOwner, summaryRepo, summaryPrNumber, "🤖 **Pipeline cost summary**");
2115
+ if (alreadyPosted) {
2116
+ runLog.info({ prNumber: summaryPrNumber }, "BEC-175: cost summary already exists on PR — skipping");
2117
+ }
2118
+ else {
2119
+ await addPRComment(summaryOctokit, summaryOwner, summaryRepo, summaryPrNumber, costBody);
2120
+ runLog.info({ prNumber: summaryPrNumber }, "BEC-175: posted PR cost summary");
2121
+ }
1976
2122
  }
1977
2123
  }
1978
2124
  }
@@ -1983,36 +2129,90 @@ export class PipelineRunner {
1983
2129
  }
1984
2130
  // PR change-summary comment for review-feedback runs. Always-on (no env
1985
2131
  // flag) — a review-feedback run only exists because a human asked for
1986
- // changes, so silent shipping is a bug.
1987
- if (run.runType === "review-feedback" &&
1988
- prUrl &&
1989
- repoConfig.provider !== "gitlab" &&
1990
- this.githubConfig) {
2132
+ // changes, so silent shipping is a bug. Supported on all providers.
2133
+ if (run.runType === "review-feedback" && prUrl) {
1991
2134
  try {
1992
- const summaryPrMatch = prUrl.match(/\/pull\/(\d+)/);
1993
- const summaryPrNumber = summaryPrMatch
1994
- ? parseInt(summaryPrMatch[1], 10)
1995
- : null;
1996
- const { owner: csOwner, repo: csRepo } = parseRepoUrl(repoConfig.url);
1997
- const csOctokit = await createGitHubClient(this.githubConfig);
1998
- await maybePostChangeSummary({
1999
- run: {
2000
- id: run.id,
2001
- runType: run.runType,
2002
- prUrl: run.prUrl,
2003
- feedbackContext: run.feedbackContext ?? null,
2004
- totalInputTokens: run.totalInputTokens,
2005
- totalOutputTokens: run.totalOutputTokens,
2006
- },
2007
- handoff: handoff ?? null,
2008
- prNumber: summaryPrNumber,
2009
- owner: csOwner,
2010
- repo: csRepo,
2011
- octokit: csOctokit,
2012
- postPRComment: addPRComment,
2013
- dashboardBaseUrl: process.env.URATEAM_DASHBOARD_URL ?? "",
2014
- logger: runLog,
2015
- });
2135
+ if (isGitLab && this.gitlabConfig) {
2136
+ // GitLab: build change summary and post via addMRComment
2137
+ const { projectPath } = parseGitLabUrl(repoConfig.url);
2138
+ const mrIidMatch = prUrl.match(/\/merge_requests\/(\d+)/);
2139
+ const mrIid = mrIidMatch ? parseInt(mrIidMatch[1], 10) : null;
2140
+ if (mrIid !== null && handoff) {
2141
+ const { renderChangeSummary } = await import("./pr-change-summary.js");
2142
+ let triggeringComments = [];
2143
+ if (run.feedbackContext) {
2144
+ try {
2145
+ const parsed = JSON.parse(run.feedbackContext);
2146
+ if (Array.isArray(parsed))
2147
+ triggeringComments = parsed;
2148
+ }
2149
+ catch { /* ignore */ }
2150
+ }
2151
+ const csBody = renderChangeSummary({
2152
+ handoff,
2153
+ run: { id: run.id, totalInputTokens: run.totalInputTokens, totalOutputTokens: run.totalOutputTokens },
2154
+ triggeringComments,
2155
+ dashboardBaseUrl: process.env.URATEAM_DASHBOARD_URL ?? "",
2156
+ prUrl,
2157
+ });
2158
+ await addMRComment(this.gitlabConfig, projectPath, mrIid, csBody);
2159
+ runLog.info({ mrIid }, "posted GitLab MR change summary for review-feedback run");
2160
+ }
2161
+ }
2162
+ else if (isBitbucket && this.bitbucketConfig) {
2163
+ // Bitbucket: build change summary and post via addBitbucketPRComment
2164
+ const { workspace, repoSlug } = parseBitbucketUrl(repoConfig.url);
2165
+ const prIdMatch = prUrl.match(/\/pull-requests\/(\d+)/);
2166
+ const prId = prIdMatch ? parseInt(prIdMatch[1], 10) : null;
2167
+ if (prId !== null && handoff) {
2168
+ const { renderChangeSummary } = await import("./pr-change-summary.js");
2169
+ let triggeringComments = [];
2170
+ if (run.feedbackContext) {
2171
+ try {
2172
+ const parsed = JSON.parse(run.feedbackContext);
2173
+ if (Array.isArray(parsed))
2174
+ triggeringComments = parsed;
2175
+ }
2176
+ catch { /* ignore */ }
2177
+ }
2178
+ const csBody = renderChangeSummary({
2179
+ handoff,
2180
+ run: { id: run.id, totalInputTokens: run.totalInputTokens, totalOutputTokens: run.totalOutputTokens },
2181
+ triggeringComments,
2182
+ dashboardBaseUrl: process.env.URATEAM_DASHBOARD_URL ?? "",
2183
+ prUrl,
2184
+ });
2185
+ await addBitbucketPRComment(this.bitbucketConfig, workspace, repoSlug, prId, csBody);
2186
+ runLog.info({ prId }, "posted Bitbucket PR change summary for review-feedback run");
2187
+ }
2188
+ }
2189
+ else {
2190
+ // GitHub: use existing maybePostChangeSummary helper
2191
+ const summaryPrMatch = prUrl.match(/\/pull\/(\d+)/);
2192
+ const summaryPrNumber = summaryPrMatch
2193
+ ? parseInt(summaryPrMatch[1], 10)
2194
+ : null;
2195
+ const { owner: csOwner, repo: csRepo } = requireParsedRepoUrl();
2196
+ const csOctokit = await this.getOctokit();
2197
+ await maybePostChangeSummary({
2198
+ run: {
2199
+ id: run.id,
2200
+ runType: run.runType,
2201
+ prUrl: run.prUrl,
2202
+ feedbackContext: run.feedbackContext ?? null,
2203
+ totalInputTokens: run.totalInputTokens,
2204
+ totalOutputTokens: run.totalOutputTokens,
2205
+ },
2206
+ handoff: handoff ?? null,
2207
+ prNumber: summaryPrNumber,
2208
+ owner: csOwner,
2209
+ repo: csRepo,
2210
+ octokit: csOctokit,
2211
+ postPRComment: addPRComment,
2212
+ dashboardBaseUrl: process.env.URATEAM_DASHBOARD_URL ?? "",
2213
+ logger: runLog,
2214
+ });
2215
+ }
2016
2216
  }
2017
2217
  catch (err) {
2018
2218
  runLog.warn({ err: err instanceof Error ? err.message : String(err) }, "PR change summary post failed (non-fatal)");