@brainervirus/workit-core 2.4.0 → 2.6.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/package.json +1 -1
- package/scripts/analyze-release-scope.ts +99 -8
- package/scripts/bundled-deps.json +133 -0
- package/scripts/publish-changed-packages.ts +28 -16
- package/scripts/rewrite-workspace-deps.ts +11 -5
- package/scripts/sync-release-manifests.ts +5 -1
- package/src/autonomy.ts +135 -0
- package/src/core/detect-hosts.ts +9 -3
- package/src/core/doctor.ts +61 -0
- package/src/core/external-action-effects.ts +4 -9
- package/src/core/host-install.ts +144 -1
- package/src/core/pr-create.ts +2 -71
- package/src/core/setup.ts +27 -15
- package/src/core/support-matrix.ts +4 -0
- package/src/core/uninstall.ts +107 -4
- package/src/forge/exec.ts +40 -0
- package/src/forge/github.ts +84 -1
- package/src/forge/gitlab.ts +102 -1
- package/src/forge/pr-body.ts +100 -0
- package/src/forge/pr-ops.ts +450 -0
- package/src/forge/report.ts +1 -1
- package/src/forge/types.ts +30 -0
- package/src/forge/verify.ts +398 -0
- package/src/git/ops.ts +626 -0
- package/src/git/rev.ts +24 -7
- package/src/hooks/handle.ts +15 -4
- package/src/ledger.ts +17 -1
package/src/forge/github.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GitHub adapter: `gh api graphql` for PR state, checks and review threads,
|
|
2
2
|
// `gh api` REST for required checks, job logs and reruns (design §2.0).
|
|
3
|
-
import { apiJson, apiText, cliFailure, FORGE_TIMEOUTS, type ForgeRunner } from "./exec";
|
|
3
|
+
import { apiJson, apiText, apiWrite, cliFailure, FORGE_TIMEOUTS, type ForgeRunner } from "./exec";
|
|
4
4
|
import { logTail, redactText, shortBody } from "./redact";
|
|
5
5
|
import {
|
|
6
6
|
failure,
|
|
@@ -442,5 +442,88 @@ export function createGitHubForge(options: {
|
|
|
442
442
|
);
|
|
443
443
|
return run.ok ? success(undefined) : run;
|
|
444
444
|
},
|
|
445
|
+
|
|
446
|
+
createPr(input) {
|
|
447
|
+
// Cross-repository PRs name the head as owner:branch.
|
|
448
|
+
const headOwner = input.headRepo.split("/")[0];
|
|
449
|
+
const head = input.headRepo === repo ? input.head : `${headOwner}:${input.head}`;
|
|
450
|
+
const created = apiWrite<{
|
|
451
|
+
number?: number;
|
|
452
|
+
html_url?: string;
|
|
453
|
+
state?: string;
|
|
454
|
+
merged?: boolean;
|
|
455
|
+
head?: { ref?: string; sha?: string };
|
|
456
|
+
}>(
|
|
457
|
+
runner,
|
|
458
|
+
"gh",
|
|
459
|
+
apiHost,
|
|
460
|
+
[
|
|
461
|
+
"api",
|
|
462
|
+
"-X",
|
|
463
|
+
"POST",
|
|
464
|
+
`repos/${repo}/pulls`,
|
|
465
|
+
"-f",
|
|
466
|
+
`title=${input.title}`,
|
|
467
|
+
"-f",
|
|
468
|
+
`head=${head}`,
|
|
469
|
+
"-f",
|
|
470
|
+
`base=${input.base}`,
|
|
471
|
+
"-f",
|
|
472
|
+
`body=${input.body}`,
|
|
473
|
+
"-F",
|
|
474
|
+
`draft=${input.draft ? "true" : "false"}`,
|
|
475
|
+
],
|
|
476
|
+
`pull request for ${input.head}`,
|
|
477
|
+
);
|
|
478
|
+
if (!created.ok) return created;
|
|
479
|
+
const value = created.data;
|
|
480
|
+
if (typeof value.number !== "number" || typeof value.html_url !== "string")
|
|
481
|
+
return failure("failed", "gh api pulls returned no pull request number");
|
|
482
|
+
return success({
|
|
483
|
+
number: value.number,
|
|
484
|
+
url: value.html_url,
|
|
485
|
+
state: value.merged ? "merged" : value.state === "closed" ? "closed" : "open",
|
|
486
|
+
headBranch: value.head?.ref ?? input.head,
|
|
487
|
+
headSha: value.head?.sha ?? null,
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
merge(number, mergeOptions) {
|
|
492
|
+
// sha= is GitHub's atomic head guard: 409 when the head moved.
|
|
493
|
+
const merged = apiWrite<{ sha?: string; merged?: boolean; message?: string }>(
|
|
494
|
+
runner,
|
|
495
|
+
"gh",
|
|
496
|
+
apiHost,
|
|
497
|
+
[
|
|
498
|
+
"api",
|
|
499
|
+
"-X",
|
|
500
|
+
"PUT",
|
|
501
|
+
`repos/${repo}/pulls/${number}/merge`,
|
|
502
|
+
"-f",
|
|
503
|
+
`sha=${mergeOptions.sha}`,
|
|
504
|
+
"-f",
|
|
505
|
+
`merge_method=${mergeOptions.method}`,
|
|
506
|
+
],
|
|
507
|
+
`merge of pull request #${number}`,
|
|
508
|
+
);
|
|
509
|
+
if (!merged.ok) return merged;
|
|
510
|
+
if (merged.data.merged === false)
|
|
511
|
+
return failure(
|
|
512
|
+
"blocked",
|
|
513
|
+
`merge of pull request #${number} refused: ${redactText(merged.data.message ?? "not merged").slice(0, 200)}`,
|
|
514
|
+
);
|
|
515
|
+
return success({ mergeSha: typeof merged.data.sha === "string" ? merged.data.sha : null });
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
updateBase(number, base) {
|
|
519
|
+
const updated = apiWrite<unknown>(
|
|
520
|
+
runner,
|
|
521
|
+
"gh",
|
|
522
|
+
apiHost,
|
|
523
|
+
["api", "-X", "PATCH", `repos/${repo}/pulls/${number}`, "-f", `base=${base}`],
|
|
524
|
+
`retarget of pull request #${number}`,
|
|
525
|
+
);
|
|
526
|
+
return updated.ok ? success(undefined) : updated;
|
|
527
|
+
},
|
|
445
528
|
};
|
|
446
529
|
}
|
package/src/forge/gitlab.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GitLab adapter: `glab api` REST only (design §2.0 GitLab endpoints): the MR
|
|
2
2
|
// (`detailed_merge_status`, `has_conflicts`, `head_pipeline`), pipeline jobs,
|
|
3
3
|
// job traces, discussions, and pipeline/job retry.
|
|
4
|
-
import { apiJson, apiText, FORGE_TIMEOUTS, type ForgeRunner } from "./exec";
|
|
4
|
+
import { apiJson, apiText, apiWrite, FORGE_TIMEOUTS, type ForgeRunner } from "./exec";
|
|
5
5
|
import { logTail, redactText, shortBody } from "./redact";
|
|
6
6
|
import {
|
|
7
7
|
failure,
|
|
@@ -451,5 +451,106 @@ export function createGitLabForge(options: {
|
|
|
451
451
|
);
|
|
452
452
|
return run.ok ? success(undefined) : run;
|
|
453
453
|
},
|
|
454
|
+
|
|
455
|
+
createPr(input) {
|
|
456
|
+
// An MR from a fork is created in the source project and targets ours.
|
|
457
|
+
const fromFork = input.headRepo !== repo;
|
|
458
|
+
let source = project;
|
|
459
|
+
const extra: string[] = [];
|
|
460
|
+
if (fromFork) {
|
|
461
|
+
const target = apiJson<{ id?: number }>(
|
|
462
|
+
runner,
|
|
463
|
+
"glab",
|
|
464
|
+
apiHost,
|
|
465
|
+
api(project),
|
|
466
|
+
`project ${repo}`,
|
|
467
|
+
);
|
|
468
|
+
if (!target.ok) return target;
|
|
469
|
+
if (input.headProjectId === null || typeof target.data.id !== "number")
|
|
470
|
+
return failure(
|
|
471
|
+
"failed",
|
|
472
|
+
`cannot resolve the project ids for an MR from ${input.headRepo}`,
|
|
473
|
+
);
|
|
474
|
+
source = `projects/${input.headProjectId}`;
|
|
475
|
+
extra.push("-F", `target_project_id=${target.data.id}`);
|
|
476
|
+
}
|
|
477
|
+
const title =
|
|
478
|
+
input.draft && !/^draft:/iu.test(input.title) ? `Draft: ${input.title}` : input.title;
|
|
479
|
+
const created = apiWrite<GlMr>(
|
|
480
|
+
runner,
|
|
481
|
+
"glab",
|
|
482
|
+
apiHost,
|
|
483
|
+
[
|
|
484
|
+
"api",
|
|
485
|
+
"-X",
|
|
486
|
+
"POST",
|
|
487
|
+
`${source}/merge_requests`,
|
|
488
|
+
"-f",
|
|
489
|
+
`source_branch=${input.head}`,
|
|
490
|
+
"-f",
|
|
491
|
+
`target_branch=${input.base}`,
|
|
492
|
+
"-f",
|
|
493
|
+
`title=${title}`,
|
|
494
|
+
"-f",
|
|
495
|
+
`description=${input.body}`,
|
|
496
|
+
...extra,
|
|
497
|
+
],
|
|
498
|
+
`merge request for ${input.head}`,
|
|
499
|
+
);
|
|
500
|
+
if (!created.ok) return created;
|
|
501
|
+
const value = created.data;
|
|
502
|
+
if (typeof value.iid !== "number" || typeof value.web_url !== "string")
|
|
503
|
+
return failure("failed", "glab api merge_requests returned no merge request iid");
|
|
504
|
+
return success({
|
|
505
|
+
number: value.iid,
|
|
506
|
+
url: value.web_url,
|
|
507
|
+
state: mrState(value.state),
|
|
508
|
+
headBranch: value.source_branch ?? input.head,
|
|
509
|
+
headSha: value.sha ?? null,
|
|
510
|
+
});
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
merge(iid, mergeOptions) {
|
|
514
|
+
// GitLab's merge method is a project setting; squash is per request.
|
|
515
|
+
if (mergeOptions.method === "rebase")
|
|
516
|
+
return failure(
|
|
517
|
+
"invalid_input",
|
|
518
|
+
"GitLab merges with the project's merge method; pass --method squash or merge",
|
|
519
|
+
);
|
|
520
|
+
const merged = apiWrite<
|
|
521
|
+
GlMr & { merge_commit_sha?: string | null; squash_commit_sha?: string | null }
|
|
522
|
+
>(
|
|
523
|
+
runner,
|
|
524
|
+
"glab",
|
|
525
|
+
apiHost,
|
|
526
|
+
[
|
|
527
|
+
"api",
|
|
528
|
+
"-X",
|
|
529
|
+
"PUT",
|
|
530
|
+
`${project}/merge_requests/${iid}/merge`,
|
|
531
|
+
"-f",
|
|
532
|
+
`sha=${mergeOptions.sha}`,
|
|
533
|
+
"-F",
|
|
534
|
+
`squash=${mergeOptions.method === "squash" ? "true" : "false"}`,
|
|
535
|
+
],
|
|
536
|
+
`merge of merge request !${iid}`,
|
|
537
|
+
);
|
|
538
|
+
if (!merged.ok) return merged;
|
|
539
|
+
const value = merged.data;
|
|
540
|
+
return success({
|
|
541
|
+
mergeSha: value.merge_commit_sha ?? value.squash_commit_sha ?? null,
|
|
542
|
+
});
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
updateBase(iid, base) {
|
|
546
|
+
const updated = apiWrite<unknown>(
|
|
547
|
+
runner,
|
|
548
|
+
"glab",
|
|
549
|
+
apiHost,
|
|
550
|
+
["api", "-X", "PUT", `${project}/merge_requests/${iid}`, "-f", `target_branch=${base}`],
|
|
551
|
+
`retarget of merge request !${iid}`,
|
|
552
|
+
);
|
|
553
|
+
return updated.ok ? success(undefined) : updated;
|
|
554
|
+
},
|
|
454
555
|
};
|
|
455
556
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// PR/MR body issue linking (port of scripts/pr-create.sh --build-body), shared
|
|
2
|
+
// by the managed `hosting.pull_request` action (core/pr-create.ts re-exports
|
|
3
|
+
// these until S16) and `workit pr create` (S11), which passes typed input
|
|
4
|
+
// instead of environment variables.
|
|
5
|
+
import { vcsConfig } from "../core/vcs-config";
|
|
6
|
+
|
|
7
|
+
export function parseGhRepo(remote: string): string | null {
|
|
8
|
+
remote = (remote || "").trim().replace(/\/+$/, "");
|
|
9
|
+
if (!remote) return null;
|
|
10
|
+
if (remote.endsWith(".git")) remote = remote.slice(0, -4);
|
|
11
|
+
if (remote.includes(":")) remote = remote.split(":").pop() ?? ""; // drop git@host part (scp-style URL)
|
|
12
|
+
const parts = remote.split("/").filter(Boolean);
|
|
13
|
+
return parts.length >= 2 ? parts.slice(-2).join("/") : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseGhIssue(value: string): string {
|
|
17
|
+
const m = /issues\/(\d+)/.exec(value);
|
|
18
|
+
return m ? m[1] : String(value).trim().replace(/^#/, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// RL-03/CA-25/AR-08: a branch-derived numeric issue id must be a bare number at
|
|
22
|
+
// a segment or dash boundary — and never part of a date segment. Year-first
|
|
23
|
+
// (feature/2024-01-15/x) and day-first (feature/15-01-2024/x) dates are both
|
|
24
|
+
// skipped — a complete date anywhere in a segment (release-2024-01-15,
|
|
25
|
+
// v2-2024-01-15-fix) — so no date digit ever closes an issue. Deliberate
|
|
26
|
+
// numeric issue branches (feature/42-title, feature/2024-fix) keep linking.
|
|
27
|
+
function deriveGhIssueFromBranch(branch: string): string {
|
|
28
|
+
for (const segment of branch.split("/")) {
|
|
29
|
+
if (/^\d{4}-\d/.test(segment)) continue; // year-first date-like segment (incl. year-month)
|
|
30
|
+
if (/\d{4}-\d{1,2}-\d{1,2}/.test(segment)) continue; // complete year-first date anywhere
|
|
31
|
+
if (/\d{1,2}-\d{1,2}-\d{4}/.test(segment)) continue; // complete day-first date anywhere
|
|
32
|
+
const m = /(?:^|-)(\d+)(?:-|$)/.exec(segment);
|
|
33
|
+
if (m) return m[1];
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildBody(
|
|
39
|
+
body: string,
|
|
40
|
+
branch: string,
|
|
41
|
+
linkIssues: boolean,
|
|
42
|
+
baseUrl: string,
|
|
43
|
+
ytIssue: string,
|
|
44
|
+
ghLinkOnPr: boolean,
|
|
45
|
+
ghIssue: string,
|
|
46
|
+
ghRelation: string,
|
|
47
|
+
ghRepo: string | null,
|
|
48
|
+
): string {
|
|
49
|
+
let line: string | null = null;
|
|
50
|
+
if (linkIssues) {
|
|
51
|
+
let issue = ytIssue;
|
|
52
|
+
if (!issue && branch) {
|
|
53
|
+
// anchored prefix + \b boundary, 3+ digits so version-like tokens (POSTGRES-16, HTTP-3) never link
|
|
54
|
+
const m = /(?:^|\/|-)([A-Z]{2,}-\d{3,})\b/.exec(branch);
|
|
55
|
+
if (m) issue = m[1];
|
|
56
|
+
}
|
|
57
|
+
if (issue && baseUrl) line = `Related to: ${baseUrl.replace(/\/+$/, "")}/issue/${issue}`;
|
|
58
|
+
} else if (ghLinkOnPr) {
|
|
59
|
+
let issue = parseGhIssue(ghIssue);
|
|
60
|
+
if (!issue && branch) {
|
|
61
|
+
// pure-number issue id (feature/42-title -> 42); digits must be followed by a dash or end-of-string
|
|
62
|
+
// so version tokens (release/1.2.3, backport/8.0.1, lodash-4.17.21, 2024.1) never link
|
|
63
|
+
issue = deriveGhIssueFromBranch(branch);
|
|
64
|
+
}
|
|
65
|
+
if (issue) {
|
|
66
|
+
if (ghRelation === "related") {
|
|
67
|
+
line = `Related to #${issue}`;
|
|
68
|
+
if (ghRepo) line += ` — https://github.com/${ghRepo}/issues/${issue}`;
|
|
69
|
+
} else {
|
|
70
|
+
line = `Closes #${issue}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (line === null) return body;
|
|
75
|
+
return body ? `${body}\n\n${line}` : line;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The body `workit pr create` sends: the agent's text plus the workspace's
|
|
80
|
+
* issue link (YouTrack `link_issues`, or GitHub `issues.link_on_pr` with the
|
|
81
|
+
* issue number derived from the branch). Reads config, never writes it.
|
|
82
|
+
*/
|
|
83
|
+
export function prBodyFor(
|
|
84
|
+
cwd: string,
|
|
85
|
+
input: { body: string; branch: string; repo: string | null },
|
|
86
|
+
): string {
|
|
87
|
+
const resolved = vcsConfig("resolve", cwd);
|
|
88
|
+
if (resolved.ok === false) return input.body;
|
|
89
|
+
return buildBody(
|
|
90
|
+
input.body,
|
|
91
|
+
input.branch,
|
|
92
|
+
resolved.link_issues === true,
|
|
93
|
+
typeof resolved.youtrack_base_url === "string" ? resolved.youtrack_base_url : "",
|
|
94
|
+
"",
|
|
95
|
+
resolved.link_on_pr === true,
|
|
96
|
+
"",
|
|
97
|
+
"closes",
|
|
98
|
+
input.repo,
|
|
99
|
+
);
|
|
100
|
+
}
|