sequant 2.9.0 → 2.10.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +14 -3
- package/dist/bin/cli.js +97 -17
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +56 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.js +2 -0
- package/dist/src/commands/logs.js +14 -5
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +25 -3
- package/dist/src/commands/ready.js +39 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.js +37 -4
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -0
- package/dist/src/lib/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +33 -0
- package/dist/src/lib/cli-flags.js +46 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +62 -0
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/index.d.ts +3 -3
- package/dist/src/lib/locks/index.js +2 -2
- package/dist/src/lib/locks/lock-manager.d.ts +20 -2
- package/dist/src/lib/locks/lock-manager.js +47 -3
- package/dist/src/lib/locks/types.d.ts +34 -3
- package/dist/src/lib/locks/types.js +16 -0
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +14 -0
- package/dist/src/lib/settings.js +61 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +9 -2
- package/dist/src/lib/test-tautology-detector.js +153 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +477 -44
- package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
- package/dist/src/lib/workflow/chain-resume.js +24 -20
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
- package/dist/src/lib/workflow/config-resolver.js +54 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +20 -2
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +44 -1
- package/dist/src/lib/workflow/log-writer.js +75 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +457 -52
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
- package/dist/src/lib/workflow/ready-gate.js +25 -9
- package/dist/src/lib/workflow/run-log-schema.d.ts +14 -0
- package/dist/src/lib/workflow/run-log-schema.js +47 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +141 -20
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/types.d.ts +126 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +28 -2
- package/dist/src/lib/workflow/worktree-manager.js +102 -23
- package/dist/src/mcp/tools/run.d.ts +9 -1
- package/dist/src/mcp/tools/run.js +20 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +8 -7
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +75 -10
- package/templates/scripts/cleanup-worktree.sh +182 -19
- package/templates/scripts/new-feature.sh +264 -16
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +215 -390
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +26 -6
- package/templates/skills/fullsolve/SKILL.md +3 -1
- package/templates/skills/loop/SKILL.md +38 -2
- package/templates/skills/merger/SKILL.md +2 -2
- package/templates/skills/qa/SKILL.md +337 -57
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- package/templates/skills/release/SKILL.md +1 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +2 -0
- package/templates/skills/test/SKILL.md +1 -1
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- package/dist/src/lib/workflow/run-summary.js +0 -142
|
@@ -2,10 +2,37 @@
|
|
|
2
2
|
* Combined branch testing (AC-1)
|
|
3
3
|
*
|
|
4
4
|
* Creates a temporary branch merging all feature branches from a run batch,
|
|
5
|
-
*
|
|
5
|
+
* reinstalls dependencies when the merge moved a lockfile, then runs the
|
|
6
|
+
* project's test and build scripts on the combined state and reports results.
|
|
6
7
|
*/
|
|
7
8
|
import { spawnSync } from "child_process";
|
|
8
9
|
import { getBranchRef } from "./types.js";
|
|
10
|
+
import { detectPackageManagerSync, resolvePackageManagerConfig, } from "../stacks.js";
|
|
11
|
+
import { toCommandResult, resolveFailureReason, } from "./command-result.js";
|
|
12
|
+
// Re-exported for the existing test suite and any downstream consumer that
|
|
13
|
+
// imported these from here before they moved to ./command-result.js.
|
|
14
|
+
export { resolveFailureReason };
|
|
15
|
+
/**
|
|
16
|
+
* Lockfile names for the JS package managers we support. Kept in sync with the
|
|
17
|
+
* equivalent list in `workflow/worktree-manager.ts`.
|
|
18
|
+
*/
|
|
19
|
+
const LOCKFILES = [
|
|
20
|
+
"package-lock.json",
|
|
21
|
+
"pnpm-lock.yaml",
|
|
22
|
+
"bun.lockb",
|
|
23
|
+
"bun.lock",
|
|
24
|
+
"yarn.lock",
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Per-step command timeouts. A dependency install and a full test suite are
|
|
28
|
+
* both slower than the previous flat 2-minute budget allowed. An expired
|
|
29
|
+
* timeout kills the process with `status: null` and no captured output, which
|
|
30
|
+
* showed up as a BLOCKED verdict with no stated reason (#803).
|
|
31
|
+
*/
|
|
32
|
+
const INSTALL_TIMEOUT_MS = 300_000; // 5 min
|
|
33
|
+
const TEST_BUILD_TIMEOUT_MS = 600_000; // 10 min
|
|
34
|
+
/** Ref the combined state is built on top of, and compared against. */
|
|
35
|
+
const BASE_REF = "origin/main";
|
|
9
36
|
/**
|
|
10
37
|
* Run a git command and return the result
|
|
11
38
|
*/
|
|
@@ -22,23 +49,39 @@ function git(args, cwd) {
|
|
|
22
49
|
};
|
|
23
50
|
}
|
|
24
51
|
/**
|
|
25
|
-
* Run
|
|
52
|
+
* Run a package-manager command line (e.g. "npm ci", "pnpm run build").
|
|
53
|
+
*
|
|
54
|
+
* @param command Full command line, as stored in PM_CONFIG
|
|
55
|
+
* @param cwd Working directory
|
|
56
|
+
* @param timeoutMs Kill the command after this long
|
|
57
|
+
* @internal Exported for testing
|
|
26
58
|
*/
|
|
27
|
-
function
|
|
28
|
-
const
|
|
59
|
+
export function runPackageManagerCommand(command, cwd, timeoutMs) {
|
|
60
|
+
const [bin, ...args] = command.split(" ");
|
|
61
|
+
const result = spawnSync(bin, args, {
|
|
29
62
|
cwd,
|
|
30
63
|
stdio: "pipe",
|
|
31
64
|
encoding: "utf-8",
|
|
32
|
-
timeout:
|
|
65
|
+
timeout: timeoutMs,
|
|
66
|
+
// On Windows the package managers are `.cmd` shims, which CreateProcess
|
|
67
|
+
// will not resolve from a bare "npm"/"pnpm". `command` is always a constant
|
|
68
|
+
// from PM_CONFIG — never user input — so there is nothing to inject here.
|
|
69
|
+
shell: process.platform === "win32",
|
|
33
70
|
});
|
|
34
|
-
return
|
|
35
|
-
ok: result.status === 0,
|
|
36
|
-
stdout: result.stdout?.trim() ?? "",
|
|
37
|
-
stderr: result.stderr?.trim() ?? "",
|
|
38
|
-
};
|
|
71
|
+
return toCommandResult(result);
|
|
39
72
|
}
|
|
40
73
|
/**
|
|
41
|
-
*
|
|
74
|
+
* Whether any lockfile differs between `baseRef` and the current HEAD.
|
|
75
|
+
*
|
|
76
|
+
* @internal Exported for testing
|
|
77
|
+
*/
|
|
78
|
+
export function lockfileChanged(repoRoot, baseRef) {
|
|
79
|
+
const result = git(["diff", "--name-only", baseRef, "HEAD", "--", ...LOCKFILES], repoRoot);
|
|
80
|
+
return result.ok && result.stdout.length > 0;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create temp branch, merge all feature branches, reinstall dependencies if the
|
|
84
|
+
* lockfile moved, then run tests and build.
|
|
42
85
|
*
|
|
43
86
|
* @param branches - Feature branches to merge
|
|
44
87
|
* @param repoRoot - Path to the git repository root
|
|
@@ -49,123 +92,203 @@ export function runCombinedBranchTest(branches, repoRoot) {
|
|
|
49
92
|
const tempBranch = `merge-check/temp-${Date.now()}`;
|
|
50
93
|
const branchResults = [];
|
|
51
94
|
const batchFindings = [];
|
|
52
|
-
const
|
|
53
|
-
//
|
|
95
|
+
const pm = detectPackageManagerSync(repoRoot);
|
|
96
|
+
// Resolved against the repo rather than read straight off PM_CONFIG: yarn's
|
|
97
|
+
// frozen install differs between classic and berry, and `pm` is "yarn" for
|
|
98
|
+
// both (#871).
|
|
99
|
+
//
|
|
100
|
+
// This resolution describes the PRE-MERGE tree, which is what the restore
|
|
101
|
+
// install in the `finally` below runs against — it fires after
|
|
102
|
+
// `git checkout restoreBranch`, so the original branch's yarn major is the
|
|
103
|
+
// right one there. `runChecks` re-resolves for the combined state, where a
|
|
104
|
+
// merged branch may have changed the major.
|
|
105
|
+
const pmConfig = resolvePackageManagerConfig(pm, repoRoot);
|
|
106
|
+
// Flipped the moment we start installing against the combined lockfile, so
|
|
107
|
+
// the caller's node_modules can be put back afterwards. A mutable holder
|
|
108
|
+
// rather than a return value: if runChecks throws, a return value never
|
|
109
|
+
// arrives and the restore would be skipped precisely when the tree is most
|
|
110
|
+
// likely to be inconsistent.
|
|
111
|
+
const installState = { installedCombinedDeps: false };
|
|
112
|
+
// Save current branch to restore in the cleanup step
|
|
54
113
|
const originalBranch = git(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
|
|
55
114
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
issueNumber: branch.issueNumber,
|
|
79
|
-
verdict: "PASS",
|
|
80
|
-
findings: [
|
|
81
|
-
{
|
|
82
|
-
check: "combined-branch-test",
|
|
83
|
-
severity: "info",
|
|
84
|
-
message: `Branch merged cleanly into combined state`,
|
|
85
|
-
issueNumber: branch.issueNumber,
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
// Get conflicting files
|
|
92
|
-
const conflictResult = git(["diff", "--name-only", "--diff-filter=U"], repoRoot);
|
|
93
|
-
const conflictFiles = conflictResult.stdout
|
|
94
|
-
? conflictResult.stdout.split("\n")
|
|
95
|
-
: [];
|
|
96
|
-
mergeAttempts.push({
|
|
97
|
-
issueNumber: branch.issueNumber,
|
|
98
|
-
branch: branch.branch,
|
|
99
|
-
success: false,
|
|
100
|
-
conflictFiles,
|
|
101
|
-
error: mergeResult.stderr,
|
|
102
|
-
});
|
|
103
|
-
branchResults.push({
|
|
104
|
-
issueNumber: branch.issueNumber,
|
|
105
|
-
verdict: "FAIL",
|
|
106
|
-
findings: [
|
|
107
|
-
{
|
|
108
|
-
check: "combined-branch-test",
|
|
109
|
-
severity: "error",
|
|
110
|
-
message: `Merge conflict with ${conflictFiles.length} file(s): ${conflictFiles.join(", ")}`,
|
|
111
|
-
issueNumber: branch.issueNumber,
|
|
112
|
-
},
|
|
113
|
-
],
|
|
115
|
+
runChecks(branches, repoRoot, tempBranch, pm, branchResults, batchFindings, installState);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
// Restore original branch and delete temp branch
|
|
119
|
+
const restoreBranch = originalBranch.ok ? originalBranch.stdout : "main";
|
|
120
|
+
git(["checkout", restoreBranch], repoRoot);
|
|
121
|
+
git(["branch", "-D", tempBranch], repoRoot);
|
|
122
|
+
// We installed the combined state's dependency tree into the caller's
|
|
123
|
+
// node_modules; put it back so the restored branch is not left running
|
|
124
|
+
// against another branch's dependencies.
|
|
125
|
+
//
|
|
126
|
+
// Frozen again, for the same reason as the forward install: a plain
|
|
127
|
+
// `npm install` normalizes and rewrites the lockfile, which would leave
|
|
128
|
+
// the user's restored branch with a dirty working tree that this check
|
|
129
|
+
// never asked them for.
|
|
130
|
+
if (installState.installedCombinedDeps) {
|
|
131
|
+
const restoreInstall = runPackageManagerCommand(pmConfig.ciInstall, repoRoot, INSTALL_TIMEOUT_MS);
|
|
132
|
+
if (!restoreInstall.ok) {
|
|
133
|
+
batchFindings.push({
|
|
134
|
+
check: "combined-branch-test",
|
|
135
|
+
severity: "warning",
|
|
136
|
+
message: `Dependencies were reinstalled for the combined state but could not be restored for \`${restoreBranch}\`. Run \`${pmConfig.ciInstall}\` to resync node_modules: ${resolveFailureReason(restoreInstall)}`,
|
|
114
137
|
});
|
|
115
|
-
// Abort the failed merge and continue
|
|
116
|
-
git(["merge", "--abort"], repoRoot);
|
|
117
138
|
}
|
|
118
139
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
}
|
|
141
|
+
// Built after cleanup so findings pushed during cleanup are reflected in the
|
|
142
|
+
// verdict rather than silently arriving too late to affect `passed`.
|
|
143
|
+
return buildResult(branchResults, batchFindings, startTime);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Assemble the combined state and run install/test/build against it.
|
|
147
|
+
*
|
|
148
|
+
* Mutates `branchResults`, `batchFindings`, and `installState` — the last so
|
|
149
|
+
* the caller's cleanup can restore node_modules even if this function throws.
|
|
150
|
+
*/
|
|
151
|
+
function runChecks(branches, repoRoot, tempBranch, pm, branchResults, batchFindings, installState) {
|
|
152
|
+
const mergeAttempts = [];
|
|
153
|
+
// Fetch latest from remote
|
|
154
|
+
git(["fetch", "origin"], repoRoot);
|
|
155
|
+
// Create temp branch from main
|
|
156
|
+
const createResult = git(["checkout", "-b", tempBranch, BASE_REF], repoRoot);
|
|
157
|
+
if (!createResult.ok) {
|
|
158
|
+
batchFindings.push({
|
|
159
|
+
check: "combined-branch-test",
|
|
160
|
+
severity: "error",
|
|
161
|
+
message: `Failed to create temp branch: ${createResult.stderr}`,
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Merge each feature branch
|
|
166
|
+
for (const branch of branches) {
|
|
167
|
+
const mergeResult = git(["merge", "--no-ff", "--no-edit", getBranchRef(branch)], repoRoot);
|
|
168
|
+
if (mergeResult.ok) {
|
|
169
|
+
mergeAttempts.push({
|
|
170
|
+
issueNumber: branch.issueNumber,
|
|
171
|
+
branch: branch.branch,
|
|
172
|
+
success: true,
|
|
126
173
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
174
|
+
branchResults.push({
|
|
175
|
+
issueNumber: branch.issueNumber,
|
|
176
|
+
verdict: "PASS",
|
|
177
|
+
findings: [
|
|
178
|
+
{
|
|
179
|
+
check: "combined-branch-test",
|
|
180
|
+
severity: "info",
|
|
181
|
+
message: `Branch merged cleanly into combined state`,
|
|
182
|
+
issueNumber: branch.issueNumber,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
136
185
|
});
|
|
137
186
|
}
|
|
138
187
|
else {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
188
|
+
// Get conflicting files
|
|
189
|
+
const conflictResult = git(["diff", "--name-only", "--diff-filter=U"], repoRoot);
|
|
190
|
+
const conflictFiles = conflictResult.stdout
|
|
191
|
+
? conflictResult.stdout.split("\n")
|
|
192
|
+
: [];
|
|
193
|
+
mergeAttempts.push({
|
|
194
|
+
issueNumber: branch.issueNumber,
|
|
195
|
+
branch: branch.branch,
|
|
196
|
+
success: false,
|
|
197
|
+
conflictFiles,
|
|
198
|
+
error: mergeResult.stderr,
|
|
143
199
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
200
|
+
branchResults.push({
|
|
201
|
+
issueNumber: branch.issueNumber,
|
|
202
|
+
verdict: "FAIL",
|
|
203
|
+
findings: [
|
|
204
|
+
{
|
|
205
|
+
check: "combined-branch-test",
|
|
206
|
+
severity: "error",
|
|
207
|
+
message: `Merge conflict with ${conflictFiles.length} file(s): ${conflictFiles.join(", ")}`,
|
|
208
|
+
issueNumber: branch.issueNumber,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
152
211
|
});
|
|
212
|
+
// Abort the failed merge and continue
|
|
213
|
+
git(["merge", "--abort"], repoRoot);
|
|
153
214
|
}
|
|
154
|
-
|
|
215
|
+
}
|
|
216
|
+
// If any merges failed, skip tests but report what we have
|
|
217
|
+
const failedMerges = mergeAttempts.filter((m) => !m.success);
|
|
218
|
+
if (failedMerges.length > 0) {
|
|
219
|
+
batchFindings.push({
|
|
220
|
+
check: "combined-branch-test",
|
|
221
|
+
severity: "error",
|
|
222
|
+
message: `${failedMerges.length}/${branches.length} branches had merge conflicts — skipping test/build`,
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// Resolved HERE, after the merges landed, not from the caller's pre-merge
|
|
227
|
+
// resolution: the commands below run against the combined tree, and a merged
|
|
228
|
+
// branch can move the yarn major (a `packageManager` bump, a new
|
|
229
|
+
// `.yarnrc.yml`). Installing the combined state with the pre-merge major's
|
|
230
|
+
// frozen-install flag would fail on an unknown option and report a false
|
|
231
|
+
// BLOCKED — the exact failure class #803 added this install to prevent (#871).
|
|
232
|
+
const pmConfig = resolvePackageManagerConfig(pm, repoRoot);
|
|
233
|
+
// Reinstall dependencies when the merged branches moved the lockfile (#803).
|
|
234
|
+
// Without this, test/build run against the node_modules of whatever branch
|
|
235
|
+
// the user happened to be on, so a batch that merely added a dependency fails
|
|
236
|
+
// with module-not-found errors — a false BLOCKED on a healthy stack.
|
|
237
|
+
//
|
|
238
|
+
// A frozen install is used rather than a plain one: it mirrors what CI does,
|
|
239
|
+
// it will not rewrite the lockfile (which would dirty the temp branch and
|
|
240
|
+
// break the checkout during cleanup), and it fails loudly on a lockfile that
|
|
241
|
+
// is inconsistent with package.json.
|
|
242
|
+
if (lockfileChanged(repoRoot, BASE_REF)) {
|
|
243
|
+
// Marked before the install runs, not after: a frozen install deletes
|
|
244
|
+
// node_modules up front, so even a failed or interrupted one leaves the
|
|
245
|
+
// caller's tree needing a restore.
|
|
246
|
+
installState.installedCombinedDeps = true;
|
|
247
|
+
const installResult = runPackageManagerCommand(pmConfig.ciInstall, repoRoot, INSTALL_TIMEOUT_MS);
|
|
248
|
+
if (!installResult.ok) {
|
|
249
|
+
// Surface install failures on their own terms rather than letting them
|
|
250
|
+
// resurface downstream as an unexplained test failure.
|
|
155
251
|
batchFindings.push({
|
|
156
252
|
check: "combined-branch-test",
|
|
157
253
|
severity: "error",
|
|
158
|
-
message: `
|
|
254
|
+
message: `Dependency install failed on combined state (\`${pmConfig.ciInstall}\`) — skipping test/build. The merged lockfile may be inconsistent with package.json: ${resolveFailureReason(installResult)}`,
|
|
159
255
|
});
|
|
256
|
+
return;
|
|
160
257
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
git(["checkout", restoreBranch], repoRoot);
|
|
167
|
-
git(["branch", "-D", tempBranch], repoRoot);
|
|
258
|
+
batchFindings.push({
|
|
259
|
+
check: "combined-branch-test",
|
|
260
|
+
severity: "info",
|
|
261
|
+
message: `Lockfile changed in the combined state — reinstalled dependencies with \`${pmConfig.ciInstall}\``,
|
|
262
|
+
});
|
|
168
263
|
}
|
|
264
|
+
// Run the test suite
|
|
265
|
+
const testCommand = `${pmConfig.run} test`;
|
|
266
|
+
const testResult = runPackageManagerCommand(testCommand, repoRoot, TEST_BUILD_TIMEOUT_MS);
|
|
267
|
+
batchFindings.push(testResult.ok
|
|
268
|
+
? {
|
|
269
|
+
check: "combined-branch-test",
|
|
270
|
+
severity: "info",
|
|
271
|
+
message: `\`${testCommand}\` passed on combined state`,
|
|
272
|
+
}
|
|
273
|
+
: {
|
|
274
|
+
check: "combined-branch-test",
|
|
275
|
+
severity: "error",
|
|
276
|
+
message: `\`${testCommand}\` failed on combined state: ${resolveFailureReason(testResult)}`,
|
|
277
|
+
});
|
|
278
|
+
// Run the build
|
|
279
|
+
const buildCommand = `${pmConfig.run} build`;
|
|
280
|
+
const buildOutcome = runPackageManagerCommand(buildCommand, repoRoot, TEST_BUILD_TIMEOUT_MS);
|
|
281
|
+
batchFindings.push(buildOutcome.ok
|
|
282
|
+
? {
|
|
283
|
+
check: "combined-branch-test",
|
|
284
|
+
severity: "info",
|
|
285
|
+
message: `\`${buildCommand}\` passed on combined state`,
|
|
286
|
+
}
|
|
287
|
+
: {
|
|
288
|
+
check: "combined-branch-test",
|
|
289
|
+
severity: "error",
|
|
290
|
+
message: `\`${buildCommand}\` failed on combined state: ${resolveFailureReason(buildOutcome)}`,
|
|
291
|
+
});
|
|
169
292
|
}
|
|
170
293
|
export function buildResult(branchResults, batchFindings, startTime) {
|
|
171
294
|
const hasErrors = batchFindings.some((f) => f.severity === "error");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized spawn results and never-empty failure reasons (#803)
|
|
3
|
+
*
|
|
4
|
+
* Shared by every merge-check module that reports on a spawned command. The
|
|
5
|
+
* motivating defect: a failure reason built from `stderr` alone renders as
|
|
6
|
+
* `... failed: ` with nothing after it whenever the command wrote its
|
|
7
|
+
* diagnostics to stdout — which is where vitest, tsc, and npm put theirs.
|
|
8
|
+
*/
|
|
9
|
+
import type { SpawnSyncReturns } from "child_process";
|
|
10
|
+
/** Max characters of captured output included in a failure message. */
|
|
11
|
+
export declare const REASON_MAX_CHARS = 500;
|
|
12
|
+
/**
|
|
13
|
+
* Outcome of a spawned command, normalized across success, non-zero exit,
|
|
14
|
+
* signal termination, and spawn failure.
|
|
15
|
+
*/
|
|
16
|
+
export interface CommandResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
status: number | null;
|
|
21
|
+
signal: string | null;
|
|
22
|
+
spawnError?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a raw `spawnSync` return into a {@link CommandResult}.
|
|
26
|
+
*/
|
|
27
|
+
export declare function toCommandResult(result: SpawnSyncReturns<string>): CommandResult;
|
|
28
|
+
/**
|
|
29
|
+
* Produce a human-readable, never-empty reason for a failed command.
|
|
30
|
+
*
|
|
31
|
+
* Falls through: stderr → stdout tail → spawn error → signal → exit code. The
|
|
32
|
+
* stdout *tail* is used because test runners put the failure summary last,
|
|
33
|
+
* while the stderr *head* is kept because the first error is usually the
|
|
34
|
+
* proximate cause.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveFailureReason(result: CommandResult): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized spawn results and never-empty failure reasons (#803)
|
|
3
|
+
*
|
|
4
|
+
* Shared by every merge-check module that reports on a spawned command. The
|
|
5
|
+
* motivating defect: a failure reason built from `stderr` alone renders as
|
|
6
|
+
* `... failed: ` with nothing after it whenever the command wrote its
|
|
7
|
+
* diagnostics to stdout — which is where vitest, tsc, and npm put theirs.
|
|
8
|
+
*/
|
|
9
|
+
/** Max characters of captured output included in a failure message. */
|
|
10
|
+
export const REASON_MAX_CHARS = 500;
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a raw `spawnSync` return into a {@link CommandResult}.
|
|
13
|
+
*/
|
|
14
|
+
export function toCommandResult(result) {
|
|
15
|
+
return {
|
|
16
|
+
ok: result.status === 0,
|
|
17
|
+
stdout: result.stdout?.trim() ?? "",
|
|
18
|
+
stderr: result.stderr?.trim() ?? "",
|
|
19
|
+
status: result.status ?? null,
|
|
20
|
+
signal: result.signal ?? null,
|
|
21
|
+
spawnError: result.error?.message,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Produce a human-readable, never-empty reason for a failed command.
|
|
26
|
+
*
|
|
27
|
+
* Falls through: stderr → stdout tail → spawn error → signal → exit code. The
|
|
28
|
+
* stdout *tail* is used because test runners put the failure summary last,
|
|
29
|
+
* while the stderr *head* is kept because the first error is usually the
|
|
30
|
+
* proximate cause.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveFailureReason(result) {
|
|
33
|
+
if (result.stderr) {
|
|
34
|
+
return truncateHead(result.stderr);
|
|
35
|
+
}
|
|
36
|
+
if (result.stdout) {
|
|
37
|
+
return truncateTail(result.stdout);
|
|
38
|
+
}
|
|
39
|
+
if (result.spawnError) {
|
|
40
|
+
return result.spawnError;
|
|
41
|
+
}
|
|
42
|
+
if (result.signal) {
|
|
43
|
+
return `no output; killed by signal ${result.signal} (likely a timeout)`;
|
|
44
|
+
}
|
|
45
|
+
return `no output; exited with code ${result.status ?? "unknown"}`;
|
|
46
|
+
}
|
|
47
|
+
function truncateHead(text) {
|
|
48
|
+
return text.length > REASON_MAX_CHARS
|
|
49
|
+
? `${text.slice(0, REASON_MAX_CHARS)}…`
|
|
50
|
+
: text;
|
|
51
|
+
}
|
|
52
|
+
function truncateTail(text) {
|
|
53
|
+
return text.length > REASON_MAX_CHARS
|
|
54
|
+
? `…${text.slice(-REASON_MAX_CHARS)}`
|
|
55
|
+
: text;
|
|
56
|
+
}
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
import { type RunLog } from "../workflow/run-log-schema.js";
|
|
8
8
|
import type { BranchInfo, CheckResult, MergeCommandOptions, MergeReport } from "./types.js";
|
|
9
9
|
import { formatReportMarkdown, formatBranchReportMarkdown, postReportToGitHub } from "./report.js";
|
|
10
|
+
/**
|
|
11
|
+
* Resolve log directory path
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveLogDir(customPath?: string): string;
|
|
10
14
|
/**
|
|
11
15
|
* Find the most recent run log file
|
|
12
16
|
*/
|
|
@@ -20,7 +20,7 @@ import { buildReport, formatReportMarkdown, formatBranchReportMarkdown, postRepo
|
|
|
20
20
|
/**
|
|
21
21
|
* Resolve log directory path
|
|
22
22
|
*/
|
|
23
|
-
function resolveLogDir(customPath) {
|
|
23
|
+
export function resolveLogDir(customPath) {
|
|
24
24
|
if (customPath) {
|
|
25
25
|
return customPath.replace("~", os.homedir());
|
|
26
26
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* batch-level verdict. Optionally posts to GitHub as PR comment.
|
|
6
6
|
*/
|
|
7
7
|
import { spawnSync } from "child_process";
|
|
8
|
+
import { resolveFailureReason, toCommandResult } from "./command-result.js";
|
|
8
9
|
/**
|
|
9
10
|
* Compute per-issue verdicts from check results
|
|
10
11
|
*/
|
|
@@ -232,7 +233,10 @@ export function postReportToGitHub(report) {
|
|
|
232
233
|
const markdown = formatBranchReportMarkdown(report, branch.issueNumber);
|
|
233
234
|
const result = spawnSync("gh", ["pr", "comment", String(branch.prNumber), "--body", markdown], { stdio: "pipe", encoding: "utf-8" });
|
|
234
235
|
if (result.status !== 0) {
|
|
235
|
-
|
|
236
|
+
// Same never-empty reason resolution as the combined-branch test (#803):
|
|
237
|
+
// `gh` usually writes to stderr, but a killed or unspawnable process
|
|
238
|
+
// leaves it empty, which would print a message with nothing after it.
|
|
239
|
+
console.error(`Failed to post comment on PR #${branch.prNumber}: ${resolveFailureReason(toCommandResult(result))}`);
|
|
236
240
|
}
|
|
237
241
|
}
|
|
238
242
|
}
|
|
@@ -171,6 +171,15 @@ export interface MergeCommandOptions {
|
|
|
171
171
|
json?: boolean;
|
|
172
172
|
/** Enable verbose output */
|
|
173
173
|
verbose?: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* Watch mode (#818): poll each resolved PR's CI rollup until every check is
|
|
176
|
+
* terminal, then run the normal merge-check report. Never merges.
|
|
177
|
+
*/
|
|
178
|
+
watch?: boolean;
|
|
179
|
+
/** Watch poll interval in seconds (default 30). Only used with `watch`. */
|
|
180
|
+
interval?: number;
|
|
181
|
+
/** Watch give-up timeout in seconds (default 1800). Only used with `watch`. */
|
|
182
|
+
timeout?: number;
|
|
174
183
|
}
|
|
175
184
|
/**
|
|
176
185
|
* Configuration for paired directories (template mirroring)
|