@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.
- package/dist/__tests__/bitbucket-webhook.test.d.ts +13 -0
- package/dist/__tests__/bitbucket-webhook.test.d.ts.map +1 -0
- package/dist/__tests__/bitbucket-webhook.test.js +379 -0
- package/dist/__tests__/bitbucket-webhook.test.js.map +1 -0
- package/dist/__tests__/bitbucket.test.d.ts +15 -0
- package/dist/__tests__/bitbucket.test.d.ts.map +1 -0
- package/dist/__tests__/bitbucket.test.js +237 -0
- package/dist/__tests__/bitbucket.test.js.map +1 -0
- package/dist/__tests__/gitlab-webhook.test.d.ts +13 -0
- package/dist/__tests__/gitlab-webhook.test.d.ts.map +1 -0
- package/dist/__tests__/gitlab-webhook.test.js +388 -0
- package/dist/__tests__/gitlab-webhook.test.js.map +1 -0
- package/dist/__tests__/runner-multi-vcs.test.d.ts +19 -0
- package/dist/__tests__/runner-multi-vcs.test.d.ts.map +1 -0
- package/dist/__tests__/runner-multi-vcs.test.js +346 -0
- package/dist/__tests__/runner-multi-vcs.test.js.map +1 -0
- package/dist/__tests__/triage-v2-prediction.test.d.ts +2 -0
- package/dist/__tests__/triage-v2-prediction.test.d.ts.map +1 -0
- package/dist/__tests__/triage-v2-prediction.test.js +70 -0
- package/dist/__tests__/triage-v2-prediction.test.js.map +1 -0
- package/dist/__tests__/triage-v2-prompt.test.d.ts +2 -0
- package/dist/__tests__/triage-v2-prompt.test.d.ts.map +1 -0
- package/dist/__tests__/triage-v2-prompt.test.js +127 -0
- package/dist/__tests__/triage-v2-prompt.test.js.map +1 -0
- package/dist/__tests__/triage-v2-render.test.d.ts +2 -0
- package/dist/__tests__/triage-v2-render.test.d.ts.map +1 -0
- package/dist/__tests__/triage-v2-render.test.js +200 -0
- package/dist/__tests__/triage-v2-render.test.js.map +1 -0
- package/dist/__tests__/triage-v2-schema.test.d.ts +2 -0
- package/dist/__tests__/triage-v2-schema.test.d.ts.map +1 -0
- package/dist/__tests__/triage-v2-schema.test.js +115 -0
- package/dist/__tests__/triage-v2-schema.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/feedback-pipeline.d.ts +2 -0
- package/dist/pipeline/feedback-pipeline.d.ts.map +1 -1
- package/dist/pipeline/feedback-pipeline.js +4 -1
- package/dist/pipeline/feedback-pipeline.js.map +1 -1
- package/dist/pipeline/runner.d.ts +11 -0
- package/dist/pipeline/runner.d.ts.map +1 -1
- package/dist/pipeline/runner.js +314 -114
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/pm/actions/triage-prompt.d.ts +42 -0
- package/dist/pm/actions/triage-prompt.d.ts.map +1 -0
- package/dist/pm/actions/triage-prompt.js +192 -0
- package/dist/pm/actions/triage-prompt.js.map +1 -0
- package/dist/pm/actions/triage-render.d.ts +39 -0
- package/dist/pm/actions/triage-render.d.ts.map +1 -0
- package/dist/pm/actions/triage-render.js +158 -0
- package/dist/pm/actions/triage-render.js.map +1 -0
- package/dist/pm/actions/triage.d.ts +2 -1
- package/dist/pm/actions/triage.d.ts.map +1 -1
- package/dist/pm/actions/triage.js +44 -58
- package/dist/pm/actions/triage.js.map +1 -1
- package/dist/pm/triage-prediction-quality.d.ts +26 -0
- package/dist/pm/triage-prediction-quality.d.ts.map +1 -0
- package/dist/pm/triage-prediction-quality.js +41 -0
- package/dist/pm/triage-prediction-quality.js.map +1 -0
- package/dist/pm/types.d.ts +60 -0
- package/dist/pm/types.d.ts.map +1 -1
- package/dist/pm/types.js +119 -0
- package/dist/pm/types.js.map +1 -1
- package/dist/repo/bitbucket.d.ts +136 -0
- package/dist/repo/bitbucket.d.ts.map +1 -0
- package/dist/repo/bitbucket.js +237 -0
- package/dist/repo/bitbucket.js.map +1 -0
- package/dist/repo/gitlab.d.ts +11 -0
- package/dist/repo/gitlab.d.ts.map +1 -1
- package/dist/repo/gitlab.js +37 -0
- package/dist/repo/gitlab.js.map +1 -1
- package/dist/repo/index.d.ts +3 -1
- package/dist/repo/index.d.ts.map +1 -1
- package/dist/repo/index.js +2 -1
- package/dist/repo/index.js.map +1 -1
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +32 -0
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/dist/webhook/bitbucket-handler.d.ts +65 -0
- package/dist/webhook/bitbucket-handler.d.ts.map +1 -0
- package/dist/webhook/bitbucket-handler.js +153 -0
- package/dist/webhook/bitbucket-handler.js.map +1 -0
- package/dist/webhook/gitlab-handler.d.ts +66 -0
- package/dist/webhook/gitlab-handler.d.ts.map +1 -0
- package/dist/webhook/gitlab-handler.js +159 -0
- package/dist/webhook/gitlab-handler.js.map +1 -0
- package/dist/webhook/index.d.ts +3 -0
- package/dist/webhook/index.d.ts.map +1 -1
- package/dist/webhook/index.js +3 -0
- package/dist/webhook/index.js.map +1 -1
- package/dist/webhook/shared-handlers.d.ts +110 -0
- package/dist/webhook/shared-handlers.d.ts.map +1 -0
- package/dist/webhook/shared-handlers.js +251 -0
- package/dist/webhook/shared-handlers.js.map +1 -0
- package/package.json +1 -1
package/dist/pipeline/runner.js
CHANGED
|
@@ -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.
|
|
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 (
|
|
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 } =
|
|
1631
|
-
const octokit = await
|
|
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
|
|
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 } =
|
|
1699
|
-
const fanoutOctokit = await
|
|
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
|
|
1843
|
+
// 6. Auto-merge (skip drafts, unresolved conflicts)
|
|
1786
1844
|
const maxLines = config.autoMergeMaxLines ?? 200;
|
|
1787
|
-
|
|
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
|
|
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
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
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
|
-
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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(
|
|
1933
|
-
.where(
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
:
|
|
1941
|
-
|
|
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
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
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)");
|