automata-cli 0.6.0-develop.273 → 0.6.0-develop.282
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/index.js +453 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -778,6 +778,47 @@ function deleteLocalBranch(branch) {
|
|
|
778
778
|
throw new Error(`Failed to delete branch ${branch}: ${result.stderr.trim()}`);
|
|
779
779
|
}
|
|
780
780
|
}
|
|
781
|
+
function listLocalBranches() {
|
|
782
|
+
const { stdout, status } = run2("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads"]);
|
|
783
|
+
if (status !== 0) return [];
|
|
784
|
+
return stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
785
|
+
}
|
|
786
|
+
function listRemoteBranches() {
|
|
787
|
+
const { stdout, status } = run2("git", ["ls-remote", "--heads", "origin"]);
|
|
788
|
+
if (status !== 0) return null;
|
|
789
|
+
const names = [];
|
|
790
|
+
for (const line of stdout.split("\n")) {
|
|
791
|
+
const match = /^[0-9a-f]+\s+refs\/heads\/(.+)$/.exec(line.trim());
|
|
792
|
+
if (match) names.push(match[1]);
|
|
793
|
+
}
|
|
794
|
+
return names;
|
|
795
|
+
}
|
|
796
|
+
function createBranchAtHead(branch) {
|
|
797
|
+
return gitCommand(["checkout", "-b", branch]);
|
|
798
|
+
}
|
|
799
|
+
function stageAllExcept(excludePaths) {
|
|
800
|
+
const args = ["add", "-A"];
|
|
801
|
+
if (excludePaths.length > 0) {
|
|
802
|
+
args.push("--", ".", ...excludePaths.map((path) => `:(exclude)${path}`));
|
|
803
|
+
}
|
|
804
|
+
return gitCommand(args);
|
|
805
|
+
}
|
|
806
|
+
function commitStaged(message) {
|
|
807
|
+
return gitCommand(["commit", "-m", message]);
|
|
808
|
+
}
|
|
809
|
+
function pushSetUpstream(branch) {
|
|
810
|
+
return gitCommand(["push", "-u", "origin", branch]);
|
|
811
|
+
}
|
|
812
|
+
function countCommitsNotIn(baseBranch, branch) {
|
|
813
|
+
const { stdout, status } = run2("git", ["rev-list", "--count", `${baseBranch}..${branch}`]);
|
|
814
|
+
if (status !== 0) return null;
|
|
815
|
+
const trimmed2 = stdout.trim();
|
|
816
|
+
if (!/^\d+$/.test(trimmed2)) return null;
|
|
817
|
+
return Number(trimmed2);
|
|
818
|
+
}
|
|
819
|
+
function forceDeleteLocalBranch(branch) {
|
|
820
|
+
return gitCommand(["branch", "-D", branch]);
|
|
821
|
+
}
|
|
781
822
|
var REVIEW_THREADS_QUERY = `
|
|
782
823
|
query($owner:String!,$repo:String!,$prNumber:Int!){
|
|
783
824
|
repository(owner:$owner,name:$repo){
|
|
@@ -2765,6 +2806,68 @@ function deleteMarker(marker) {
|
|
|
2765
2806
|
throw new Error(stderr.trim() || `Failed to delete comment ${marker.commentId}.`);
|
|
2766
2807
|
}
|
|
2767
2808
|
}
|
|
2809
|
+
function toHeadState(state) {
|
|
2810
|
+
return state === "OPEN" || state === "MERGED" ? state : "CLOSED";
|
|
2811
|
+
}
|
|
2812
|
+
function listPullRequestsForHead(branch) {
|
|
2813
|
+
const raw = ghJson(
|
|
2814
|
+
["pr", "list", "--head", branch, "--state", "all", "--json", "number,state,url,updatedAt"],
|
|
2815
|
+
`list pull requests for branch ${branch}`
|
|
2816
|
+
);
|
|
2817
|
+
return raw.map((pr) => ({
|
|
2818
|
+
number: pr.number,
|
|
2819
|
+
url: pr.url,
|
|
2820
|
+
state: toHeadState(pr.state),
|
|
2821
|
+
updatedAt: pr.updatedAt
|
|
2822
|
+
})).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2823
|
+
}
|
|
2824
|
+
var MISSING_LABEL_PATTERNS = [
|
|
2825
|
+
// gh's own message: `could not add label: 'rescue' not found`.
|
|
2826
|
+
/could not add label/i,
|
|
2827
|
+
// The GraphQL error it wraps, seen directly on some gh versions.
|
|
2828
|
+
/could not resolve to a label/i,
|
|
2829
|
+
/\blabels?\b[^\n]*\b(?:not found|does not exist)\b/i
|
|
2830
|
+
];
|
|
2831
|
+
function isMissingLabelError(stderr) {
|
|
2832
|
+
return MISSING_LABEL_PATTERNS.some((pattern) => pattern.test(stderr));
|
|
2833
|
+
}
|
|
2834
|
+
function createDraftPullRequest(input) {
|
|
2835
|
+
const base = [
|
|
2836
|
+
"pr",
|
|
2837
|
+
"create",
|
|
2838
|
+
"--draft",
|
|
2839
|
+
"--head",
|
|
2840
|
+
input.head,
|
|
2841
|
+
"--base",
|
|
2842
|
+
input.base,
|
|
2843
|
+
"--title",
|
|
2844
|
+
input.title,
|
|
2845
|
+
"--body",
|
|
2846
|
+
input.body
|
|
2847
|
+
];
|
|
2848
|
+
if (input.label !== void 0 && input.label.length > 0) {
|
|
2849
|
+
const labelled = run4("gh", [...base, "--label", input.label]);
|
|
2850
|
+
if (labelled.status === 0) return parseCreatedPrUrl(labelled.stdout, input.head);
|
|
2851
|
+
if (!isMissingLabelError(labelled.stderr)) {
|
|
2852
|
+
throw new Error(
|
|
2853
|
+
labelled.stderr.trim() || `Failed to open a draft pull request for ${input.head}.`
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
const { stdout, stderr, status } = run4("gh", base);
|
|
2858
|
+
if (status !== 0) {
|
|
2859
|
+
throw new Error(stderr.trim() || `Failed to open a draft pull request for ${input.head}.`);
|
|
2860
|
+
}
|
|
2861
|
+
return parseCreatedPrUrl(stdout, input.head);
|
|
2862
|
+
}
|
|
2863
|
+
function parseCreatedPrUrl(stdout, head) {
|
|
2864
|
+
const url = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
2865
|
+
const match = /\/pull\/(\d+)\s*$/.exec(url);
|
|
2866
|
+
if (!match) {
|
|
2867
|
+
throw new Error(`Opened a pull request for ${head} but could not read its number from: ${url}`);
|
|
2868
|
+
}
|
|
2869
|
+
return { number: Number(match[1]), url };
|
|
2870
|
+
}
|
|
2768
2871
|
|
|
2769
2872
|
// src/github/workDetection.ts
|
|
2770
2873
|
function isAssignedToAgent(assignees, agentUser) {
|
|
@@ -3402,6 +3505,283 @@ function preparePrBranch(headRefName) {
|
|
|
3402
3505
|
return { ok: true, branch: headRefName };
|
|
3403
3506
|
}
|
|
3404
3507
|
|
|
3508
|
+
// src/git/repoHygiene.ts
|
|
3509
|
+
var RESCUE_PR_LABEL = "rescue";
|
|
3510
|
+
var RESCUE_BRANCH_PREFIX = "rescue/";
|
|
3511
|
+
function utcStamp(now) {
|
|
3512
|
+
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
3513
|
+
}
|
|
3514
|
+
function flattenBranchName(branch) {
|
|
3515
|
+
return branch.replaceAll("/", "-");
|
|
3516
|
+
}
|
|
3517
|
+
function detectRescueTarget(baseBranch, now) {
|
|
3518
|
+
const current = getCurrentBranch();
|
|
3519
|
+
const detached = current === "HEAD" || current.length === 0;
|
|
3520
|
+
if (!detached && current !== baseBranch) {
|
|
3521
|
+
return { branch: current, createdBranch: false, source: current };
|
|
3522
|
+
}
|
|
3523
|
+
const source = detached ? "detached HEAD" : baseBranch;
|
|
3524
|
+
const label = detached ? "detached" : flattenBranchName(baseBranch);
|
|
3525
|
+
return {
|
|
3526
|
+
branch: `${RESCUE_BRANCH_PREFIX}${label}-${utcStamp(now)}`,
|
|
3527
|
+
createdBranch: true,
|
|
3528
|
+
source
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
function rescueBody(source) {
|
|
3532
|
+
return `Opened by \`automata do-work\`'s repository-hygiene pre-flight because this branch carried work that existed only in the local checkout.
|
|
3533
|
+
|
|
3534
|
+
Source: \`${source}\`.
|
|
3535
|
+
|
|
3536
|
+
It is a draft and references no issue: nothing here is claimed to be finished, it is here so it cannot be lost. Review it, fold it into the right pull request, or close it.`;
|
|
3537
|
+
}
|
|
3538
|
+
function findOpenPr(branch) {
|
|
3539
|
+
const prs = listPullRequestsForHead(branch);
|
|
3540
|
+
return prs.find((pr) => pr.state === "OPEN") ?? null;
|
|
3541
|
+
}
|
|
3542
|
+
function rescueUncommittedChanges(options, now) {
|
|
3543
|
+
if (!hasUncommittedChanges([RUN_LOCK_RELATIVE_PATH])) {
|
|
3544
|
+
options.log(" rescue nothing to do; the working tree is clean\n");
|
|
3545
|
+
return { kind: "clean" };
|
|
3546
|
+
}
|
|
3547
|
+
const target = detectRescueTarget(options.baseBranch, now);
|
|
3548
|
+
if (options.dryRun) {
|
|
3549
|
+
options.log(
|
|
3550
|
+
target.createdBranch ? ` rescue would create ${target.branch} off ${target.source}, commit the uncommitted changes, push it and open a draft PR
|
|
3551
|
+
` : ` rescue would commit the uncommitted changes onto ${target.branch}, push it and open a draft PR if it has none open
|
|
3552
|
+
`
|
|
3553
|
+
);
|
|
3554
|
+
return { kind: "would-rescue", branch: target.branch, createdBranch: target.createdBranch };
|
|
3555
|
+
}
|
|
3556
|
+
if (target.createdBranch) {
|
|
3557
|
+
const created = createBranchAtHead(target.branch);
|
|
3558
|
+
if (!created.ok) {
|
|
3559
|
+
options.log(` rescue FAILED to create ${target.branch}: ${created.stderr}
|
|
3560
|
+
`);
|
|
3561
|
+
return { kind: "failed", step: "branch", detail: created.stderr };
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
const staged = stageAllExcept([RUN_LOCK_RELATIVE_PATH]);
|
|
3565
|
+
if (!staged.ok) {
|
|
3566
|
+
options.log(` rescue FAILED to stage the changes: ${staged.stderr}
|
|
3567
|
+
`);
|
|
3568
|
+
return { kind: "failed", step: "stage", detail: staged.stderr };
|
|
3569
|
+
}
|
|
3570
|
+
const committed = commitStaged(`chore(automata): rescue uncommitted work from ${target.source}`);
|
|
3571
|
+
if (!committed.ok) {
|
|
3572
|
+
options.log(` rescue FAILED to commit: ${committed.stderr}
|
|
3573
|
+
`);
|
|
3574
|
+
return { kind: "failed", step: "commit", detail: committed.stderr };
|
|
3575
|
+
}
|
|
3576
|
+
const pushed = pushSetUpstream(target.branch);
|
|
3577
|
+
if (!pushed.ok) {
|
|
3578
|
+
options.log(` rescue FAILED to push ${target.branch}: ${pushed.stderr}
|
|
3579
|
+
`);
|
|
3580
|
+
return { kind: "failed", step: "push", detail: pushed.stderr };
|
|
3581
|
+
}
|
|
3582
|
+
let existing;
|
|
3583
|
+
try {
|
|
3584
|
+
existing = findOpenPr(target.branch);
|
|
3585
|
+
} catch (err) {
|
|
3586
|
+
options.log(
|
|
3587
|
+
` rescue committed and pushed ${target.branch}, but could not check for an open PR: ${err.message}
|
|
3588
|
+
`
|
|
3589
|
+
);
|
|
3590
|
+
return { kind: "failed", step: "pr", detail: err.message };
|
|
3591
|
+
}
|
|
3592
|
+
if (existing !== null) {
|
|
3593
|
+
options.log(
|
|
3594
|
+
` rescue committed and pushed ${target.branch}; PR #${String(existing.number)} is already open
|
|
3595
|
+
`
|
|
3596
|
+
);
|
|
3597
|
+
return {
|
|
3598
|
+
kind: "rescued",
|
|
3599
|
+
branch: target.branch,
|
|
3600
|
+
createdBranch: target.createdBranch,
|
|
3601
|
+
pr: existing.number,
|
|
3602
|
+
prUrl: existing.url,
|
|
3603
|
+
prCreated: false
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
try {
|
|
3607
|
+
const pr = createDraftPullRequest({
|
|
3608
|
+
head: target.branch,
|
|
3609
|
+
base: options.baseBranch,
|
|
3610
|
+
title: `rescue: uncommitted work from ${target.source}`,
|
|
3611
|
+
body: rescueBody(target.source),
|
|
3612
|
+
label: RESCUE_PR_LABEL
|
|
3613
|
+
});
|
|
3614
|
+
options.log(
|
|
3615
|
+
` rescue committed and pushed ${target.branch}; opened draft PR #${String(pr.number)}
|
|
3616
|
+
`
|
|
3617
|
+
);
|
|
3618
|
+
return {
|
|
3619
|
+
kind: "rescued",
|
|
3620
|
+
branch: target.branch,
|
|
3621
|
+
createdBranch: target.createdBranch,
|
|
3622
|
+
pr: pr.number,
|
|
3623
|
+
prUrl: pr.url,
|
|
3624
|
+
prCreated: true
|
|
3625
|
+
};
|
|
3626
|
+
} catch (err) {
|
|
3627
|
+
options.log(
|
|
3628
|
+
` rescue committed and pushed ${target.branch}, but could not open a draft PR: ${err.message}
|
|
3629
|
+
`
|
|
3630
|
+
);
|
|
3631
|
+
return { kind: "failed", step: "pr", detail: err.message };
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
function prepareBase(options) {
|
|
3635
|
+
if (options.dryRun) {
|
|
3636
|
+
options.log(` base would check out ${options.baseBranch} and pull --ff-only
|
|
3637
|
+
`);
|
|
3638
|
+
return { ok: true };
|
|
3639
|
+
}
|
|
3640
|
+
const checkout = checkoutBranch(options.baseBranch);
|
|
3641
|
+
if (!checkout.ok) {
|
|
3642
|
+
options.log(` base FAILED to check out ${options.baseBranch}: ${checkout.stderr}
|
|
3643
|
+
`);
|
|
3644
|
+
return { ok: false, step: "checkout", detail: checkout.stderr };
|
|
3645
|
+
}
|
|
3646
|
+
const pull = pullFastForwardOnly();
|
|
3647
|
+
if (!pull.ok) {
|
|
3648
|
+
options.log(` base FAILED to pull ${options.baseBranch}: ${pull.stderr}
|
|
3649
|
+
`);
|
|
3650
|
+
return { ok: false, step: "pull", detail: pull.stderr };
|
|
3651
|
+
}
|
|
3652
|
+
options.log(` base ${options.baseBranch} checked out and up to date
|
|
3653
|
+
`);
|
|
3654
|
+
return { ok: true };
|
|
3655
|
+
}
|
|
3656
|
+
function collectCandidates(options) {
|
|
3657
|
+
const remote = listRemoteBranches();
|
|
3658
|
+
if (remote === null) return null;
|
|
3659
|
+
const remoteNames = new Set(remote);
|
|
3660
|
+
const current = getCurrentBranch();
|
|
3661
|
+
const untouchable = /* @__PURE__ */ new Set([options.baseBranch, current, ...options.protectedBranches]);
|
|
3662
|
+
return listLocalBranches().filter(
|
|
3663
|
+
(branch) => !untouchable.has(branch) && !remoteNames.has(branch)
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
function deleteOrReport(branch, options, why) {
|
|
3667
|
+
if (options.dryRun) {
|
|
3668
|
+
options.log(` prune would delete ${branch} (no remote, ${why})
|
|
3669
|
+
`);
|
|
3670
|
+
return { kind: "would-delete", branch };
|
|
3671
|
+
}
|
|
3672
|
+
const deleted = forceDeleteLocalBranch(branch);
|
|
3673
|
+
if (!deleted.ok) {
|
|
3674
|
+
options.log(` prune kept ${branch}: delete failed \u2014 ${deleted.stderr}
|
|
3675
|
+
`);
|
|
3676
|
+
return { kind: "kept", branch, reason: "delete-failed", detail: deleted.stderr };
|
|
3677
|
+
}
|
|
3678
|
+
options.log(` prune deleted ${branch} (${why})
|
|
3679
|
+
`);
|
|
3680
|
+
return { kind: "deleted", branch };
|
|
3681
|
+
}
|
|
3682
|
+
function pruneCandidate(branch, options) {
|
|
3683
|
+
let prs;
|
|
3684
|
+
try {
|
|
3685
|
+
prs = listPullRequestsForHead(branch);
|
|
3686
|
+
} catch (err) {
|
|
3687
|
+
const detail = err.message;
|
|
3688
|
+
options.log(` prune kept ${branch}: could not read its pull requests \u2014 ${detail}
|
|
3689
|
+
`);
|
|
3690
|
+
return { kind: "kept", branch, reason: "lookup-failed", detail };
|
|
3691
|
+
}
|
|
3692
|
+
const openPr = prs.find((pr) => pr.state === "OPEN") ?? null;
|
|
3693
|
+
if (openPr !== null) {
|
|
3694
|
+
options.log(` prune kept ${branch}: PR #${String(openPr.number)} is open
|
|
3695
|
+
`);
|
|
3696
|
+
return {
|
|
3697
|
+
kind: "kept",
|
|
3698
|
+
branch,
|
|
3699
|
+
reason: "open-pr",
|
|
3700
|
+
detail: `PR #${String(openPr.number)} is open`
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
const merged = prs.find((pr) => pr.state === "MERGED") ?? null;
|
|
3704
|
+
if (merged !== null) {
|
|
3705
|
+
return deleteOrReport(branch, options, `PR #${String(merged.number)} was merged`);
|
|
3706
|
+
}
|
|
3707
|
+
const closed = prs.find((pr) => pr.state === "CLOSED") ?? null;
|
|
3708
|
+
if (closed !== null) {
|
|
3709
|
+
return deleteOrReport(branch, options, `PR #${String(closed.number)} was closed unmerged`);
|
|
3710
|
+
}
|
|
3711
|
+
const unmerged = countCommitsNotIn(options.baseBranch, branch);
|
|
3712
|
+
if (unmerged === null) {
|
|
3713
|
+
const detail = `could not count commits outside ${options.baseBranch}`;
|
|
3714
|
+
options.log(` prune kept ${branch}: ${detail}
|
|
3715
|
+
`);
|
|
3716
|
+
return { kind: "kept", branch, reason: "lookup-failed", detail };
|
|
3717
|
+
}
|
|
3718
|
+
if (unmerged === 0) {
|
|
3719
|
+
return deleteOrReport(branch, options, `nothing outside ${options.baseBranch}`);
|
|
3720
|
+
}
|
|
3721
|
+
if (options.dryRun) {
|
|
3722
|
+
options.log(
|
|
3723
|
+
` prune would push ${branch} and open a draft PR (${String(unmerged)} commit(s) outside ${options.baseBranch}), keeping the branch
|
|
3724
|
+
`
|
|
3725
|
+
);
|
|
3726
|
+
return { kind: "would-rescue", branch, unmergedCommits: unmerged };
|
|
3727
|
+
}
|
|
3728
|
+
const pushed = pushSetUpstream(branch);
|
|
3729
|
+
if (!pushed.ok) {
|
|
3730
|
+
options.log(
|
|
3731
|
+
` prune kept ${branch}: has ${String(unmerged)} commit(s) outside ${options.baseBranch} and could not be pushed \u2014 ${pushed.stderr}
|
|
3732
|
+
`
|
|
3733
|
+
);
|
|
3734
|
+
return { kind: "kept", branch, reason: "push-failed", detail: pushed.stderr };
|
|
3735
|
+
}
|
|
3736
|
+
try {
|
|
3737
|
+
const pr = createDraftPullRequest({
|
|
3738
|
+
head: branch,
|
|
3739
|
+
base: options.baseBranch,
|
|
3740
|
+
title: `rescue: unmerged commits on ${branch}`,
|
|
3741
|
+
body: rescueBody(branch),
|
|
3742
|
+
label: RESCUE_PR_LABEL
|
|
3743
|
+
});
|
|
3744
|
+
options.log(
|
|
3745
|
+
` prune rescued ${branch}: pushed and opened draft PR #${String(pr.number)}
|
|
3746
|
+
`
|
|
3747
|
+
);
|
|
3748
|
+
return { kind: "rescued", branch, pr: pr.number, prUrl: pr.url };
|
|
3749
|
+
} catch (err) {
|
|
3750
|
+
options.log(
|
|
3751
|
+
` prune rescued ${branch}: pushed, but could not open a draft PR \u2014 ${err.message}
|
|
3752
|
+
`
|
|
3753
|
+
);
|
|
3754
|
+
return { kind: "rescued", branch, pr: null, prUrl: null };
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
function prune(options) {
|
|
3758
|
+
const candidates = collectCandidates(options);
|
|
3759
|
+
if (candidates === null) {
|
|
3760
|
+
options.log(
|
|
3761
|
+
" prune skipped: could not list origin's branches, so no branch can be shown to have no remote\n"
|
|
3762
|
+
);
|
|
3763
|
+
return { outcomes: [], remoteUnreadable: true };
|
|
3764
|
+
}
|
|
3765
|
+
if (candidates.length === 0) {
|
|
3766
|
+
options.log(" prune nothing to do; every local branch exists on origin\n");
|
|
3767
|
+
return { outcomes: [], remoteUnreadable: false };
|
|
3768
|
+
}
|
|
3769
|
+
return {
|
|
3770
|
+
outcomes: candidates.map((branch) => pruneCandidate(branch, options)),
|
|
3771
|
+
remoteUnreadable: false
|
|
3772
|
+
};
|
|
3773
|
+
}
|
|
3774
|
+
function runRepoHygiene(options, now = /* @__PURE__ */ new Date()) {
|
|
3775
|
+
options.log(options.dryRun ? "\nPre-flight (dry run):\n" : "\nPre-flight:\n");
|
|
3776
|
+
const rescue = rescueUncommittedChanges(options, now);
|
|
3777
|
+
const base = prepareBase(options);
|
|
3778
|
+
const { outcomes, remoteUnreadable } = prune(options);
|
|
3779
|
+
const degraded = rescue.kind === "failed" || !base.ok || remoteUnreadable || outcomes.some(
|
|
3780
|
+
(outcome) => outcome.kind === "kept" && outcome.reason !== "open-pr" || outcome.kind === "rescued" && outcome.pr === null
|
|
3781
|
+
);
|
|
3782
|
+
return { rescue, base, prunes: outcomes, degraded };
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3405
3785
|
// src/commands/doWork.ts
|
|
3406
3786
|
var inFlightMarker = null;
|
|
3407
3787
|
function out(message) {
|
|
@@ -4148,6 +4528,12 @@ function explainInterruptedMarker() {
|
|
|
4148
4528
|
}
|
|
4149
4529
|
}
|
|
4150
4530
|
async function runTick(settings, options) {
|
|
4531
|
+
const hygiene = runRepoHygiene({
|
|
4532
|
+
baseBranch: settings.baseBranch,
|
|
4533
|
+
protectedBranches: settings.protectedBranches,
|
|
4534
|
+
dryRun: options.dryRun === true,
|
|
4535
|
+
log: progress
|
|
4536
|
+
});
|
|
4151
4537
|
const issues = discoverIssues(settings);
|
|
4152
4538
|
const linkMap = getOpenPrLinkMap();
|
|
4153
4539
|
const decisions = issues.map(
|
|
@@ -4166,7 +4552,7 @@ ${describePlan(decisions)}`;
|
|
|
4166
4552
|
out(planText);
|
|
4167
4553
|
}
|
|
4168
4554
|
if (options.dryRun) {
|
|
4169
|
-
reportDryRun(items, decisions, settings, options);
|
|
4555
|
+
reportDryRun(items, decisions, settings, options, hygiene);
|
|
4170
4556
|
return 0;
|
|
4171
4557
|
}
|
|
4172
4558
|
const reports = [];
|
|
@@ -4206,17 +4592,24 @@ ${describePlan(decisions)}`;
|
|
|
4206
4592
|
detail: `run cap of ${String(settings.maxRuns)} reached`
|
|
4207
4593
|
});
|
|
4208
4594
|
}
|
|
4209
|
-
const degraded = reports.some((report) => report.outcome !== "answered");
|
|
4595
|
+
const degraded = hygiene.degraded || reports.some((report) => report.outcome !== "answered");
|
|
4210
4596
|
const exitCode = degraded ? 2 : 0;
|
|
4211
4597
|
if (options.json) {
|
|
4212
4598
|
out(
|
|
4213
4599
|
JSON.stringify(
|
|
4214
|
-
{
|
|
4600
|
+
{
|
|
4601
|
+
dryRun: false,
|
|
4602
|
+
preflight: toHygieneJson(hygiene),
|
|
4603
|
+
plan: decisions.map(toPlanJson),
|
|
4604
|
+
items: reports.map(toItemJson),
|
|
4605
|
+
exitCode
|
|
4606
|
+
},
|
|
4215
4607
|
null,
|
|
4216
4608
|
2
|
|
4217
4609
|
) + "\n"
|
|
4218
4610
|
);
|
|
4219
4611
|
} else {
|
|
4612
|
+
summarizeHygiene(hygiene);
|
|
4220
4613
|
summarize(reports);
|
|
4221
4614
|
}
|
|
4222
4615
|
return exitCode;
|
|
@@ -4255,7 +4648,7 @@ function toRunJson(entry) {
|
|
|
4255
4648
|
prompt: entry.run.prompt
|
|
4256
4649
|
};
|
|
4257
4650
|
}
|
|
4258
|
-
function reportDryRun(items, decisions, settings, options) {
|
|
4651
|
+
function reportDryRun(items, decisions, settings, options, hygiene) {
|
|
4259
4652
|
const describable = settings.maxRuns > 0 ? items.slice(0, settings.maxRuns) : items;
|
|
4260
4653
|
const planned = describable.map((item) => {
|
|
4261
4654
|
const resolved = resolveItemExecution(item, settings);
|
|
@@ -4270,6 +4663,7 @@ function reportDryRun(items, decisions, settings, options) {
|
|
|
4270
4663
|
JSON.stringify(
|
|
4271
4664
|
{
|
|
4272
4665
|
dryRun: true,
|
|
4666
|
+
preflight: toHygieneJson(hygiene),
|
|
4273
4667
|
plan: decisions.map(toPlanJson),
|
|
4274
4668
|
runs: planned.map(toRunJson)
|
|
4275
4669
|
},
|
|
@@ -4279,6 +4673,7 @@ function reportDryRun(items, decisions, settings, options) {
|
|
|
4279
4673
|
);
|
|
4280
4674
|
return;
|
|
4281
4675
|
}
|
|
4676
|
+
summarizeHygiene(hygiene);
|
|
4282
4677
|
for (const entry of planned) {
|
|
4283
4678
|
out(
|
|
4284
4679
|
"\n" + (entry.kind === "refused" ? describeRefusedRun(entry.item, entry.refusal) : describePlannedRun(entry.item, settings, entry.run, entry.execution))
|
|
@@ -4290,7 +4685,60 @@ function reportDryRun(items, decisions, settings, options) {
|
|
|
4290
4685
|
(${String(deferred)} further item(s) deferred by the run cap.)
|
|
4291
4686
|
`);
|
|
4292
4687
|
}
|
|
4293
|
-
out(
|
|
4688
|
+
out(
|
|
4689
|
+
"\nDry run: nothing was rescued, pruned, pulled, assigned, posted, checked out or executed.\n"
|
|
4690
|
+
);
|
|
4691
|
+
}
|
|
4692
|
+
function summarizeHygiene(hygiene) {
|
|
4693
|
+
out("\nPre-flight:\n");
|
|
4694
|
+
out(` rescue ${describeRescue(hygiene.rescue)}
|
|
4695
|
+
`);
|
|
4696
|
+
out(
|
|
4697
|
+
hygiene.base.ok ? " base ready\n" : ` base ${hygiene.base.step} failed \u2014 ${hygiene.base.detail}
|
|
4698
|
+
`
|
|
4699
|
+
);
|
|
4700
|
+
if (hygiene.prunes.length === 0) {
|
|
4701
|
+
out(" prune no candidates\n");
|
|
4702
|
+
} else {
|
|
4703
|
+
for (const outcome of hygiene.prunes) {
|
|
4704
|
+
out(` prune ${describePrune(outcome)}
|
|
4705
|
+
`);
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
function describeRescue(rescue) {
|
|
4710
|
+
switch (rescue.kind) {
|
|
4711
|
+
case "clean":
|
|
4712
|
+
return "nothing to rescue";
|
|
4713
|
+
case "rescued":
|
|
4714
|
+
return rescue.prCreated ? `committed and pushed ${rescue.branch}, opened draft PR #${String(rescue.pr)}` : `committed and pushed ${rescue.branch}, PR #${String(rescue.pr)} already open`;
|
|
4715
|
+
case "would-rescue":
|
|
4716
|
+
return `would rescue onto ${rescue.branch}`;
|
|
4717
|
+
case "failed":
|
|
4718
|
+
return `${rescue.step} failed \u2014 ${rescue.detail}; the tree is still dirty and nothing was discarded`;
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
function describePrune(outcome) {
|
|
4722
|
+
switch (outcome.kind) {
|
|
4723
|
+
case "deleted":
|
|
4724
|
+
return `deleted ${outcome.branch}`;
|
|
4725
|
+
case "would-delete":
|
|
4726
|
+
return `would delete ${outcome.branch}`;
|
|
4727
|
+
case "kept":
|
|
4728
|
+
return `kept ${outcome.branch} (${outcome.reason}: ${outcome.detail})`;
|
|
4729
|
+
case "rescued":
|
|
4730
|
+
return outcome.pr === null ? `rescued ${outcome.branch} (pushed; no PR opened)` : `rescued ${outcome.branch} (pushed, draft PR #${String(outcome.pr)})`;
|
|
4731
|
+
case "would-rescue":
|
|
4732
|
+
return `would rescue ${outcome.branch} (${String(outcome.unmergedCommits)} unmerged commit(s))`;
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
function toHygieneJson(hygiene) {
|
|
4736
|
+
return {
|
|
4737
|
+
rescue: hygiene.rescue,
|
|
4738
|
+
base: hygiene.base,
|
|
4739
|
+
prunes: hygiene.prunes,
|
|
4740
|
+
degraded: hygiene.degraded
|
|
4741
|
+
};
|
|
4294
4742
|
}
|
|
4295
4743
|
function toPlanJson(decision) {
|
|
4296
4744
|
if (decision.kind === "skip") {
|